From 7a4aa1e98c46ff7ef08cda0eeb638c3d5b6477b9 Mon Sep 17 00:00:00 2001
From: Zach Fox <fox@highfidelity.io>
Date: Tue, 18 Jun 2019 17:13:38 -0700
Subject: [PATCH] BUGZ-657; BUGZ-713: Add a copy of the system library JS files
 because lots of system scripts depend on them being in the default scripts
 location

---
 .../system/libraries/EditEntityList.qml       |   12 +
 .../simplifiedUI/system/libraries/ToolTip.js  |   85 +
 .../simplifiedUI/system/libraries/Trigger.js  |   87 +
 .../system/libraries/WebTablet.js             |  606 ++++
 .../simplifiedUI/system/libraries/Xform.js    |   48 +
 .../system/libraries/accountUtils.js          |   10 +
 .../system/libraries/cloneEntityUtils.js      |   57 +
 .../system/libraries/connectionUtils.js       |   94 +
 .../libraries/controllerDispatcherUtils.js    |  629 ++++
 .../system/libraries/controllers.js           |   81 +
 .../system/libraries/dataViewHelpers.js       |   62 +
 .../system/libraries/entityCameraTool.js      |  657 ++++
 .../libraries/entityIconOverlayManager.js     |  155 +
 .../system/libraries/entityList.js            |  330 ++
 .../system/libraries/entitySelectionTool.js   | 2925 +++++++++++++++++
 .../simplifiedUI/system/libraries/globals.js  |   13 +
 .../simplifiedUI/system/libraries/gridTool.js |  311 ++
 .../system/libraries/overlayUtils.js          |   74 +
 .../system/libraries/pointersUtils.js         |  204 ++
 .../system/libraries/progressDialog.js        |  147 +
 .../system/libraries/soundArray.js            |   42 +
 .../system/libraries/stringHelpers.js         |  476 +++
 .../simplifiedUI/system/libraries/toolBars.js |  493 +++
 .../system/libraries/touchEventUtils.js       |  250 ++
 .../simplifiedUI/system/libraries/utils.js    |  504 +++
 25 files changed, 8352 insertions(+)
 create mode 100644 scripts/simplifiedUI/system/libraries/EditEntityList.qml
 create mode 100644 scripts/simplifiedUI/system/libraries/ToolTip.js
 create mode 100644 scripts/simplifiedUI/system/libraries/Trigger.js
 create mode 100644 scripts/simplifiedUI/system/libraries/WebTablet.js
 create mode 100644 scripts/simplifiedUI/system/libraries/Xform.js
 create mode 100644 scripts/simplifiedUI/system/libraries/accountUtils.js
 create mode 100644 scripts/simplifiedUI/system/libraries/cloneEntityUtils.js
 create mode 100644 scripts/simplifiedUI/system/libraries/connectionUtils.js
 create mode 100644 scripts/simplifiedUI/system/libraries/controllerDispatcherUtils.js
 create mode 100644 scripts/simplifiedUI/system/libraries/controllers.js
 create mode 100644 scripts/simplifiedUI/system/libraries/dataViewHelpers.js
 create mode 100644 scripts/simplifiedUI/system/libraries/entityCameraTool.js
 create mode 100644 scripts/simplifiedUI/system/libraries/entityIconOverlayManager.js
 create mode 100644 scripts/simplifiedUI/system/libraries/entityList.js
 create mode 100644 scripts/simplifiedUI/system/libraries/entitySelectionTool.js
 create mode 100644 scripts/simplifiedUI/system/libraries/globals.js
 create mode 100644 scripts/simplifiedUI/system/libraries/gridTool.js
 create mode 100644 scripts/simplifiedUI/system/libraries/overlayUtils.js
 create mode 100644 scripts/simplifiedUI/system/libraries/pointersUtils.js
 create mode 100644 scripts/simplifiedUI/system/libraries/progressDialog.js
 create mode 100644 scripts/simplifiedUI/system/libraries/soundArray.js
 create mode 100644 scripts/simplifiedUI/system/libraries/stringHelpers.js
 create mode 100644 scripts/simplifiedUI/system/libraries/toolBars.js
 create mode 100644 scripts/simplifiedUI/system/libraries/touchEventUtils.js
 create mode 100644 scripts/simplifiedUI/system/libraries/utils.js

diff --git a/scripts/simplifiedUI/system/libraries/EditEntityList.qml b/scripts/simplifiedUI/system/libraries/EditEntityList.qml
new file mode 100644
index 0000000000..4fc5ff19ef
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/EditEntityList.qml
@@ -0,0 +1,12 @@
+import QtQuick 2.7
+import QtQuick.Controls 2.2
+import QtWebChannel 1.0
+import QtGraphicalEffects 1.0
+import "qrc:///qml/controls" as HifiControls
+
+HifiControls.WebView {
+    id: entityListToolWebView
+    url: Qt.resolvedUrl("../html/entityList.html")
+    enabled: true
+    blurOnCtrlShift: false
+}
diff --git a/scripts/simplifiedUI/system/libraries/ToolTip.js b/scripts/simplifiedUI/system/libraries/ToolTip.js
new file mode 100644
index 0000000000..f6f14d7f2b
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/ToolTip.js
@@ -0,0 +1,85 @@
+//
+//  ToolTip.js
+//  examples/libraries
+//
+//  Copyright 2014 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 Tooltip() {
+    this.x = 285;
+    this.y = 115;
+    this.width = 500;
+    this.height = 180; // 145;
+    this.margin = 5;
+    this.decimals = 3;
+
+    this.textOverlay = Overlays.addOverlay("text", {
+        x: this.x,
+        y: this.y,
+        width: this.width,
+        height: this.height,
+        margin: this.margin,
+        text: "",
+        color: { red: 128, green: 128, blue: 128 },
+        alpha: 0.2,
+        backgroundAlpha: 0.2,
+        visible: false
+    });
+    this.show = function (doShow) {
+        Overlays.editOverlay(this.textOverlay, { visible: doShow });
+    }
+    this.updateText = function(properties) {
+        var angles = Quat.safeEulerAngles(properties.rotation);
+        var text = "Entity Properties:\n"
+        text += "type: " + properties.type + "\n"
+        text += "X: " + properties.position.x.toFixed(this.decimals) + "\n"
+        text += "Y: " + properties.position.y.toFixed(this.decimals) + "\n"
+        text += "Z: " + properties.position.z.toFixed(this.decimals) + "\n"
+        text += "Pitch: " + angles.x.toFixed(this.decimals) + "\n"
+        text += "Yaw:  " + angles.y.toFixed(this.decimals) + "\n"
+        text += "Roll:    " + angles.z.toFixed(this.decimals) + "\n"
+        text += "Dimensions: " + properties.dimensions.x.toFixed(this.decimals) + ", "
+                               + properties.dimensions.y.toFixed(this.decimals) + ", "
+                               + properties.dimensions.z.toFixed(this.decimals) + "\n";
+
+        text += "Natural Dimensions: " + properties.naturalDimensions.x.toFixed(this.decimals) + ", "
+                                       + properties.naturalDimensions.y.toFixed(this.decimals) + ", "
+                                       + properties.naturalDimensions.z.toFixed(this.decimals) + "\n";
+
+        text += "ID: " + properties.id + "\n"
+        if (properties.type == "Model") {
+            text += "Model URL: " + properties.modelURL + "\n"
+            text += "Shape Type: " + properties.shapeType + "\n"
+            text += "Compound Shape URL: " + properties.compoundShapeURL + "\n"
+            text += "Animation URL: " + properties.animationURL + "\n"
+            text += "Animation is playing: " + properties.animationIsPlaying + "\n"
+            if (properties.sittingPoints && properties.sittingPoints.length > 0) {
+                text += properties.sittingPoints.length + " Sitting points: "
+                for (var i = 0; i < properties.sittingPoints.length; ++i) {
+                    text += properties.sittingPoints[i].name + " "
+                }
+            } else {
+                text += "No sitting points" + "\n"
+            }
+        }
+        if (properties.lifetime > -1) {
+            text += "Lifetime: " + properties.lifetime + "\n"
+        }
+        text += "Age: " + properties.ageAsText + "\n"
+        text += "Density: " + properties.density + "\n"
+        text += "Script: " + properties.script + "\n"
+
+
+        Overlays.editOverlay(this.textOverlay, { text: text });
+    }
+
+    this.cleanup = function () {
+        Overlays.deleteOverlay(this.textOverlay);
+    }
+}
+
+tooltip = new Tooltip();
diff --git a/scripts/simplifiedUI/system/libraries/Trigger.js b/scripts/simplifiedUI/system/libraries/Trigger.js
new file mode 100644
index 0000000000..ffde021f5d
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/Trigger.js
@@ -0,0 +1,87 @@
+"use strict";
+
+/*jslint vars: true, plusplus: true*/
+/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Menu, ScriptDiscoveryService, Window, Vec3, Quat, print*/
+
+Trigger = function(properties) {
+    properties = properties || {};
+    var that = this;
+    that.label = properties.label || Math.random();
+    that.SMOOTH_RATIO = properties.smooth || 0.1; //  Time averaging of trigger - 0.0 disables smoothing
+    that.DEADZONE = properties.deadzone || 0.10; // Once pressed, a trigger must fall below the deadzone to be considered un-pressed once pressed.
+    that.HYSTERESIS = properties.hystersis || 0.05; // If not pressed, a trigger must go above DEADZONE + HYSTERSIS to be considered pressed
+
+    that.value = 0;
+    that.pressed = false;
+    that.clicked = false;
+
+    // Handlers
+    that.onPress = properties.onPress || function(){
+        print("Pressed trigger " + that.label)
+    };
+    that.onRelease = properties.onRelease || function(){
+        print("Released trigger " + that.label)
+    };
+    that.onClick = properties.onClick || function(){
+        print("Clicked trigger " + that.label)
+    };
+    that.onUnclick = properties.onUnclick || function(){
+        print("Unclicked trigger " + that.label)
+    };
+    
+    // Getters
+    that.isPressed = function() {
+        return that.pressed;
+    }
+    
+    that.isClicked = function() {
+        return that.clicked;
+    }
+    
+    that.getValue = function() {
+        return that.value;
+    }
+
+    
+    // Private values
+    var controller = properties.controller || Controller.Standard.LT;
+    var controllerClick = properties.controllerClick || Controller.Standard.LTClick;
+    that.mapping =  Controller.newMapping('com.highfidelity.controller.trigger.' + controller + '-' + controllerClick + '.' + that.label + Math.random());
+    Script.scriptEnding.connect(that.mapping.disable);
+
+    // Setup mapping,
+    that.mapping.from(controller).peek().to(function(value) {
+        that.value = (that.value * that.SMOOTH_RATIO) +
+            (value * (1.0 - that.SMOOTH_RATIO)); 
+
+        var oldPressed = that.pressed;
+        if (!that.pressed && that.value >= (that.DEADZONE + that.HYSTERESIS)) {
+            that.pressed = true;
+            that.onPress();
+        }
+
+        if (that.pressed && that.value < that.HYSTERESIS) {
+            that.pressed = false;
+            that.onRelease();
+        }
+    });
+    
+    that.mapping.from(controllerClick).peek().to(function(value){
+        if (!that.clicked && value > 0.0) {
+            that.clicked = true;
+            that.onClick();
+        } 
+        if (that.clicked && value == 0.0) {
+            that.clicked = false;
+            that.onUnclick();
+        }
+    });
+
+    that.enable = function() {
+        that.mapping.enable();
+    }
+
+    that.disable = function() {
+        that.mapping.disable();
+    }
+}
diff --git a/scripts/simplifiedUI/system/libraries/WebTablet.js b/scripts/simplifiedUI/system/libraries/WebTablet.js
new file mode 100644
index 0000000000..9d333a1ae4
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/WebTablet.js
@@ -0,0 +1,606 @@
+//
+//  WebTablet.js
+//
+//  Created by Anthony J. Thibault on 8/8/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
+//
+/* global getControllerWorldLocation, Tablet, WebTablet:true, HMD, Settings, Script,
+   Vec3, Quat, MyAvatar, Entities, Overlays, Camera, Messages, Xform, clamp, Controller, Mat4, resizeTablet */
+
+Script.include(Script.resolvePath("../libraries/utils.js"));
+Script.include(Script.resolvePath("../libraries/controllers.js"));
+Script.include(Script.resolvePath("../libraries/Xform.js"));
+
+var Y_AXIS = {x: 0, y: 1, z: 0};
+var X_AXIS = {x: 1, y: 0, z: 0};
+var DEFAULT_DPI = 31;
+var DEFAULT_WIDTH = 0.4375;
+var DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees
+var SENSOR_TO_ROOM_MATRIX = -2;
+var CAMERA_MATRIX = -7;
+var ROT_Y_180 = {x: 0.0, y: 1.0, z: 0, w: 0};
+var ROT_LANDSCAPE = {x: 1.0, y: 1.0, z: 0, w: 0};
+var TABLET_TEXTURE_RESOLUTION = { x: 480, y: 706 };
+var INCHES_TO_METERS = 1 / 39.3701;
+
+var NO_HANDS = -1;
+var DELAY_FOR_30HZ = 33; // milliseconds
+
+var TABLET_MATERIAL_ENTITY_NAME = 'Tablet-Material-Entity';
+
+
+// will need to be recaclulated if dimensions of fbx model change.
+var TABLET_NATURAL_DIMENSIONS = {x: 32.083, y: 48.553, z: 2.269};
+
+var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "images/button-close.png";
+// var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-close.png";
+// var TABLET_MODEL_PATH = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx";
+
+var LOCAL_TABLET_MODEL_PATH = Script.resourcesPath() + "meshes/tablet-with-home-button-small-bezel.fbx";
+var HIGH_PRIORITY = 1;
+var LOW_PRIORITY = 0;
+var SUBMESH = 2;
+
+// returns object with two fields:
+//    * position - position in front of the user
+//    * rotation - rotation of entity so it faces the user.
+function calcSpawnInfo(hand, landscape) {
+    var finalPosition;
+
+    var LEFT_HAND = Controller.Standard.LeftHand;
+    var sensorToWorldScale = MyAvatar.sensorToWorldScale;
+    var headPos = (HMD.active && Camera.mode === "first person") ? HMD.position : Camera.position;
+    var headRot = Quat.cancelOutRollAndPitch((HMD.active && Camera.mode === "first person") ?
+        HMD.orientation : Camera.orientation);
+
+    var right = Quat.getRight(headRot);
+    var forward = Quat.getForward(headRot);
+    var up = Quat.getUp(headRot);
+
+    var FORWARD_OFFSET = 0.5 * sensorToWorldScale;
+    var UP_OFFSET = -0.16 * sensorToWorldScale;
+    var RIGHT_OFFSET = ((hand === LEFT_HAND) ? -0.18 : 0.18) * sensorToWorldScale;
+
+    var forwardPosition = Vec3.sum(headPos, Vec3.multiply(FORWARD_OFFSET, forward));
+    var lateralPosition = Vec3.sum(forwardPosition, Vec3.multiply(RIGHT_OFFSET, right));
+    finalPosition = Vec3.sum(lateralPosition, Vec3.multiply(UP_OFFSET, up));
+
+    var MY_EYES = { x: 0.0, y: 0.15, z: 0.0 };
+    var lookAtEndPosition = Vec3.sum(Vec3.multiply(RIGHT_OFFSET, right), Vec3.multiply(FORWARD_OFFSET, forward));
+    var orientation = Quat.lookAt(MY_EYES, lookAtEndPosition, Vec3.multiplyQbyV(MyAvatar.orientation, Vec3.UNIT_Y));
+
+    return {
+        position: finalPosition,
+        rotation: landscape ? Quat.multiply(orientation, ROT_LANDSCAPE) : Quat.multiply(orientation, ROT_Y_180)
+    };
+}
+
+
+cleanUpOldMaterialEntities = function() {
+    var avatarEntityData = MyAvatar.getAvatarEntityData();
+    for (var entityID in avatarEntityData) {
+        var entityName = Entities.getEntityProperties(entityID, ["name"]).name;
+
+        if (entityName === TABLET_MATERIAL_ENTITY_NAME) {
+            Entities.deleteEntity(entityID);
+        }
+    }
+};
+
+/**
+ * WebTablet
+ * @param url [string] url of content to show on the tablet.
+ * @param width [number] width in meters of the tablet model
+ * @param dpi [number] dpi of web surface used to show the content.
+ * @param hand [number] -1 indicates no hand, Controller.Standard.RightHand or Controller.Standard.LeftHand
+ */
+WebTablet = function (url, width, dpi, hand, location, visible) {
+
+    var _this = this;
+
+    var sensorScaleFactor = MyAvatar.sensorToWorldScale;
+
+    // scale factor of natural tablet dimensions.
+    var tabletWidth = (width || DEFAULT_WIDTH) * sensorScaleFactor;
+    var tabletScaleFactor = tabletWidth / TABLET_NATURAL_DIMENSIONS.x;
+    var tabletHeight = TABLET_NATURAL_DIMENSIONS.y * tabletScaleFactor;
+    var tabletDepth = TABLET_NATURAL_DIMENSIONS.z * tabletScaleFactor;
+    this.landscape = false;
+
+    visible = visible === true;
+
+    var tabletDpi;
+    if (dpi) {
+        tabletDpi = dpi;
+    } else {
+        tabletDpi = DEFAULT_DPI * (DEFAULT_WIDTH / tabletWidth);
+    }
+
+    var modelURL = LOCAL_TABLET_MODEL_PATH;
+    var tabletProperties = {
+        name: "WebTablet Tablet",
+        type: "Model",
+        modelURL: modelURL,
+        url: modelURL, // for overlay
+        grabbable: true, // for overlay
+        loadPriority: 10.0, // for overlay
+        grab: { grabbable: true },
+        dimensions: { x: tabletWidth, y: tabletHeight, z: tabletDepth },
+        parentID: MyAvatar.SELF_ID,
+        visible: visible,
+        isGroupCulled: true
+    };
+
+    // compute position, rotation & parentJointIndex of the tablet
+    this.calculateTabletAttachmentProperties(hand, true, tabletProperties);
+    if (location) {
+        tabletProperties.localPosition = location.localPosition;
+        tabletProperties.localRotation = location.localRotation;
+    }
+
+    this.cleanUpOldTablets();
+    cleanUpOldMaterialEntities();
+
+    this.tabletEntityID = Overlays.addOverlay("model", tabletProperties);
+
+    if (this.webOverlayID) {
+        Overlays.deleteOverlay(this.webOverlayID);
+    }
+
+    var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.5) * sensorScaleFactor;
+    var WEB_ENTITY_Y_OFFSET = 1.25 * tabletScaleFactor;
+    var screenWidth = 0.9367 * tabletWidth;
+    var screenHeight = 0.9000 * tabletHeight;
+    this.webOverlayID = Overlays.addOverlay("web3d", {
+        name: "WebTablet Web",
+        url: url,
+        localPosition: { x: 0, y: WEB_ENTITY_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET },
+        localRotation: Quat.angleAxis(180, Y_AXIS),
+        dimensions: {x: screenWidth, y: screenHeight, z: 1.0},
+        dpi: tabletDpi,
+        color: { red: 255, green: 255, blue: 255 },
+        alpha: 1.0,
+        parentID: this.tabletEntityID,
+        parentJointIndex: -1,
+        showKeyboardFocusHighlight: false,
+        visible: visible
+    });
+
+    var homeButtonDim = 4.0 * tabletScaleFactor / 1.5;
+    var HOME_BUTTON_X_OFFSET = 0.00079 * sensorScaleFactor;
+    var HOME_BUTTON_Y_OFFSET = -1 * ((tabletHeight / 2) - (4.0 * tabletScaleFactor / 2));
+    var HOME_BUTTON_Z_OFFSET = (tabletDepth / 1.9) * sensorScaleFactor;
+    this.homeButtonID = Overlays.addOverlay("circle3d", {
+        name: "homeButton",
+        localPosition: { x: HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
+        localRotation: Quat.fromVec3Degrees({ x: 180, y: 180, z: 0}),
+        dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim },
+        solid: true,
+        alpha: 0.0,
+        visible: visible,
+        drawInFront: false,
+        parentID: this.tabletEntityID,
+        parentJointIndex: -1
+    });
+
+    this.homeButtonHighlightID = Overlays.addOverlay("circle3d", {
+        name: "homeButtonHighlight",
+        localPosition: { x: -HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
+        localRotation: Quat.fromVec3Degrees({ x: 180, y: 180, z: 0}),
+        dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim },
+        color: {red: 255, green: 255, blue: 255},
+        solid: true,
+        innerRadius: 0.9,
+        ignorePickIntersection: true,
+        alpha: 0.0,
+        visible: visible,
+        drawInFront: false,
+        parentID: this.tabletEntityID,
+        parentJointIndex: -1
+    });
+
+    this.receive = function (channel, senderID, senderUUID, localOnly) {
+        if (_this.homeButtonID === senderID) {
+            var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
+            var onHomeScreen = tablet.onHomeScreen();
+            var isMessageOpen;
+            if (onHomeScreen) {
+                isMessageOpen = tablet.isMessageDialogOpen();
+                if (isMessageOpen === false) {
+                    HMD.closeTablet();
+                }
+            } else {
+                isMessageOpen = tablet.isMessageDialogOpen();
+                if (isMessageOpen === false) {
+                    tablet.gotoHomeScreen();
+                    _this.setHomeButtonTexture();
+                }
+            }
+        }
+    };
+
+    this.state = "idle";
+
+    this.getRoot = function() {
+        return Entities.getWebViewRoot(_this.tabletEntityID);
+    };
+
+    this.clicked = false;
+
+    this.myOnHmdChanged = function () {
+        _this.onHmdChanged();
+    };
+    HMD.displayModeChanged.connect(this.myOnHmdChanged);
+
+    this.myMousePressEvent = function (event) {
+        _this.mousePressEvent(event);
+    };
+
+    this.myMouseMoveEvent = function (event) {
+        _this.mouseMoveEvent(event);
+    };
+
+    this.myMouseReleaseEvent = function (event) {
+        _this.mouseReleaseEvent(event);
+    };
+
+    Controller.mousePressEvent.connect(this.myMousePressEvent);
+    Controller.mouseMoveEvent.connect(this.myMouseMoveEvent);
+    Controller.mouseReleaseEvent.connect(this.myMouseReleaseEvent);
+
+    this.dragging = false;
+    this.initialLocalIntersectionPoint = {x: 0, y: 0, z: 0};
+    this.initialLocalPosition = {x: 0, y: 0, z: 0};
+
+    this.myGeometryChanged = function (geometry) {
+        _this.geometryChanged(geometry);
+    };
+    Window.geometryChanged.connect(this.myGeometryChanged);
+
+    this.myCameraModeChanged = function(newMode) {
+        _this.cameraModeChanged(newMode);
+    };
+    Camera.modeUpdated.connect(this.myCameraModeChanged);
+};
+
+WebTablet.prototype.getTabletTextureResolution = function() {
+    if (this.landscape) {
+        return { x: TABLET_TEXTURE_RESOLUTION.y , y: TABLET_TEXTURE_RESOLUTION.x };
+    } else {
+        return TABLET_TEXTURE_RESOLUTION;
+    }
+};
+
+WebTablet.prototype.getLandscape = function() {
+    return this.landscape;
+}
+
+WebTablet.prototype.setLandscape = function(newLandscapeValue) {
+    if (this.landscape === newLandscapeValue) {
+        return;
+    }
+
+    this.landscape = newLandscapeValue;
+    var cameraOrientation = Quat.cancelOutRollAndPitch(Camera.orientation);
+    var tabletRotation = Quat.multiply(cameraOrientation, this.landscape ? ROT_LANDSCAPE : ROT_Y_180);
+    Overlays.editOverlay(this.tabletEntityID, {
+        rotation: tabletRotation
+    });
+
+    var tabletWidth = getTabletWidthFromSettings() * MyAvatar.sensorToWorldScale;
+    var tabletScaleFactor = tabletWidth / TABLET_NATURAL_DIMENSIONS.x;
+    var tabletHeight = TABLET_NATURAL_DIMENSIONS.y * tabletScaleFactor;
+    var screenWidth = 0.9275 * tabletWidth;
+    var screenHeight = 0.8983 * tabletHeight;
+    var screenRotation = Quat.angleAxis(180, Vec3.UP);
+    Overlays.editOverlay(this.webOverlayID, {
+        localRotation: this.landscape ? Quat.multiply(screenRotation, Quat.angleAxis(-90, Vec3.FRONT)) : screenRotation,
+        dimensions: {x: this.landscape ? screenHeight : screenWidth, y: this.landscape ? screenWidth : screenHeight, z: 0.1}
+    });
+};
+
+WebTablet.prototype.getLocation = function() {
+    var location = Overlays.getProperty(this.tabletEntityID, "localPosition");
+    var orientation = Overlays.getProperty(this.tabletEntityID, "localOrientation");
+    return {
+        localPosition: location,
+        localRotation: orientation
+    };
+};
+
+WebTablet.prototype.setHomeButtonTexture = function() {
+    // TODO - is this still needed?
+    // Entities.editEntity(this.tabletEntityID, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})});
+};
+
+WebTablet.prototype.setURL = function (url) {
+    Overlays.editOverlay(this.webOverlayID, { url: url });
+};
+
+WebTablet.prototype.setScriptURL = function (scriptURL) {
+    Overlays.editOverlay(this.webOverlayID, { scriptURL: scriptURL });
+};
+
+WebTablet.prototype.getOverlayObject = function () {
+    return Overlays.getOverlayObject(this.webOverlayID);
+};
+
+WebTablet.prototype.setWidth = function (width) {
+    // imported from libraries/utils.js
+    resizeTablet(width);
+};
+
+WebTablet.prototype.destroy = function () {
+    Overlays.deleteOverlay(this.webOverlayID);
+    Overlays.deleteOverlay(this.tabletEntityID);
+    Overlays.deleteOverlay(this.homeButtonID);
+    Overlays.deleteOverlay(this.homeButtonHighlightID);
+    HMD.displayModeChanged.disconnect(this.myOnHmdChanged);
+
+    Controller.mousePressEvent.disconnect(this.myMousePressEvent);
+    Controller.mouseMoveEvent.disconnect(this.myMouseMoveEvent);
+    Controller.mouseReleaseEvent.disconnect(this.myMouseReleaseEvent);
+
+    Window.geometryChanged.disconnect(this.myGeometryChanged);
+    Camera.modeUpdated.disconnect(this.myCameraModeChanged);
+};
+
+WebTablet.prototype.geometryChanged = function (geometry) {
+    if (!HMD.active && HMD.tabletID) {
+        var tabletProperties = {};
+        // compute position, rotation & parentJointIndex of the tablet
+        this.calculateTabletAttachmentProperties(NO_HANDS, false, tabletProperties);
+        Overlays.editOverlay(HMD.tabletID, tabletProperties);
+    }
+};
+
+function gluPerspective(fovy, aspect, zNear, zFar) {
+    var cotan = 1 / Math.tan(fovy / 2);
+    var alpha = -(zFar + zNear) / (zFar - zNear);
+    var beta = -(2 * zFar * zNear) / (zFar - zNear);
+    var col0 = {x: cotan / aspect, y: 0, z: 0, w: 0};
+    var col1 = {x: 0, y: cotan, z: 0, w: 0};
+    var col2 = {x: 0, y: 0, z: alpha, w: -1};
+    var col3 = {x: 0, y: 0, z: beta, w: 0};
+    return Mat4.createFromColumns(col0, col1, col2, col3);
+}
+
+// calclulate the appropriate position of the tablet in world space, such that it fits in the center of the screen.
+// with a bit of padding on the top and bottom.
+// windowPos is used to position the center of the tablet at the given position.
+WebTablet.prototype.calculateWorldAttitudeRelativeToCamera = function (windowPos) {
+
+    var DEFAULT_DESKTOP_TABLET_SCALE = 75;
+    var DESKTOP_TABLET_SCALE = Settings.getValue("desktopTabletScale") || DEFAULT_DESKTOP_TABLET_SCALE;
+
+    // clamp window pos so 2d tablet is not off-screen.
+    var TABLET_TEXEL_PADDING = {x: 60, y: 90};
+    var X_CLAMP = (DESKTOP_TABLET_SCALE / 100) * ((this.getTabletTextureResolution().x / 2) + TABLET_TEXEL_PADDING.x);
+    var Y_CLAMP = (DESKTOP_TABLET_SCALE / 100) * ((this.getTabletTextureResolution().y / 2) + TABLET_TEXEL_PADDING.y);
+    windowPos.x = clamp(windowPos.x, X_CLAMP, Window.innerWidth - X_CLAMP);
+    windowPos.y = clamp(windowPos.y, Y_CLAMP, Window.innerHeight - Y_CLAMP);
+
+    var fov = (Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW) * (Math.PI / 180);
+
+    // scale factor of natural tablet dimensions.
+    var sensorScaleFactor = MyAvatar.sensorToWorldScale;
+    var tabletWidth = getTabletWidthFromSettings() * sensorScaleFactor;
+    var tabletScaleFactor = tabletWidth / TABLET_NATURAL_DIMENSIONS.x;
+    var tabletHeight = TABLET_NATURAL_DIMENSIONS.y * tabletScaleFactor;
+    var tabletDepth = TABLET_NATURAL_DIMENSIONS.z * tabletScaleFactor;
+    var tabletDpi = DEFAULT_DPI * (DEFAULT_WIDTH / tabletWidth);
+
+    var MAX_PADDING_FACTOR = 2.2;
+    var PADDING_FACTOR = Math.min(Window.innerHeight / this.getTabletTextureResolution().y, MAX_PADDING_FACTOR);
+    var TABLET_HEIGHT = (this.getTabletTextureResolution().y / tabletDpi) * INCHES_TO_METERS;
+    var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2);
+
+    // calcualte distance from camera
+    var dist = (PADDING_FACTOR * TABLET_HEIGHT) / (2 * Math.tan(fov / 2) * (DESKTOP_TABLET_SCALE / 100)) - WEB_ENTITY_Z_OFFSET;
+
+    var Z_NEAR = 0.01;
+    var Z_FAR = 100.0;
+
+    // calculate mouse position in clip space
+    var alpha = -(Z_FAR + Z_NEAR) / (Z_FAR - Z_NEAR);
+    var beta = -(2 * Z_FAR * Z_NEAR) / (Z_FAR - Z_NEAR);
+    var clipZ = (beta / dist) - alpha;
+    var clipMousePosition = {x: (2 * windowPos.x / Window.innerWidth) - 1,
+                             y: (2 * ((Window.innerHeight - windowPos.y) / Window.innerHeight)) - 1,
+                             z: clipZ};
+
+    // calculate projection matrix
+    var aspect = Window.innerWidth / Window.innerHeight;
+    var projMatrix = gluPerspective(fov, aspect, Z_NEAR, Z_FAR);
+
+    // transform mouse clip position into view coordinates.
+    var viewMousePosition = Mat4.transformPoint(Mat4.inverse(projMatrix), clipMousePosition);
+
+    // transform view mouse position into world coordinates.
+    var viewToWorldMatrix = Mat4.createFromRotAndTrans(Camera.orientation, Camera.position);
+    var worldMousePosition = Mat4.transformPoint(viewToWorldMatrix, viewMousePosition);
+
+    return {
+        position: worldMousePosition,
+        rotation: this.landscape ? Quat.multiply(Camera.orientation, ROT_LANDSCAPE) : Quat.multiply(Camera.orientation, ROT_Y_180)
+    };
+};
+
+// compute position, rotation & parentJointIndex of the tablet
+WebTablet.prototype.calculateTabletAttachmentProperties = function (hand, useMouse, tabletProperties) {
+    if (HMD.active) {
+        // in HMD mode, the tablet should be relative to the sensor to world matrix.
+        tabletProperties.parentJointIndex = SENSOR_TO_ROOM_MATRIX;
+
+        // compute the appropriate position of the tablet, near the hand controller that was used to spawn it.
+        var spawnInfo = calcSpawnInfo(hand, this.landscape);
+        tabletProperties.position = spawnInfo.position;
+        tabletProperties.rotation = spawnInfo.rotation;
+    } else {
+        // in desktop mode, the tablet should be relative to the camera
+        tabletProperties.parentJointIndex = CAMERA_MATRIX;
+
+        var windowPos;
+        if (useMouse) {
+            // compute the appropriate postion of the tablet such that it fits in the center of the screen nicely.
+            windowPos = {x: Controller.getValue(Controller.Hardware.Keyboard.MouseX),
+                         y: Controller.getValue(Controller.Hardware.Keyboard.MouseY)};
+        } else {
+            windowPos = {x: Window.innerWidth / 2,
+                         y: Window.innerHeight / 2};
+        }
+        var attitude = this.calculateWorldAttitudeRelativeToCamera(windowPos);
+        tabletProperties.position = attitude.position;
+        tabletProperties.rotation = attitude.rotation;
+    }
+};
+
+WebTablet.prototype.onHmdChanged = function () {
+    if (!HMD.tabletID) {
+        return;
+    }
+    var tabletProperties = {};
+    // compute position, rotation & parentJointIndex of the tablet
+    this.calculateTabletAttachmentProperties(NO_HANDS, false, tabletProperties);
+    Overlays.editOverlay(HMD.tabletID, tabletProperties);
+};
+
+WebTablet.prototype.pickle = function () {
+    return JSON.stringify({ webOverlayID: this.webOverlayID, tabletEntityID: this.tabletEntityID });
+};
+
+WebTablet.prototype.register = function() {
+    Messages.subscribe("home");
+    Messages.messageReceived.connect(this.receive);
+};
+
+WebTablet.prototype.cleanUpOldTabletsOnJoint = function(jointIndex) {
+    var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, jointIndex);
+    children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.SELF_ID, jointIndex));
+    children.forEach(function(childID) {
+        var props = Entities.getEntityProperties(childID, ["name"]);
+        if (props.name === "WebTablet Tablet") {
+            Entities.deleteEntity(childID);
+        }
+    });
+};
+
+WebTablet.prototype.cleanUpOldTablets = function() {
+    this.cleanUpOldTabletsOnJoint(-1);
+    this.cleanUpOldTabletsOnJoint(SENSOR_TO_ROOM_MATRIX);
+    this.cleanUpOldTabletsOnJoint(CAMERA_MATRIX);
+    this.cleanUpOldTabletsOnJoint(65529);
+    this.cleanUpOldTabletsOnJoint(65534);
+};
+
+WebTablet.prototype.unregister = function() {
+    Messages.unsubscribe("home");
+    Messages.messageReceived.disconnect(this.receive);
+};
+
+WebTablet.unpickle = function (string) {
+    if (!string) {
+        return;
+    }
+    var tablet = JSON.parse(string);
+    tablet.__proto__ = WebTablet.prototype;
+    return tablet;
+};
+
+WebTablet.prototype.getPosition = function () {
+    return Overlays.getProperty(this.webOverlayID, "position");
+};
+
+WebTablet.prototype.mousePressEvent = function (event) {
+    if (!HMD.active) {
+        var pickRay = Camera.computePickRay(event.x, event.y);
+        var tabletBackPickResults = Overlays.findRayIntersection(pickRay, true, [this.tabletEntityID]);
+        if (tabletBackPickResults.intersects) {
+            var overlayPickResults = Overlays.findRayIntersection(pickRay, true, [this.webOverlayID, this.homeButtonID]);
+            if (!overlayPickResults.intersects) {
+                this.dragging = true;
+                var invCameraXform = new Xform(Camera.orientation, Camera.position).inv();
+                this.initialLocalIntersectionPoint = invCameraXform.xformPoint(tabletBackPickResults.intersection);
+                this.initialLocalPosition = Overlays.getProperty(this.tabletEntityID, "localPosition");
+            }
+        }
+    }
+};
+
+WebTablet.prototype.cameraModeChanged = function (newMode) {
+    ;
+};
+
+function rayIntersectPlane(planePosition, planeNormal, rayStart, rayDirection) {
+    var rayDirectionDotPlaneNormal = Vec3.dot(rayDirection, planeNormal);
+    if (rayDirectionDotPlaneNormal > 0.00001 || rayDirectionDotPlaneNormal < -0.00001) {
+        var rayStartDotPlaneNormal = Vec3.dot(Vec3.subtract(planePosition, rayStart), planeNormal);
+        var distance = rayStartDotPlaneNormal / rayDirectionDotPlaneNormal;
+        return {hit: true, distance: distance};
+    } else {
+        // ray is parallel to the plane
+        return {hit: false, distance: 0};
+    }
+}
+
+WebTablet.prototype.scheduleMouseMoveProcessor = function() {
+    var _this = this;
+    if (!this.moveEventTimer) {
+        this.moveEventTimer = Script.setTimeout(function() {
+            _this.mouseMoveProcessor();
+        }, DELAY_FOR_30HZ);
+    }
+};
+
+WebTablet.prototype.handleHomeButtonHover = function(x, y) {
+    var pickRay = Camera.computePickRay(x, y);
+    var homePickResult = Overlays.findRayIntersection(pickRay, true, [this.homeButtonID]);
+    Overlays.editOverlay(this.homeButtonHighlightID, { alpha: homePickResult.intersects ? 1.0 : 0.0 });
+};
+
+WebTablet.prototype.mouseMoveEvent = function (event) {
+    if (this.dragging) {
+        this.currentMouse = {
+            x: event.x,
+            y: event.y
+        };
+        this.scheduleMouseMoveProcessor();
+    } else {
+        this.handleHomeButtonHover(event.x, event.y);
+    }
+};
+
+WebTablet.prototype.mouseMoveProcessor = function () {
+    this.moveEventTimer = null;
+    if (this.dragging) {
+        var pickRay = Camera.computePickRay(this.currentMouse.x, this.currentMouse.y);
+
+        // transform pickRay into camera local coordinates
+        var invCameraXform = new Xform(Camera.orientation, Camera.position).inv();
+        var localPickRay = {
+            origin: invCameraXform.xformPoint(pickRay.origin),
+            direction: invCameraXform.xformVector(pickRay.direction)
+        };
+
+        var NORMAL = {x: 0, y: 0, z: -1};
+        var result = rayIntersectPlane(this.initialLocalIntersectionPoint, NORMAL, localPickRay.origin, localPickRay.direction);
+        if (result.hit) {
+            var localIntersectionPoint = Vec3.sum(localPickRay.origin, Vec3.multiply(localPickRay.direction, result.distance));
+            var localOffset = Vec3.subtract(localIntersectionPoint, this.initialLocalIntersectionPoint);
+            var localPosition = Vec3.sum(this.initialLocalPosition, localOffset);
+            Overlays.editOverlay(this.tabletEntityID, {
+                localPosition: localPosition
+            });
+        }
+        this.scheduleMouseMoveProcessor();
+    } else {
+        this.handleHomeButtonHover(this.currentMouse.x, this.currentMouse.y);
+    }
+};
+
+WebTablet.prototype.mouseReleaseEvent = function (event) {
+    this.dragging = false;
+};
diff --git a/scripts/simplifiedUI/system/libraries/Xform.js b/scripts/simplifiedUI/system/libraries/Xform.js
new file mode 100644
index 0000000000..1aa43cf30b
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/Xform.js
@@ -0,0 +1,48 @@
+//
+//  Created by Anthony J. Thibault on 2016/06/21
+//  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
+//
+
+// ctor
+Xform = function(rot, pos) {
+    this.rot = rot;
+    this.pos = pos;
+}
+
+Xform.ident = function() {
+    return new Xform({x: 0, y: 0, z: 0, w: 1}, {x: 0, y: 0, z: 0});
+};
+
+Xform.mul = function(lhs, rhs) {
+    var rot = Quat.multiply(lhs.rot, rhs.rot);
+    var pos = Vec3.sum(lhs.pos, Vec3.multiplyQbyV(lhs.rot, rhs.pos));
+    return new Xform(rot, pos);
+};
+
+Xform.prototype.inv = function() {
+    var invRot = Quat.inverse(this.rot);
+    var invPos = Vec3.multiply(-1, this.pos);
+    return new Xform(invRot, Vec3.multiplyQbyV(invRot, invPos));
+};
+
+Xform.prototype.mirrorX = function() {
+    return new Xform({x: this.rot.x, y: -this.rot.y, z: -this.rot.z, w: this.rot.w},
+                     {x: -this.pos.x, y: this.pos.y, z: this.pos.z});
+};
+
+Xform.prototype.xformVector = function (vector) {
+    return Vec3.multiplyQbyV(this.rot, vector);
+}
+
+Xform.prototype.xformPoint = function (point) {
+    return Vec3.sum(Vec3.multiplyQbyV(this.rot, point), this.pos);
+}
+
+Xform.prototype.toString = function() {
+    var rot = this.rot;
+    var pos = this.pos;
+    return "Xform rot = (" + rot.x + ", " + rot.y + ", " + rot.z + ", " + rot.w + "), pos = (" + pos.x + ", " + pos.y + ", " + pos.z + ")";
+};
diff --git a/scripts/simplifiedUI/system/libraries/accountUtils.js b/scripts/simplifiedUI/system/libraries/accountUtils.js
new file mode 100644
index 0000000000..42204340c7
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/accountUtils.js
@@ -0,0 +1,10 @@
+//
+//  accountUtils.js
+//  scripts/system/libraries/libraries
+//
+//  Copyright 2017 High Fidelity, Inc.
+//
+
+openLoginWindow = function openLoginWindow() {
+    Menu.triggerOption("Login/Sign Up");
+};        
diff --git a/scripts/simplifiedUI/system/libraries/cloneEntityUtils.js b/scripts/simplifiedUI/system/libraries/cloneEntityUtils.js
new file mode 100644
index 0000000000..f789e19cd8
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/cloneEntityUtils.js
@@ -0,0 +1,57 @@
+"use strict";
+
+//  cloneEntity.js
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+
+/* global entityIsCloneable:true, cloneEntity:true, propsAreCloneDynamic:true, Script,
+   propsAreCloneDynamic:true, Entities, Uuid */
+
+Script.include("/~/system/libraries/controllerDispatcherUtils.js");
+
+// Object assign  polyfill
+if (typeof Object.assign !== 'function') {
+    Object.assign = function(target, varArgs) {
+        if (target === null) {
+            throw new TypeError('Cannot convert undefined or null to object');
+        }
+        var to = Object(target);
+        for (var index = 1; index < arguments.length; index++) {
+            var nextSource = arguments[index];
+            if (nextSource !== null) {
+                for (var nextKey in nextSource) {
+                    if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+                        to[nextKey] = nextSource[nextKey];
+                    }
+                }
+            }
+        }
+        return to;
+    };
+}
+
+entityIsCloneable = function(props) {
+    if (props) {
+        return props.cloneable;
+    }
+    return false;
+};
+
+propsAreCloneDynamic = function(props) {
+    var cloneable = entityIsCloneable(props);
+    if (cloneable) {
+        return props.cloneDynamic;
+    }
+    return false;
+};
+
+cloneEntity = function(props) {
+    var entityIDToClone = props.id;
+    if (entityIsCloneable(props) &&
+        (Uuid.isNull(props.certificateID) || props.certificateType.indexOf('domainUnlimited') >= 0)) {
+        var cloneID = Entities.cloneEntity(entityIDToClone);
+        return cloneID;
+    }
+    return null;
+};
diff --git a/scripts/simplifiedUI/system/libraries/connectionUtils.js b/scripts/simplifiedUI/system/libraries/connectionUtils.js
new file mode 100644
index 0000000000..7b988d3117
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/connectionUtils.js
@@ -0,0 +1,94 @@
+//
+//  connectionUtils.js
+//  scripts/system/libraries
+//
+//  Copyright 2018 High Fidelity, Inc.
+//
+
+// Function Names:
+//   - requestJSON
+//   - getAvailableConnections
+//   - getInfoAboutUser
+//   - getConnectionData
+//
+// Description:
+//   - Update all the usernames that I am entitled to see, using my login but not dependent on canKick.
+var request = Script.require('request').request;
+var METAVERSE_BASE = Account.metaverseServerURL;
+function requestJSON(url, callback) { // callback(data) if successfull. Logs otherwise.
+    request({
+        uri: url
+    }, function (error, response) {
+        if (error || (response.status !== 'success')) {
+            print("Error: unable to get URL", error || response.status);
+            return;
+        }
+        callback(response.data);
+    });
+}
+function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successful. (Logs otherwise)
+    url = METAVERSE_BASE + '/api/v1/users?per_page=400&'
+    if (domain) {
+        url += 'status=' + domain.slice(1, -1); // without curly braces
+    } else {
+        url += 'filter=connections'; // regardless of whether online
+    }
+    requestJSON(url, function (connectionsData) {
+        callback(connectionsData.users);
+    });
+}
+function getInfoAboutUser(specificUsername, callback) {
+    url = METAVERSE_BASE + '/api/v1/users?filter=connections'
+    requestJSON(url, function (connectionsData) {
+        for (user in connectionsData.users) {
+            if (connectionsData.users[user].username === specificUsername) {
+                callback(connectionsData.users[user]);
+                return;
+            }
+        }
+        callback(false);
+    });
+}
+getConnectionData = function getConnectionData(specificUsername, domain) {
+    function frob(user) { // get into the right format
+        var formattedSessionId = user.location.node_id || '';
+        if (formattedSessionId !== '' && formattedSessionId.indexOf("{") != 0) {
+            formattedSessionId = "{" + formattedSessionId + "}";
+        }
+        return {
+            sessionId: formattedSessionId,
+            userName: user.username,
+            connection: user.connection,
+            profileUrl: user.images.thumbnail,
+            placeName: (user.location.root || user.location.domain || {}).name || ''
+        };
+    }
+    if (specificUsername) {
+        getInfoAboutUser(specificUsername, function (user) {
+            if (user) {
+                updateUser(frob(user));
+            } else {
+                print('Error: Unable to find information about ' + specificUsername + ' in connectionsData!');
+            }
+        });
+    } else {
+        getAvailableConnections(domain, function (users) {
+            if (domain) {
+                users.forEach(function (user) {
+                    updateUser(frob(user));
+                });
+            } else {
+                sendToQml({ method: 'updateConnections', connections: users.map(frob) });
+            }
+        });
+    }
+}
+
+// Function Name: sendToQml()
+//
+// Description:
+//   -Use this function to send a message to the QML (i.e. to change appearances). The "message" argument is what is sent to
+//    the QML in the format "{method, params}", like json-rpc. See also fromQml().
+function sendToQml(message) {
+    Tablet.getTablet("com.highfidelity.interface.tablet.system").sendToQml(message);
+}
diff --git a/scripts/simplifiedUI/system/libraries/controllerDispatcherUtils.js b/scripts/simplifiedUI/system/libraries/controllerDispatcherUtils.js
new file mode 100644
index 0000000000..3b81e17473
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/controllerDispatcherUtils.js
@@ -0,0 +1,629 @@
+"use strict";
+
+//  controllerDispatcherUtils.js
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+
+/* global module, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays, Xform, Mat4,
+   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,
+   TRIGGER_OFF_VALUE:true,
+   TRIGGER_ON_VALUE:true,
+   PICK_MAX_DISTANCE:true,
+   DEFAULT_SEARCH_SPHERE_DISTANCE:true,
+   NEAR_GRAB_PICK_RADIUS:true,
+   COLORS_GRAB_SEARCHING_HALF_SQUEEZE:true,
+   COLORS_GRAB_SEARCHING_FULL_SQUEEZE:true,
+   COLORS_GRAB_DISTANCE_HOLD:true,
+   NEAR_GRAB_RADIUS:true,
+   DISPATCHER_PROPERTIES:true,
+   HAPTIC_PULSE_STRENGTH:true,
+   HAPTIC_PULSE_DURATION:true,
+   DISPATCHER_HOVERING_LIST:true,
+   DISPATCHER_HOVERING_STYLE:true,
+   Entities,
+   makeDispatcherModuleParameters:true,
+   makeRunningValues:true,
+   enableDispatcherModule:true,
+   disableDispatcherModule:true,
+   getEnabledModuleByName:true,
+   getGrabbableData:true,
+   isAnothersAvatarEntity:true,
+   isAnothersChildEntity:true,
+   entityIsEquippable:true,
+   entityIsGrabbable:true,
+   entityIsDistanceGrabbable:true,
+   getControllerJointIndexCacheTime:true,
+   getControllerJointIndexCache:true,
+   getControllerJointIndex:true,
+   propsArePhysical:true,
+   controllerDispatcherPluginsNeedSort:true,
+   projectOntoXYPlane:true,
+   projectOntoEntityXYPlane:true,
+   projectOntoOverlayXYPlane:true,
+   makeLaserLockInfo:true,
+   entityHasActions:true,
+   ensureDynamic:true,
+   findGrabbableGroupParent:true,
+   BUMPER_ON_VALUE:true,
+   getEntityParents:true,
+   findHandChildEntities:true,
+   findFarGrabJointChildEntities:true,
+   makeLaserParams:true,
+   TEAR_AWAY_DISTANCE:true,
+   TEAR_AWAY_COUNT:true,
+   TEAR_AWAY_CHECK_TIME:true,
+   NEAR_GRAB_DISTANCE: true,
+   distanceBetweenPointAndEntityBoundingBox:true,
+   entityIsEquipped:true,
+   highlightTargetEntity:true,
+   clearHighlightedEntities:true,
+   unhighlightTargetEntity:true,
+   distanceBetweenEntityLocalPositionAndBoundingBox: true,
+   worldPositionToRegistrationFrameMatrix: true
+*/
+
+MSECS_PER_SEC = 1000.0;
+INCHES_TO_METERS = 1.0 / 39.3701;
+
+HAPTIC_PULSE_STRENGTH = 1.0;
+HAPTIC_PULSE_DURATION = 13.0;
+
+ZERO_VEC = { x: 0, y: 0, z: 0 };
+ONE_VEC = { x: 1, y: 1, z: 1 };
+
+LEFT_HAND = 0;
+RIGHT_HAND = 1;
+
+FORBIDDEN_GRAB_TYPES = ["Unknown", "Light", "PolyLine", "Zone"];
+
+HAPTIC_PULSE_STRENGTH = 1.0;
+HAPTIC_PULSE_DURATION = 13.0;
+
+DEFAULT_REGISTRATION_POINT = { x: 0.5, y: 0.5, z: 0.5 };
+
+TRIGGER_OFF_VALUE = 0.1;
+TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab
+BUMPER_ON_VALUE = 0.5;
+
+PICK_MAX_DISTANCE = 500; // max length of pick-ray
+DEFAULT_SEARCH_SPHERE_DISTANCE = 1000; // how far from camera to search intersection?
+NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing.
+
+COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 };
+COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 };
+COLORS_GRAB_DISTANCE_HOLD = { red: 238, green: 75, blue: 214 };
+
+NEAR_GRAB_RADIUS = 1.0;
+
+TEAR_AWAY_DISTANCE = 0.15; // ungrab an entity if its bounding-box moves this far from the hand
+TEAR_AWAY_COUNT = 2; // multiply by TEAR_AWAY_CHECK_TIME to know how long the item must be away
+TEAR_AWAY_CHECK_TIME = 0.15; // seconds, duration between checks
+
+TELEPORT_DEADZONE = 0.15;
+
+NEAR_GRAB_DISTANCE = 0.14; // Grab an entity if its bounding box is within this distance.
+// Smaller than TEAR_AWAY_DISTANCE for hysteresis.
+
+DISPATCHER_HOVERING_LIST = "dispatcherHoveringList";
+DISPATCHER_HOVERING_STYLE = {
+    isOutlineSmooth: true,
+    outlineWidth: 0,
+    outlineUnoccludedColor: {red: 255, green: 128, blue: 128},
+    outlineUnoccludedAlpha: 0.0,
+    outlineOccludedColor: {red: 255, green: 128, blue: 128},
+    outlineOccludedAlpha:0.0,
+    fillUnoccludedColor: {red: 255, green: 255, blue: 255},
+    fillUnoccludedAlpha: 0.12,
+    fillOccludedColor: {red: 255, green: 255, blue: 255},
+    fillOccludedAlpha: 0.0
+};
+
+DISPATCHER_PROPERTIES = [
+    "position",
+    "registrationPoint",
+    "rotation",
+    "gravity",
+    "collidesWith",
+    "dynamic",
+    "collisionless",
+    "locked",
+    "name",
+    "shapeType",
+    "parentID",
+    "parentJointIndex",
+    "density",
+    "dimensions",
+    "type",
+    "href",
+    "cloneable",
+    "cloneDynamic",
+    "localPosition",
+    "localRotation",
+    "grab.grabbable",
+    "grab.grabKinematic",
+    "grab.grabFollowsController",
+    "grab.triggerable",
+    "grab.equippable",
+    "grab.grabDelegateToParent",
+    "grab.equippableLeftPosition",
+    "grab.equippableLeftRotation",
+    "grab.equippableRightPosition",
+    "grab.equippableRightRotation",
+    "grab.equippableIndicatorURL",
+    "grab.equippableIndicatorScale",
+    "grab.equippableIndicatorOffset",
+    "userData",
+    "avatarEntity",
+    "owningAvatarID",
+    "certificateID",
+    "certificateType"
+];
+
+// priority -- a lower priority means the module will be asked sooner than one with a higher priority in a given update step
+// activitySlots -- indicates which "slots" must not yet be in use for this module to start
+// requiredDataForReady -- which "situation" parts this module looks at to decide if it will start
+// sleepMSBetweenRuns -- how long to wait between calls to this module's "run" method
+makeDispatcherModuleParameters = function (priority, activitySlots, requiredDataForReady, sleepMSBetweenRuns, enableLaserForHand) {
+    if (enableLaserForHand === undefined) {
+        enableLaserForHand = -1;
+    }
+
+    return {
+        priority: priority,
+        activitySlots: activitySlots,
+        requiredDataForReady: requiredDataForReady,
+        sleepMSBetweenRuns: sleepMSBetweenRuns,
+        handLaser: enableLaserForHand
+    };
+};
+
+makeLaserLockInfo = function(targetID, isOverlay, hand, offset) {
+    return {
+        targetID: targetID,
+        isOverlay: isOverlay,
+        hand: hand,
+        offset: offset
+    };
+};
+
+makeLaserParams = function(hand, alwaysOn) {
+    if (alwaysOn === undefined) {
+        alwaysOn = false;
+    }
+
+    return {
+        hand: hand,
+        alwaysOn: alwaysOn
+    };
+};
+
+makeRunningValues = function (active, targets, requiredDataForRun, laserLockInfo) {
+    return {
+        active: active,
+        targets: targets,
+        requiredDataForRun: requiredDataForRun,
+        laserLockInfo: laserLockInfo
+    };
+};
+
+enableDispatcherModule = function (moduleName, module, priority) {
+    if (!controllerDispatcherPlugins) {
+        controllerDispatcherPlugins = {};
+    }
+    controllerDispatcherPlugins[moduleName] = module;
+    controllerDispatcherPluginsNeedSort = true;
+};
+
+disableDispatcherModule = function (moduleName) {
+    delete controllerDispatcherPlugins[moduleName];
+    controllerDispatcherPluginsNeedSort = true;
+};
+
+getEnabledModuleByName = function (moduleName) {
+    if (controllerDispatcherPlugins.hasOwnProperty(moduleName)) {
+        return controllerDispatcherPlugins[moduleName];
+    }
+    return null;
+};
+
+getGrabbableData = function (ggdProps) {
+    // look in userData for a "grabbable" key, return the value or some defaults
+    var grabbableData = {};
+    var userDataParsed = null;
+    try {
+        if (!ggdProps.userDataParsed) {
+            ggdProps.userDataParsed = JSON.parse(ggdProps.userData);
+        }
+        userDataParsed = ggdProps.userDataParsed;
+    } catch (err) {
+        userDataParsed = {};
+    }
+
+    if (userDataParsed.grabbableKey) {
+        grabbableData = userDataParsed.grabbableKey;
+    } else {
+        grabbableData = ggdProps.grab;
+    }
+
+    // extract grab-related properties, provide defaults if any are missing
+    if (!grabbableData.hasOwnProperty("grabbable")) {
+        grabbableData.grabbable = true;
+    }
+    // kinematic has been renamed to grabKinematic
+    if (!grabbableData.hasOwnProperty("grabKinematic") &&
+        !grabbableData.hasOwnProperty("kinematic")) {
+        grabbableData.grabKinematic = true;
+    }
+    if (!grabbableData.hasOwnProperty("grabKinematic")) {
+        grabbableData.grabKinematic = grabbableData.kinematic;
+    }
+    // ignoreIK has been renamed to grabFollowsController
+    if (!grabbableData.hasOwnProperty("grabFollowsController") &&
+        !grabbableData.hasOwnProperty("ignoreIK")) {
+        grabbableData.grabFollowsController = true;
+    }
+    if (!grabbableData.hasOwnProperty("grabFollowsController")) {
+        grabbableData.grabFollowsController = grabbableData.ignoreIK;
+    }
+    // wantsTrigger has been renamed to triggerable
+    if (!grabbableData.hasOwnProperty("triggerable") &&
+        !grabbableData.hasOwnProperty("wantsTrigger")) {
+        grabbableData.triggerable = false;
+    }
+    if (!grabbableData.hasOwnProperty("triggerable")) {
+        grabbableData.triggerable = grabbableData.wantsTrigger;
+    }
+    if (!grabbableData.hasOwnProperty("equippable")) {
+        grabbableData.equippable = false;
+    }
+    if (!grabbableData.hasOwnProperty("equippableLeftPosition")) {
+        grabbableData.equippableLeftPosition = { x: 0, y: 0, z: 0 };
+    }
+    if (!grabbableData.hasOwnProperty("equippableLeftRotation")) {
+        grabbableData.equippableLeftPosition = { x: 0, y: 0, z: 0, w: 1 };
+    }
+    if (!grabbableData.hasOwnProperty("equippableRightPosition")) {
+        grabbableData.equippableRightPosition = { x: 0, y: 0, z: 0 };
+    }
+    if (!grabbableData.hasOwnProperty("equippableRightRotation")) {
+        grabbableData.equippableRightPosition = { x: 0, y: 0, z: 0, w: 1 };
+    }
+    return grabbableData;
+};
+
+isAnothersAvatarEntity = function (iaaeProps) {
+    if (!iaaeProps.avatarEntity) {
+        return false;
+    }
+    if (iaaeProps.owningAvatarID === MyAvatar.sessionUUID) {
+        return false;
+    }
+    if (iaaeProps.owningAvatarID === MyAvatar.SELF_ID) {
+        return false;
+    }
+    return true;
+};
+
+isAnothersChildEntity = function (iaceProps) {
+    while (iaceProps.parentID && iaceProps.parentID !== Uuid.NULL) {
+        if (Entities.getNestableType(iaceProps.parentID) == "avatar") {
+            if (iaceProps.parentID == MyAvatar.SELF_ID || iaceProps.parentID == MyAvatar.sessionUUID) {
+                return false; // not another's, it's mine.
+            }
+            return true;
+        }
+        // Entities.getNestableType(iaceProps.parentID) == "entity") {
+        var parentProps = Entities.getEntityProperties(iaceProps.parentID, DISPATCHER_PROPERTIES);
+        if (!parentProps) {
+            break;
+        }
+        parentProps.id = iaceProps.parentID;
+        iaceProps = parentProps;
+    }
+    return false;
+};
+
+
+entityIsEquippable = function (eieProps) {
+    var grabbable = getGrabbableData(eieProps).grabbable;
+    if (!grabbable ||
+        isAnothersAvatarEntity(eieProps) ||
+        isAnothersChildEntity(eieProps) ||
+        FORBIDDEN_GRAB_TYPES.indexOf(eieProps.type) >= 0) {
+        return false;
+    }
+    return true;
+};
+
+entityIsGrabbable = function (eigProps) {
+    var grabbable = getGrabbableData(eigProps).grabbable;
+    if (!grabbable ||
+        eigProps.locked ||
+        FORBIDDEN_GRAB_TYPES.indexOf(eigProps.type) >= 0) {
+        return false;
+    }
+    return true;
+};
+
+clearHighlightedEntities = function() {
+    Selection.clearSelectedItemsList(DISPATCHER_HOVERING_LIST);
+};
+
+highlightTargetEntity = function(entityID) {
+    Selection.addToSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", entityID);
+};
+
+unhighlightTargetEntity = function(entityID) {
+    Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", entityID);
+};
+
+entityIsDistanceGrabbable = function(eidgProps) {
+    if (!entityIsGrabbable(eidgProps)) {
+        return false;
+    }
+
+    // we can't distance-grab non-physical
+    var isPhysical = propsArePhysical(eidgProps);
+    if (!isPhysical) {
+        return false;
+    }
+
+    return true;
+};
+
+getControllerJointIndexCacheTime = [0, 0];
+getControllerJointIndexCache = [-1, -1];
+
+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 = MyAvatar.getJointIndex(hand === RIGHT_HAND ?
+                                                              "_CONTROLLER_RIGHTHAND" :
+                                                              "_CONTROLLER_LEFTHAND");
+            getControllerJointIndexCacheTime[hand] = now;
+            getControllerJointIndexCache[hand] = controllerJointIndex;
+            return controllerJointIndex;
+        }
+    } else {
+        return getControllerJointIndexCache[hand];
+    }
+
+    return -1;
+};
+
+propsArePhysical = function (papProps) {
+    if (!papProps.dynamic) {
+        return false;
+    }
+    var isPhysical = (papProps.shapeType && papProps.shapeType !== 'none');
+    return isPhysical;
+};
+
+projectOntoXYPlane = function (worldPos, position, rotation, dimensions, registrationPoint) {
+    var invRot = Quat.inverse(rotation);
+    var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(worldPos, position));
+    var invDimensions = {
+        x: 1 / dimensions.x,
+        y: 1 / dimensions.y,
+        z: 1 / dimensions.z
+    };
+
+    var normalizedPos = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), registrationPoint);
+    return {
+        x: normalizedPos.x * dimensions.x,
+        y: (1 - normalizedPos.y) * dimensions.y // flip y-axis
+    };
+};
+
+projectOntoEntityXYPlane = function (entityID, worldPos, popProps) {
+    return projectOntoXYPlane(worldPos, popProps.position, popProps.rotation,
+                              popProps.dimensions, popProps.registrationPoint);
+};
+
+projectOntoOverlayXYPlane = function projectOntoOverlayXYPlane(overlayID, worldPos) {
+    var position = Overlays.getProperty(overlayID, "position");
+    var rotation = Overlays.getProperty(overlayID, "rotation");
+    var dimensions = Overlays.getProperty(overlayID, "dimensions");
+    dimensions.z = 0.01; // we are projecting onto the XY plane of the overlay, so ignore the z dimension
+
+    return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT);
+};
+
+entityHasActions = function (entityID) {
+    return Entities.getActionIDs(entityID).length > 0;
+};
+
+ensureDynamic = function (entityID) {
+    // if we distance hold something and keep it very still before releasing it, it ends up
+    // non-dynamic in bullet.  If it's too still, give it a little bounce so it will fall.
+    var edProps = Entities.getEntityProperties(entityID, ["velocity", "dynamic", "parentID"]);
+    if (edProps.dynamic && edProps.parentID === Uuid.NULL) {
+        var velocity = edProps.velocity;
+        if (Vec3.length(velocity) < 0.05) { // see EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD
+            velocity = { x: 0.0, y: 0.2, z: 0.0 };
+            Entities.editEntity(entityID, { velocity: velocity });
+        }
+    }
+};
+
+findGrabbableGroupParent = function (controllerData, targetProps) {
+    while (targetProps.grab.grabDelegateToParent &&
+           targetProps.parentID &&
+           targetProps.parentID !== Uuid.NULL &&
+           Entities.getNestableType(targetProps.parentID) == "entity") {
+        var parentProps = Entities.getEntityProperties(targetProps.parentID, DISPATCHER_PROPERTIES);
+        if (!parentProps) {
+            break;
+        }
+        if (!entityIsGrabbable(parentProps)) {
+            break;
+        }
+        parentProps.id = targetProps.parentID;
+        targetProps = parentProps;
+        controllerData.nearbyEntityPropertiesByID[targetProps.id] = targetProps;
+    }
+
+    return targetProps;
+};
+
+getEntityParents = function(targetProps) {
+    var parentProperties = [];
+    while (targetProps.parentID &&
+           targetProps.parentID !== Uuid.NULL &&
+           Entities.getNestableType(targetProps.parentID) == "entity") {
+        var parentProps = Entities.getEntityProperties(targetProps.parentID, DISPATCHER_PROPERTIES);
+        if (!parentProps) {
+            break;
+        }
+        parentProps.id = targetProps.parentID;
+        targetProps = parentProps;
+        parentProperties.push(parentProps);
+    }
+
+    return parentProperties;
+};
+
+
+findHandChildEntities = function(hand) {
+    // find children of avatar's hand joint
+    var handJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ? "RightHand" : "LeftHand");
+    var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex);
+    children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.SELF_ID, handJointIndex));
+
+    // find children of faux controller joint
+    var controllerJointIndex = getControllerJointIndex(hand);
+    children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerJointIndex));
+    children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.SELF_ID, controllerJointIndex));
+
+    // find children of faux camera-relative controller joint
+    var controllerCRJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ?
+                                                        "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" :
+                                                        "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND");
+    children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerCRJointIndex));
+    children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.SELF_ID, controllerCRJointIndex));
+
+    return children.filter(function (childID) {
+        var childType = Entities.getNestableType(childID);
+        return childType == "entity";
+    });
+};
+
+findFarGrabJointChildEntities = function(hand) {
+    // find children of avatar's far-grab joint
+    var farGrabJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ? "_FARGRAB_RIGHTHAND" : "_FARGRAB_LEFTHAND");
+    var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, farGrabJointIndex);
+    children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.SELF_ID, farGrabJointIndex));
+
+    return children.filter(function (childID) {
+        var childType = Entities.getNestableType(childID);
+        return childType == "entity";
+    });
+};
+
+distanceBetweenEntityLocalPositionAndBoundingBox = function(entityProps, jointGrabOffset) {
+    var DEFAULT_REGISTRATION_POINT = { x: 0.5, y: 0.5, z: 0.5 };
+    var rotInv = Quat.inverse(entityProps.localRotation);
+    var localPosition = Vec3.sum(entityProps.localPosition, jointGrabOffset);
+    var localPoint = Vec3.multiplyQbyV(rotInv, Vec3.multiply(localPosition, -1.0));
+
+    var halfDims = Vec3.multiply(entityProps.dimensions, 0.5);
+    var regRatio = Vec3.subtract(DEFAULT_REGISTRATION_POINT, entityProps.registrationPoint);
+    var entityCenter = Vec3.multiplyVbyV(regRatio, entityProps.dimensions);
+    var localMin = Vec3.subtract(entityCenter, halfDims);
+    var localMax = Vec3.sum(entityCenter, halfDims);
+
+
+    var v = {x: localPoint.x, y: localPoint.y, z: localPoint.z};
+    v.x = Math.max(v.x, localMin.x);
+    v.x = Math.min(v.x, localMax.x);
+    v.y = Math.max(v.y, localMin.y);
+    v.y = Math.min(v.y, localMax.y);
+    v.z = Math.max(v.z, localMin.z);
+    v.z = Math.min(v.z, localMax.z);
+
+    return Vec3.distance(v, localPoint);
+};
+
+distanceBetweenPointAndEntityBoundingBox = function(point, entityProps) {
+    var entityXform = new Xform(entityProps.rotation, entityProps.position);
+    var localPoint = entityXform.inv().xformPoint(point);
+    var minOffset = Vec3.multiplyVbyV(entityProps.registrationPoint, entityProps.dimensions);
+    var maxOffset = Vec3.multiplyVbyV(Vec3.subtract(ONE_VEC, entityProps.registrationPoint), entityProps.dimensions);
+    var localMin = Vec3.subtract(entityXform.trans, minOffset);
+    var localMax = Vec3.sum(entityXform.trans, maxOffset);
+
+    var v = {x: localPoint.x, y: localPoint.y, z: localPoint.z};
+    v.x = Math.max(v.x, localMin.x);
+    v.x = Math.min(v.x, localMax.x);
+    v.y = Math.max(v.y, localMin.y);
+    v.y = Math.min(v.y, localMax.y);
+    v.z = Math.max(v.z, localMin.z);
+    v.z = Math.min(v.z, localMax.z);
+
+    return Vec3.distance(v, localPoint);
+};
+
+entityIsEquipped = function(entityID) {
+    var rightEquipEntity = getEnabledModuleByName("RightEquipEntity");
+    var leftEquipEntity = getEnabledModuleByName("LeftEquipEntity");
+    var equippedInRightHand = rightEquipEntity ? rightEquipEntity.targetEntityID === entityID : false;
+    var equippedInLeftHand = leftEquipEntity ? leftEquipEntity.targetEntityID === entityID : false;
+    return equippedInRightHand || equippedInLeftHand;
+};
+
+worldPositionToRegistrationFrameMatrix = function(wptrProps, pos) {
+    // get world matrix for intersection point
+    var intersectionMat = new Xform({ x: 0, y: 0, z:0, w: 1 }, pos);
+
+    // calculate world matrix for registrationPoint addjusted entity
+    var DEFAULT_REGISTRATION_POINT = { x: 0.5, y: 0.5, z: 0.5 };
+    var regRatio = Vec3.subtract(DEFAULT_REGISTRATION_POINT, wptrProps.registrationPoint);
+    var regOffset = Vec3.multiplyVbyV(regRatio, wptrProps.dimensions);
+    var regOffsetRot = Vec3.multiplyQbyV(wptrProps.rotation, regOffset);
+    var modelMat = new Xform(wptrProps.rotation, Vec3.sum(wptrProps.position, regOffsetRot));
+
+    // get inverse of model matrix
+    var modelMatInv = modelMat.inv();
+
+    // transform world intersection point into object's registrationPoint frame
+    var xformMat = Xform.mul(modelMatInv, intersectionMat);
+
+    // convert to Mat4
+    var offsetMat = Mat4.createFromRotAndTrans(xformMat.rot, xformMat.pos);
+    return offsetMat;
+};
+
+
+if (typeof module !== 'undefined') {
+    module.exports = {
+        makeDispatcherModuleParameters: makeDispatcherModuleParameters,
+        enableDispatcherModule: enableDispatcherModule,
+        disableDispatcherModule: disableDispatcherModule,
+        highlightTargetEntity: highlightTargetEntity,
+        unhighlightTargetEntity: unhighlightTargetEntity,
+        clearHighlightedEntities: clearHighlightedEntities,
+        makeRunningValues: makeRunningValues,
+        findGrabbableGroupParent: findGrabbableGroupParent,
+        LEFT_HAND: LEFT_HAND,
+        RIGHT_HAND: RIGHT_HAND,
+        BUMPER_ON_VALUE: BUMPER_ON_VALUE,
+        TEAR_AWAY_DISTANCE: TEAR_AWAY_DISTANCE,
+        propsArePhysical: propsArePhysical,
+        entityIsEquippable: entityIsEquippable,
+        entityIsGrabbable: entityIsGrabbable,
+        NEAR_GRAB_RADIUS: NEAR_GRAB_RADIUS,
+        projectOntoOverlayXYPlane: projectOntoOverlayXYPlane,
+        projectOntoEntityXYPlane: projectOntoEntityXYPlane,
+        TRIGGER_OFF_VALUE: TRIGGER_OFF_VALUE,
+        TRIGGER_ON_VALUE: TRIGGER_ON_VALUE,
+        DISPATCHER_HOVERING_LIST: DISPATCHER_HOVERING_LIST,
+        worldPositionToRegistrationFrameMatrix: worldPositionToRegistrationFrameMatrix
+    };
+}
diff --git a/scripts/simplifiedUI/system/libraries/controllers.js b/scripts/simplifiedUI/system/libraries/controllers.js
new file mode 100644
index 0000000000..be7d22e073
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/controllers.js
@@ -0,0 +1,81 @@
+//  handControllerGrab.js
+//
+//  Created by Seth Alves on 2016-9-7
+//  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
+/* global MyAvatar, Vec3, HMD, Controller, Camera, Quat, Settings,
+   getGrabPointSphereOffset:true,
+   setGrabCommunications:true,
+   getGrabCommunications:true,
+   getControllerWorldLocation:true
+ */
+
+var GRAB_COMMUNICATIONS_SETTING = "io.highfidelity.isFarGrabbing";
+setGrabCommunications = function setFarGrabCommunications(on) {
+    Settings.setValue(GRAB_COMMUNICATIONS_SETTING, on ? "on" : "");
+};
+getGrabCommunications = function getFarGrabCommunications() {
+    return !!Settings.getValue(GRAB_COMMUNICATIONS_SETTING, "");
+};
+
+// this offset needs to match the one in libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp:378
+
+getGrabPointSphereOffset = function(handController, ignoreSensorToWorldScale) {
+    var GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 };  // x = upward, y = forward, z = lateral
+    var offset = GRAB_POINT_SPHERE_OFFSET;
+    if (handController === Controller.Standard.LeftHand) {
+        offset = {
+            x: -GRAB_POINT_SPHERE_OFFSET.x,
+            y: GRAB_POINT_SPHERE_OFFSET.y,
+            z: GRAB_POINT_SPHERE_OFFSET.z
+        };
+    }
+    if (ignoreSensorToWorldScale) {
+        return offset;
+    } else {
+        return Vec3.multiply(MyAvatar.sensorToWorldScale, offset);
+    }
+};
+
+// controllerWorldLocation is where the controller would be, in-world, with an added offset
+getControllerWorldLocation = function (handController, doOffset) {
+    var orientation;
+    var position;
+    var valid = false;
+
+    if (handController >= 0) {
+        var pose = Controller.getPoseValue(handController);
+        valid = pose.valid;
+        var controllerJointIndex;
+        if (pose.valid) {
+            if (handController === Controller.Standard.RightHand) {
+                controllerJointIndex = MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND");
+            } else {
+                controllerJointIndex = MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_LEFTHAND");
+            }
+            orientation = Quat.multiply(MyAvatar.orientation, MyAvatar.getAbsoluteJointRotationInObjectFrame(controllerJointIndex));
+            position = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, MyAvatar.getAbsoluteJointTranslationInObjectFrame(controllerJointIndex)));
+
+            // add to the real position so the grab-point is out in front of the hand, a bit
+            if (doOffset) {
+                var offset = getGrabPointSphereOffset(handController);
+                position = Vec3.sum(position, Vec3.multiplyQbyV(orientation, offset));
+            }
+
+        } else if (!HMD.isHandControllerAvailable()) {
+            // NOTE: keep this offset in sync with scripts/system/controllers/handControllerPointer.js:493
+            var VERTICAL_HEAD_LASER_OFFSET = 0.1 * MyAvatar.sensorToWorldScale;
+            position = Vec3.sum(Camera.position, Vec3.multiplyQbyV(Camera.orientation, {x: 0, y: VERTICAL_HEAD_LASER_OFFSET, z: 0}));
+            orientation = Quat.multiply(Camera.orientation, Quat.angleAxis(-90, { x: 1, y: 0, z: 0 }));
+            valid = true;
+        }
+    }
+
+    return {position: position,
+            translation: position,
+            orientation: orientation,
+            rotation: orientation,
+            valid: valid};
+};
diff --git a/scripts/simplifiedUI/system/libraries/dataViewHelpers.js b/scripts/simplifiedUI/system/libraries/dataViewHelpers.js
new file mode 100644
index 0000000000..c6b83ad1f6
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/dataViewHelpers.js
@@ -0,0 +1,62 @@
+//
+//  dataViewHelpers.js
+//  examples/libraries
+//
+//  Copyright 2014 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
+//
+
+if (typeof DataView.prototype.indexOf !== "function") {
+    DataView.prototype.indexOf = function (searchString, position) {
+        var searchLength = searchString.length,
+            byteArrayLength = this.byteLength,
+            maxSearchIndex = byteArrayLength - searchLength,
+            searchCharCodes = [],
+            found,
+            i,
+            j;
+
+        searchCharCodes[searchLength] = 0;
+        for (j = 0; j < searchLength; j += 1) {
+            searchCharCodes[j] = searchString.charCodeAt(j);
+        }
+
+        i = position;
+        found = false;
+        while (i < maxSearchIndex && !found) {
+            j = 0;
+            while (j < searchLength && this.getUint8(i + j) === searchCharCodes[j]) {
+                j += 1;
+            }
+            found = (j === searchLength);
+            i += 1;
+        }
+
+        return found ? i - 1 : -1;
+    };
+}
+
+if (typeof DataView.prototype.string !== "function") {
+    DataView.prototype.string = function (start, length) {
+        var charCodes = [],
+            end,
+            i;
+
+        if (start === undefined) {
+            start = 0;
+        }
+        if (length === undefined) {
+            length = this.length;
+        }
+
+        end = start + length;
+        for (i = start; i < end; i += 1) {
+            charCodes.push(this.getUint8(i));
+        }
+
+        return String.fromCharCode.apply(String, charCodes);
+    };
+}
+
diff --git a/scripts/simplifiedUI/system/libraries/entityCameraTool.js b/scripts/simplifiedUI/system/libraries/entityCameraTool.js
new file mode 100644
index 0000000000..4410f19a5e
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/entityCameraTool.js
@@ -0,0 +1,657 @@
+//
+//  entityCameraTool.js
+//  examples
+//
+//  Created by Ryan Huffman on 10/14/14.
+//  Copyright 2014 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
+//
+
+Script.include("overlayUtils.js");
+
+var MOUSE_SENSITIVITY = 0.9;
+var SCROLL_SENSITIVITY = 0.05;
+var PAN_ZOOM_SCALE_RATIO = 0.4;
+
+var KEY_ORBIT_SENSITIVITY = 90;
+var KEY_ZOOM_SENSITIVITY = 3;
+
+// Scaling applied based on the size of the object being focused (Larger values focus further away)
+var FOCUS_ZOOM_SCALE = 2.3;
+
+// Minimum zoom level when focusing on an object
+var FOCUS_MIN_ZOOM = 0.5;
+
+// Scaling applied based on the current zoom level
+var ZOOM_SCALING = 0.02;
+
+var MIN_ZOOM_DISTANCE = 0.01;
+
+// The maximum usable zoom level is somewhere around 14km, further than that the edit handles will fade-out. (FIXME: MS17493)
+var MAX_ZOOM_DISTANCE = 14000;
+
+var MODE_INACTIVE = 'inactive';
+var MODE_ORBIT = 'orbit';
+var MODE_PAN = 'pan';
+
+var EASING_MULTIPLIER = 8;
+
+var INITIAL_ZOOM_DISTANCE = 2;
+var INITIAL_ZOOM_DISTANCE_FIRST_PERSON = 3;
+
+var easeOutCubic = function(t) {
+    t--;
+    return t * t * t + 1;
+};
+
+EASE_TIME = 0.5;
+
+function clamp(value, minimum, maximum) {
+    return Math.min(Math.max(value, minimum), maximum);
+}
+
+function mergeObjects(obj1, obj2) {
+    var newObj = {};
+    for (key in obj1) {
+        newObj[key] = obj1[key];
+    }
+    for (key in obj2) {
+        newObj[key] = obj2[key];
+    }
+    return newObj;
+}
+
+CameraManager = function() {
+    var that = {};
+
+    that.enabled = false;
+    that.mode = MODE_INACTIVE;
+
+    var actions = {
+        orbitLeft: 0,
+        orbitRight: 0,
+        orbitUp: 0,
+        orbitDown: 0,
+        orbitForward: 0,
+        orbitBackward: 0,
+    }
+
+    var keyToActionMapping = {
+        65: "orbitLeft",     // "a"
+        68: "orbitRight",    // "d"
+        87: "orbitForward",  // "w"
+        83: "orbitBackward", // "s"
+        69: "orbitUp",       // "e"
+        67: "orbitDown",     // "c"
+        
+        16777234: "orbitLeft",     // "LEFT"
+        16777236: "orbitRight",    // "RIGHT"
+        16777235: "orbitForward",  // "UP"
+        16777237: "orbitBackward", // "DOWN"
+    }
+
+    var CAPTURED_KEYS = [];
+    for (var key in keyToActionMapping) {
+        CAPTURED_KEYS.push(key);
+    }
+
+    function getActionForKeyEvent(event) {
+        if (!event.isControl) {
+            var action = keyToActionMapping[event.key];
+            if (action !== undefined) {
+                if (event.isShifted) {
+                    if (action === "orbitForward") {
+                        action = "orbitUp";
+                    } else if (action === "orbitBackward") {
+                        action = "orbitDown";
+                    }
+                }
+                return action;
+            }
+        }
+        return null;
+    }
+
+    that.zoomDistance = INITIAL_ZOOM_DISTANCE;
+    that.targetZoomDistance = INITIAL_ZOOM_DISTANCE;
+
+    that.yaw = 0;
+    that.pitch = 0;
+    that.targetYaw = 0;
+    that.targetPitch = 0;
+
+    that.focalPoint = Vec3.ZERO;
+    that.targetFocalPoint = Vec3.ZERO;
+
+    easing = false;
+    easingTime = 0;
+    startOrientation = Quat.IDENTITY;
+
+    that.previousCameraMode = null;
+
+    that.lastMousePosition = {
+        x: 0,
+        y: 0
+    };
+
+    that.enable = function() {
+        if (Camera.mode === "independent" || that.enabled || HMD.active) {
+            return;
+        }
+
+        for (var i = 0; i < CAPTURED_KEYS.length; i++) {
+            Controller.captureKeyEvents({
+                text: CAPTURED_KEYS[i]
+            });
+        }
+
+        for (var action in actions) {
+            actions[action] = 0;
+        }
+
+        that.enabled = true;
+        that.mode = MODE_INACTIVE;
+
+        // Pick a point INITIAL_ZOOM_DISTANCE in front of the camera to use as a focal point
+        that.zoomDistance = INITIAL_ZOOM_DISTANCE;
+        that.targetZoomDistance = that.zoomDistance + 3.0;
+        var focalPoint = Vec3.sum(Camera.getPosition(),
+            Vec3.multiply(that.zoomDistance, Quat.getForward(Camera.getOrientation())));
+
+        // Determine the correct yaw and pitch to keep the camera in the same location
+        var dPos = Vec3.subtract(focalPoint, Camera.getPosition());
+        var xzDist = Math.sqrt(dPos.x * dPos.x + dPos.z * dPos.z);
+
+        that.targetPitch = -Math.atan2(dPos.y, xzDist) * 180 / Math.PI;
+        that.targetPitch += (90 - that.targetPitch) / 3.0; // Swing camera "up" to look down at the focal point
+        that.targetYaw = Math.atan2(dPos.x, dPos.z) * 180 / Math.PI;
+        that.pitch = that.targetPitch;
+        that.yaw = that.targetYaw;
+
+        that.focalPoint = focalPoint;
+        that.setFocalPoint(focalPoint);
+        that.previousCameraMode = Camera.mode;
+        Camera.mode = "independent";
+
+        that.updateCamera();
+
+        cameraTool.setVisible(true);
+    }
+
+    that.disable = function(ignoreCamera) {
+        if (!that.enabled) {
+            return;
+        }
+
+        for (var i = 0; i < CAPTURED_KEYS.length; i++) {
+            Controller.releaseKeyEvents({
+                text: CAPTURED_KEYS[i]
+            });
+        }
+
+        that.enabled = false;
+        that.mode = MODE_INACTIVE;
+
+        if (!ignoreCamera) {
+            Camera.mode = that.previousCameraMode;
+        }
+        cameraTool.setVisible(false);
+    }
+
+    that.focus = function(position, dimensions, easeOrientation) {
+        if (dimensions) {
+            var size = Math.max(dimensions.x, Math.max(dimensions.y, dimensions.z));
+            that.targetZoomDistance = Math.max(size * FOCUS_ZOOM_SCALE, FOCUS_MIN_ZOOM);
+        } else {
+            that.targetZoomDistance = Vec3.length(Vec3.subtract(Camera.getPosition(), position));
+        }
+
+        if (easeOrientation) {
+            // Do eased turning towards target
+            that.focalPoint = that.targetFocalPoint = position;
+
+            that.zoomDistance = that.targetZoomDistance = Vec3.length(Vec3.subtract(Camera.getPosition(), position));
+
+            var dPos = Vec3.subtract(that.focalPoint, Camera.getPosition());
+            var xzDist = Math.sqrt(dPos.x * dPos.x + dPos.z * dPos.z);
+
+            that.targetPitch = -Math.atan2(dPos.y, xzDist) * 180 / Math.PI;
+            that.targetYaw = Math.atan2(dPos.x, dPos.z) * 180 / Math.PI;
+            that.pitch = that.targetPitch;
+            that.yaw = that.targetYaw;
+
+            startOrientation = Camera.getOrientation();
+
+            easing = true;
+            easingTime = 0;
+        } else {
+            that.setFocalPoint(position);
+        }
+
+        that.updateCamera();
+    }
+
+    that.setTargetPitchYaw = function(pitch, yaw) {
+        that.targetPitch = pitch;
+        that.targetYaw = yaw;
+    }
+
+    that.moveFocalPoint = function(dPos) {
+        that.setFocalPoint(Vec3.sum(that.focalPoint, dPos));
+    }
+
+    that.setFocalPoint = function(pos) {
+        that.targetFocalPoint = pos;
+        that.updateCamera();
+    }
+
+    that.addYaw = function(yaw) {
+        that.targetYaw += yaw;
+        that.updateCamera();
+    }
+
+    that.addPitch = function(pitch) {
+        that.targetPitch += pitch;
+        that.updateCamera();
+    }
+
+    that.addZoom = function(zoom) {
+        zoom *= that.targetZoomDistance * ZOOM_SCALING;
+        that.targetZoomDistance = Math.min(Math.max(that.targetZoomDistance + zoom, MIN_ZOOM_DISTANCE), MAX_ZOOM_DISTANCE);
+        that.updateCamera();
+    }
+
+    that.pan = function(offset) {
+        var up = Quat.getUp(Camera.getOrientation());
+        var right = Quat.getRight(Camera.getOrientation());
+
+        up = Vec3.multiply(up, offset.y * 0.01 * PAN_ZOOM_SCALE_RATIO * that.zoomDistance);
+        right = Vec3.multiply(right, offset.x * 0.01 * PAN_ZOOM_SCALE_RATIO * that.zoomDistance);
+
+        var dPosition = Vec3.sum(up, right);
+
+        that.moveFocalPoint(dPosition);
+    }
+
+    that.mouseMoveEvent = function(event) {
+        if (that.enabled && that.mode !== MODE_INACTIVE) {
+            var x = Reticle.getPosition().x;
+            var y = Reticle.getPosition().y;
+            if (!hasDragged) {
+                that.lastMousePosition.x = x;
+                that.lastMousePosition.y = y;
+                hasDragged = true;
+            }
+            if (that.mode === MODE_ORBIT) {
+                var diffX = x - that.lastMousePosition.x;
+                var diffY = y - that.lastMousePosition.y;
+                that.targetYaw -= MOUSE_SENSITIVITY * (diffX / 5.0);
+                that.targetPitch += MOUSE_SENSITIVITY * (diffY / 10.0);
+
+                while (that.targetYaw > 180.0) that.targetYaw -= 360;
+                while (that.targetYaw < -180.0) that.targetYaw += 360;
+
+                if (that.targetPitch > 90) that.targetPitch = 90;
+                if (that.targetPitch < -90) that.targetPitch = -90;
+
+                that.updateCamera();
+            } else if (that.mode === MODE_PAN) {
+                var diffX = x - that.lastMousePosition.x;
+                var diffY = y - that.lastMousePosition.y;
+
+                var up = Quat.getUp(Camera.getOrientation());
+                var right = Quat.getRight(Camera.getOrientation());
+
+                up = Vec3.multiply(up, diffY * 0.01 * PAN_ZOOM_SCALE_RATIO * that.zoomDistance);
+                right = Vec3.multiply(right, -diffX * 0.01 * PAN_ZOOM_SCALE_RATIO * that.zoomDistance);
+
+                var dPosition = Vec3.sum(up, right);
+
+                that.moveFocalPoint(dPosition);
+            }
+
+            var newX = x;
+            var newY = y;
+            var updatePosition = false;
+
+            if (x <= 0) {
+                newX = Window.innerWidth;
+                updatePosition = true;
+            } else if (x >= Window.innerWidth) {
+                newX = 0;
+                updatePosition = true;
+            }
+
+            if (y <= 0) {
+                newY = Window.innerHeight;
+                updatePosition = true;
+            } else if (y >= Window.innerHeight) {
+                newY = 0;
+                updatePosition = true;
+            }
+
+            if (updatePosition) {
+                Reticle.setPosition({ x: newX, y: newY});
+            }
+
+            that.lastMousePosition.x = newX;
+            that.lastMousePosition.y = newY;
+
+            return true;
+        }
+        return false;
+    }
+
+    var hasDragged = false;
+    that.mousePressEvent = function(event) {
+
+        if (cameraTool.mousePressEvent(event)) {
+            return true;
+        }
+
+        if (!that.enabled) {
+            return;
+        }
+
+        if (event.isRightButton || (event.isLeftButton && event.isControl && !event.isShifted)) {
+            that.mode = MODE_ORBIT;
+        } else if (event.isMiddleButton || (event.isLeftButton && event.isControl && event.isShifted)) {
+            that.mode = MODE_PAN;
+        }
+
+        if (that.mode !== MODE_INACTIVE) {
+            hasDragged = false;
+            return true;
+        }
+
+        return false;
+    }
+
+    that.mouseReleaseEvent = function(event) {
+
+        if (!that.enabled) {
+            return;
+        }
+
+        that.mode = MODE_INACTIVE;
+        Reticle.setVisible(true);
+
+    }
+
+    that.keyPressEvent = function(event) {
+        var action = getActionForKeyEvent(event);
+        if (action) {
+            actions[action] = 1;
+        }
+    };
+
+    that.keyReleaseEvent = function(event) {
+        var action = getActionForKeyEvent(event);
+        if (action) {
+            actions[action] = 0;
+        }
+    };
+
+    that.wheelEvent = function(event) {
+        if (!that.enabled) {
+            return;
+        }
+
+        var dZoom = -event.delta * SCROLL_SENSITIVITY;
+
+        // Scale based on current zoom level
+        dZoom *= that.targetZoomDistance * ZOOM_SCALING;
+
+        that.targetZoomDistance = Math.min(Math.max(that.targetZoomDistance + dZoom, MIN_ZOOM_DISTANCE), MAX_ZOOM_DISTANCE);
+
+        that.updateCamera();
+    }
+
+    that.updateCamera = function() {
+        if (!that.enabled || Camera.mode !== "independent") {
+            cameraTool.update();
+            return;
+        }
+
+        var yRot = Quat.angleAxis(that.yaw, {
+            x: 0,
+            y: 1,
+            z: 0
+        });
+        var xRot = Quat.angleAxis(that.pitch, {
+            x: 1,
+            y: 0,
+            z: 0
+        });
+        var q = Quat.multiply(yRot, xRot);
+
+        var pos = Vec3.multiply(Quat.getForward(q), that.zoomDistance);
+        Camera.setPosition(Vec3.sum(that.focalPoint, pos));
+
+        yRot = Quat.angleAxis(that.yaw - 180, {
+            x: 0,
+            y: 1,
+            z: 0
+        });
+        xRot = Quat.angleAxis(-that.pitch, {
+            x: 1,
+            y: 0,
+            z: 0
+        });
+        q = Quat.multiply(yRot, xRot);
+
+        if (easing) {
+            var t = easeOutCubic(easingTime / EASE_TIME);
+            q = Quat.slerp(startOrientation, q, t);
+        }
+
+        Camera.setOrientation(q);
+
+        cameraTool.update();
+    }
+
+    function normalizeDegrees(degrees) {
+        while (degrees > 180) {
+            degrees -= 360;
+        }
+        while (degrees < -180) {
+            degrees += 360;
+        }
+        return degrees;
+    }
+
+    // Ease the position and orbit of the camera
+    that.update = function(dt) {
+        if (Camera.mode !== "independent") {
+            that.updateCamera();
+            return;
+        }
+
+        // Update based on current actions
+        that.targetYaw += (actions.orbitRight - actions.orbitLeft) * dt * KEY_ORBIT_SENSITIVITY;
+        that.targetPitch += (actions.orbitUp - actions.orbitDown) * dt * KEY_ORBIT_SENSITIVITY;
+        that.targetPitch = clamp(that.targetPitch, -90, 90);
+
+        var dZoom = actions.orbitBackward - actions.orbitForward;
+        if (dZoom) {
+            dZoom *= that.targetZoomDistance * dt * KEY_ZOOM_SENSITIVITY;
+            that.targetZoomDistance += dZoom;
+            that.targetZoomDistance = clamp(that.targetZoomDistance, MIN_ZOOM_DISTANCE, MAX_ZOOM_DISTANCE);
+        }
+
+        if (easing) {
+            easingTime = Math.min(EASE_TIME, easingTime + dt);
+        }
+
+        var scale = Math.min(dt * EASING_MULTIPLIER, 1.0);
+
+        var dYaw = normalizeDegrees(that.targetYaw - that.yaw);
+        var dPitch = normalizeDegrees(that.targetPitch - that.pitch);
+
+        that.yaw += scale * dYaw;
+        that.pitch += scale * dPitch;
+
+        // Normalize between [-180, 180]
+        that.yaw = normalizeDegrees(that.yaw);
+        that.pitch = normalizeDegrees(that.pitch);
+
+        var dFocal = Vec3.subtract(that.targetFocalPoint, that.focalPoint);
+        that.focalPoint = Vec3.sum(that.focalPoint, Vec3.multiply(scale, dFocal));
+
+        var dZoom = that.targetZoomDistance - that.zoomDistance;
+        that.zoomDistance += scale * dZoom;
+
+        that.updateCamera();
+
+        if (easingTime >= 1) {
+            easing = false;
+        }
+    }
+
+    // Last mode that was first or third person
+    var lastAvatarCameraMode = "first person";
+    Camera.modeUpdated.connect(function(newMode) {
+        if (newMode != "independent") {
+            lastAvatarCameraMode = newMode;
+            that.disable(true);
+        } else {
+            that.enable();
+        }
+    });
+
+    Controller.keyReleaseEvent.connect(function(event) {
+        if (event.text == "ESC" && that.enabled) {
+            Camera.mode = lastAvatarCameraMode;
+            cameraManager.disable(true);
+        }
+    });
+
+    Script.update.connect(that.update);
+    Script.scriptEnding.connect(that.disable);
+
+    Controller.wheelEvent.connect(that.wheelEvent);
+
+    var cameraTool = new CameraTool(that);
+
+    return that;
+}
+
+CameraTool = function(cameraManager) {
+    var that = {};
+
+    var RED = {
+        red: 191,
+        green: 78,
+        blue: 38
+    };
+    var GREEN = {
+        red: 26,
+        green: 193,
+        blue: 105
+    };
+    var BLUE = {
+        red: 0,
+        green: 131,
+        blue: 204
+    };
+
+    var BORDER_WIDTH = 1;
+
+    var ORIENTATION_OVERLAY_SIZE = 26;
+    var ORIENTATION_OVERLAY_HALF_SIZE = ORIENTATION_OVERLAY_SIZE / 2;
+    var ORIENTATION_OVERLAY_CUBE_SIZE = 10.5;
+
+    var ORIENTATION_OVERLAY_OFFSET = {
+        x: 30,
+        y: 30,
+    }
+
+    var UI_WIDTH = 70;
+    var UI_HEIGHT = 70;
+    var UI_PADDING = 10;
+
+    var lastKnownWidth = Window.innerWidth;
+
+    var uiPosition = {
+        x: lastKnownWidth - UI_WIDTH - UI_PADDING,
+        y: UI_PADDING,
+    };
+
+    var backgroundBorder = Overlays.addOverlay("text", {
+        x: uiPosition.x - BORDER_WIDTH,
+        y: uiPosition.y - BORDER_WIDTH,
+        width: UI_WIDTH + BORDER_WIDTH * 2,
+        height: UI_HEIGHT + BORDER_WIDTH * 2,
+        alpha: 0,
+        text: "",
+        backgroundColor: {
+            red: 101,
+            green: 101,
+            blue: 101
+        },
+        backgroundAlpha: 1.0,
+        visible: false,
+    });
+
+    var background = Overlays.addOverlay("text", {
+        x: uiPosition.x,
+        y: uiPosition.y,
+        width: UI_WIDTH,
+        height: UI_HEIGHT,
+        alpha: 0,
+        text: "",
+        backgroundColor: {
+            red: 51,
+            green: 51,
+            blue: 51
+        },
+        backgroundAlpha: 1.0,
+        visible: false,
+    });
+
+    Script.scriptEnding.connect(function() {
+        Overlays.deleteOverlay(background);
+        Overlays.deleteOverlay(backgroundBorder);
+    });
+
+    var flip = Quat.fromPitchYawRollDegrees(0, 180, 0);
+    that.update = function() {
+        if (Window.innerWidth != lastKnownWidth) {
+            lastKnownWidth = Window.innerWidth;
+            uiPosition = {
+                x: lastKnownWidth - UI_WIDTH - UI_PADDING,
+                y: UI_PADDING,
+            };
+            Overlays.editOverlay(backgroundBorder, {
+                x: uiPosition.x - BORDER_WIDTH,
+                y: uiPosition.y - BORDER_WIDTH,
+            });
+            Overlays.editOverlay(background, {
+                x: uiPosition.x,
+                y: uiPosition.y,
+            });
+        }
+    }
+
+    that.mousePressEvent = function(event) {
+        var clickedOverlay = Overlays.getOverlayAtPoint({
+            x: event.x,
+            y: event.y
+        });
+    };
+
+    that.setVisible = function(visible) {
+        Overlays.editOverlay(background, {
+            visible: visible
+        });
+        Overlays.editOverlay(backgroundBorder, {
+            visible: visible
+        });
+    };
+
+    that.setVisible(false);
+
+    return that;
+};
diff --git a/scripts/simplifiedUI/system/libraries/entityIconOverlayManager.js b/scripts/simplifiedUI/system/libraries/entityIconOverlayManager.js
new file mode 100644
index 0000000000..68104ff4bb
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/entityIconOverlayManager.js
@@ -0,0 +1,155 @@
+/* globals EntityIconOverlayManager:true */
+
+EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) {
+
+    var visible = false;
+
+    // List of all created overlays
+    var allOverlays = [];
+
+    // List of overlays not currently being used
+    var unusedOverlays = [];
+
+    // Map from EntityItemID to overlay id
+    var entityOverlays = {};
+
+    // Map from EntityItemID to EntityItemID object
+    var entityIDs = {};
+
+    this.updatePositions = function(ids) {
+        for (var id in entityIDs) {
+            var entityID = entityIDs[id];
+            var properties = Entities.getEntityProperties(entityID);
+            var overlayProperties = {
+                position: properties.position
+            };
+            if (getOverlayPropertiesFunc) {
+                var customProperties = getOverlayPropertiesFunc(entityID, properties);
+                for (var key in customProperties) {
+                    overlayProperties[key] = customProperties[key];
+                }
+            }
+            Overlays.editOverlay(entityOverlays[entityID], overlayProperties);
+        }
+    };
+
+    // Finds the id for the corresponding entity that is associated with an overlay id. 
+    // Returns null if the overlay id is not contained in this manager.
+    this.findEntity = function(overlayId) {
+        for (var id in entityOverlays) {
+            if (overlayId === entityOverlays[id]) {
+                return entityIDs[id];
+            }
+        }
+        
+        return null;
+    };
+
+    this.findRayIntersection = function(pickRay) {
+        var result = Overlays.findRayIntersection(pickRay);
+
+        if (result.intersects) {
+            result.entityID = this.findEntity(result.overlayID);
+
+            if (result.entityID === null) {
+                result.intersects = false;
+            }
+        }
+
+        return result;
+    };
+
+    this.setVisible = function(isVisible) {
+        if (visible !== isVisible) {
+            visible = isVisible;
+            for (var id in entityOverlays) {
+                Overlays.editOverlay(entityOverlays[id], {
+                    visible: visible,
+                    ignorePickIntersection: !visible
+                });
+            }
+        }
+    };
+
+    // Allocate or get an unused overlay
+    function getOverlay() {
+        var overlay;
+        if (unusedOverlays.length === 0) {
+            overlay = Overlays.addOverlay("image3d", {});
+            allOverlays.push(overlay);
+        } else {
+            overlay = unusedOverlays.pop();
+        }
+        return overlay;
+    }
+
+    function releaseOverlay(overlay) {
+        unusedOverlays.push(overlay);
+        Overlays.editOverlay(overlay, {
+            visible: false,
+            ignorePickIntersection: true
+        });
+    }
+
+    function addEntity(entityID) {
+        var properties = Entities.getEntityProperties(entityID, ['position', 'type']);
+        if (entityTypes.indexOf(properties.type) > -1 && !(entityID in entityOverlays)) {
+            var overlay = getOverlay();
+            entityOverlays[entityID] = overlay;
+            entityIDs[entityID] = entityID;
+            var overlayProperties = {
+                position: properties.position,
+                rotation: Quat.fromPitchYawRollDegrees(0, 0, 270),
+                visible: visible,
+                ignorePickIntersection: !visible,
+                alpha: 0.9,
+                scale: 0.5,
+                drawInFront: true,
+                isFacingAvatar: true,
+                color: {
+                    red: 255,
+                    green: 255,
+                    blue: 255
+                }
+            };
+            if (getOverlayPropertiesFunc) {
+                var customProperties = getOverlayPropertiesFunc(entityID, properties);
+                for (var key in customProperties) {
+                    overlayProperties[key] = customProperties[key];
+                }
+            }
+            Overlays.editOverlay(overlay, overlayProperties);
+        }
+    }
+
+    function deleteEntity(entityID) {
+        if (entityID in entityOverlays) {
+            releaseOverlay(entityOverlays[entityID]);
+            delete entityOverlays[entityID];
+        }
+    }
+
+    function clearEntities() {
+        for (var id in entityOverlays) {
+            releaseOverlay(entityOverlays[id]);
+        }
+        entityOverlays = {};
+        entityIDs = {};
+    }
+
+    Entities.addingEntity.connect(addEntity);
+    Entities.deletingEntity.connect(deleteEntity);
+    Entities.clearingEntities.connect(clearEntities);
+
+    // Add existing entities
+    var ids = Entities.findEntities(MyAvatar.position, 64000);
+    for (var i = 0; i < ids.length; i++) {
+        addEntity(ids[i]);
+    }
+
+    Script.scriptEnding.connect(function() {
+        for (var i = 0; i < allOverlays.length; i++) {
+            Overlays.deleteOverlay(allOverlays[i]);
+        }
+    });
+};
diff --git a/scripts/simplifiedUI/system/libraries/entityList.js b/scripts/simplifiedUI/system/libraries/entityList.js
new file mode 100644
index 0000000000..6498c92f17
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/entityList.js
@@ -0,0 +1,330 @@
+"use strict";
+
+//  entityList.js
+//
+//  Copyright 2014 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
+//
+
+/* global EntityListTool, Tablet, selectionManager, Entities, Camera, MyAvatar, Vec3, Menu, Messages,
+   cameraManager, MENU_EASE_ON_FOCUS, deleteSelectedEntities, toggleSelectedEntitiesLocked, toggleSelectedEntitiesVisible,
+   keyUpEventFromUIWindow, Script, SelectionDisplay, SelectionManager, Clipboard */
+
+var PROFILING_ENABLED = false;
+var profileIndent = '';
+const PROFILE_NOOP = function(_name, fn, args) {
+    fn.apply(this, args);
+};
+const PROFILE = !PROFILING_ENABLED ? PROFILE_NOOP : function(name, fn, args) {
+    console.log("PROFILE-Script " + profileIndent + "(" + name + ") Begin");
+    var previousIndent = profileIndent;
+    profileIndent += '  ';
+    var before = Date.now();
+    fn.apply(this, args);
+    var delta = Date.now() - before;
+    profileIndent = previousIndent;
+    console.log("PROFILE-Script " + profileIndent + "(" + name + ") End " + delta + "ms");
+};
+
+EntityListTool = function(shouldUseEditTabletApp) {
+    var that = {};
+
+    var CreateWindow = Script.require('../modules/createWindow.js');
+
+    var TITLE_OFFSET = 60;
+    var ENTITY_LIST_WIDTH = 495;
+    var MAX_DEFAULT_CREATE_TOOLS_HEIGHT = 778;
+    var entityListWindow = new CreateWindow(
+        Script.resolvePath("EditEntityList.qml"),
+        'Entity List',
+        'com.highfidelity.create.entityListWindow',
+        function () {
+            var windowHeight = Window.innerHeight - TITLE_OFFSET;
+            if (windowHeight > MAX_DEFAULT_CREATE_TOOLS_HEIGHT) {
+                windowHeight = MAX_DEFAULT_CREATE_TOOLS_HEIGHT;
+            }
+            return {
+                size: {
+                    x: ENTITY_LIST_WIDTH,
+                    y: windowHeight
+                },
+                position: {
+                    x: Window.x,
+                    y: Window.y + TITLE_OFFSET
+                }
+            };
+        },
+        false
+    );
+
+    var webView = null;
+    webView = Tablet.getTablet("com.highfidelity.interface.tablet.system");
+    webView.setVisible = function(value){ };
+
+    var filterInView = false;
+    var searchRadius = 100;
+
+    var visible = false;
+
+    that.webView = webView;
+
+    that.setVisible = function(newVisible) {
+        visible = newVisible;
+        webView.setVisible(shouldUseEditTabletApp() && visible);
+        entityListWindow.setVisible(!shouldUseEditTabletApp() && visible);
+    };
+
+    that.isVisible = function() {
+        return entityListWindow.isVisible();
+    };
+
+    that.setVisible(false);
+
+    function emitJSONScriptEvent(data) {
+        var dataString;
+        PROFILE("Script-JSON.stringify", function() {
+            dataString = JSON.stringify(data);
+        });
+        PROFILE("Script-emitScriptEvent", function() {
+            webView.emitScriptEvent(dataString);
+            if (entityListWindow.window) {
+                entityListWindow.window.emitScriptEvent(dataString);
+            }
+        });
+    }
+
+    that.toggleVisible = function() {
+        that.setVisible(!visible);
+    };
+
+    selectionManager.addEventListener(function(isSelectionUpdate, caller) {
+        if (caller === that) {
+            // ignore events that we emitted from the entity list itself
+            return;
+        }
+        var selectedIDs = [];
+
+        for (var i = 0; i < selectionManager.selections.length; i++) {
+            selectedIDs.push(selectionManager.selections[i]);
+        }
+
+        emitJSONScriptEvent({
+            type: 'selectionUpdate',
+            selectedIDs: selectedIDs
+        });
+    });
+
+    that.setSpaceMode = function(spaceMode) {
+        emitJSONScriptEvent({
+            type: 'setSpaceMode',
+            spaceMode: spaceMode
+        });
+    };
+
+    that.clearEntityList = function() {
+        emitJSONScriptEvent({
+            type: 'clearEntityList'
+        });
+    };
+
+    that.removeEntities = function (deletedIDs, selectedIDs) {
+        emitJSONScriptEvent({
+            type: 'removeEntities',
+            deletedIDs: deletedIDs,
+            selectedIDs: selectedIDs
+        });
+    };
+
+    that.deleteEntities = function (deletedIDs) {
+        emitJSONScriptEvent({
+            type: "deleted",
+            ids: deletedIDs
+        });
+    };
+
+    function valueIfDefined(value) {
+        return value !== undefined ? value : "";
+    }
+
+    function entityIsBaked(properties) {
+        if (properties.type === "Model") {
+            var lowerModelURL = properties.modelURL.toLowerCase();
+            return lowerModelURL.endsWith(".baked.fbx") || lowerModelURL.endsWith(".baked.fst");
+        } else if (properties.type === "Zone") {
+            var lowerSkyboxURL = properties.skybox ? properties.skybox.url.toLowerCase() : "";
+            var lowerAmbientURL = properties.ambientLight ? properties.ambientLight.ambientURL.toLowerCase() : "";
+            return (lowerSkyboxURL === "" || lowerSkyboxURL.endsWith(".texmeta.json")) &&
+                (lowerAmbientURL === "" || lowerAmbientURL.endsWith(".texmeta.json"));
+        } else {
+            return false;
+        }
+    }
+
+    that.sendUpdate = function() {
+        PROFILE('Script-sendUpdate', function() {
+            var entities = [];
+
+            var ids;
+            PROFILE("findEntities", function() {
+                if (filterInView) {
+                    ids = Entities.findEntitiesInFrustum(Camera.frustum);
+                } else {
+                    ids = Entities.findEntities(MyAvatar.position, searchRadius);
+                }
+            });
+
+            var cameraPosition = Camera.position;
+            PROFILE("getMultipleProperties", function () {
+                var multipleProperties = Entities.getMultipleEntityProperties(ids, ['name', 'type', 'locked',
+                    'visible', 'renderInfo', 'modelURL', 'materialURL', 'imageURL', 'script', 'certificateID',
+                    'skybox.url', 'ambientLight.url']);
+                for (var i = 0; i < multipleProperties.length; i++) {
+                    var properties = multipleProperties[i];
+
+                    if (!filterInView || Vec3.distance(properties.position, cameraPosition) <= searchRadius) {
+                        var url = "";
+                        if (properties.type === "Model") {
+                            url = properties.modelURL;
+                        } else if (properties.type === "Material") {
+                            url = properties.materialURL;
+                        } else if (properties.type === "Image") {
+                            url = properties.imageURL;
+                        }
+                        entities.push({
+                            id: ids[i],
+                            name: properties.name,
+                            type: properties.type,
+                            url: url,
+                            locked: properties.locked,
+                            visible: properties.visible,
+                            certificateID: properties.certificateID,
+                            verticesCount: (properties.renderInfo !== undefined ?
+                                valueIfDefined(properties.renderInfo.verticesCount) : ""),
+                            texturesCount: (properties.renderInfo !== undefined ?
+                                valueIfDefined(properties.renderInfo.texturesCount) : ""),
+                            texturesSize: (properties.renderInfo !== undefined ?
+                                valueIfDefined(properties.renderInfo.texturesSize) : ""),
+                            hasTransparent: (properties.renderInfo !== undefined ?
+                                valueIfDefined(properties.renderInfo.hasTransparent) : ""),
+                            isBaked: entityIsBaked(properties),
+                            drawCalls: (properties.renderInfo !== undefined ?
+                                valueIfDefined(properties.renderInfo.drawCalls) : ""),
+                            hasScript: properties.script !== ""
+                        });
+                    }
+                }
+            });
+
+            var selectedIDs = [];
+            for (var j = 0; j < selectionManager.selections.length; j++) {
+                selectedIDs.push(selectionManager.selections[j]);
+            }
+
+            emitJSONScriptEvent({
+                type: "update",
+                entities: entities,
+                selectedIDs: selectedIDs,
+                spaceMode: SelectionDisplay.getSpaceMode(),
+            });
+        });
+    };
+
+    function onFileSaveChanged(filename) {
+        Window.saveFileChanged.disconnect(onFileSaveChanged);
+        if (filename !== "") {
+            var success = Clipboard.exportEntities(filename, selectionManager.selections);
+            if (!success) {
+                Window.notifyEditError("Export failed.");
+            }
+        }
+    }
+
+    var onWebEventReceived = function(data) {
+        try {
+            data = JSON.parse(data);
+        } catch(e) {
+            print("entityList.js: Error parsing JSON");
+            return;
+        }
+
+        if (data.type === "selectionUpdate") {
+            var ids = data.entityIds;
+            var entityIDs = [];
+            for (var i = 0; i < ids.length; i++) {
+                entityIDs.push(ids[i]);
+            }
+            selectionManager.setSelections(entityIDs, that);
+            if (data.focus) {
+                cameraManager.enable();
+                cameraManager.focus(selectionManager.worldPosition,
+                                    selectionManager.worldDimensions,
+                                    Menu.isOptionChecked(MENU_EASE_ON_FOCUS));
+            }
+        } else if (data.type === "refresh") {
+            that.sendUpdate();
+        } else if (data.type === "teleport") {
+            if (selectionManager.hasSelection()) {
+                MyAvatar.position = selectionManager.worldPosition;
+            }
+        } else if (data.type === "export") {
+            if (!selectionManager.hasSelection()) {
+                Window.notifyEditError("No entities have been selected.");
+            } else {
+                Window.saveFileChanged.connect(onFileSaveChanged);
+                Window.saveAsync("Select Where to Save", "", "*.json");
+            }
+        } else if (data.type === "pal") {
+            var sessionIds = {}; // Collect the sessionsIds of all selected entities, w/o duplicates.
+            selectionManager.selections.forEach(function (id) {
+                var lastEditedBy = Entities.getEntityProperties(id, 'lastEditedBy').lastEditedBy;
+                if (lastEditedBy) {
+                    sessionIds[lastEditedBy] = true;
+                }
+            });
+            var dedupped = Object.keys(sessionIds);
+            if (!selectionManager.selections.length) {
+                Window.alert('No objects selected.');
+            } else if (!dedupped.length) {
+                Window.alert('There were no recent users of the ' + selectionManager.selections.length + ' selected objects.');
+            } else {
+                // No need to subscribe if we're just sending.
+                Messages.sendMessage('com.highfidelity.pal', JSON.stringify({method: 'select', params: [dedupped, true, false]}), 'local');
+            }
+        } else if (data.type === "delete") {
+            deleteSelectedEntities();
+        } else if (data.type === "toggleLocked") {
+            toggleSelectedEntitiesLocked();
+        } else if (data.type === "toggleVisible") {
+            toggleSelectedEntitiesVisible();
+        } else if (data.type === "filterInView") {
+            filterInView = data.filterInView === true;
+        } else if (data.type === "radius") {
+            searchRadius = data.radius;
+        } else if (data.type === "cut") {
+            SelectionManager.cutSelectedEntities();
+        } else if (data.type === "copy") {
+            SelectionManager.copySelectedEntities();
+        } else if (data.type === "paste") {
+            SelectionManager.pasteEntities();
+        } else if (data.type === "duplicate") {
+            SelectionManager.duplicateSelection();
+            that.sendUpdate();
+        } else if (data.type === "rename") {
+            Entities.editEntity(data.entityID, {name: data.name});
+            // make sure that the name also gets updated in the properties window
+            SelectionManager._update();
+        } else if (data.type === "toggleSpaceMode") {
+            SelectionDisplay.toggleSpaceMode();
+        } else if (data.type === 'keyUpEvent') {
+            keyUpEventFromUIWindow(data.keyUpEvent);
+        }
+    };
+
+    webView.webEventReceived.connect(onWebEventReceived);
+    entityListWindow.webEventReceived.addListener(onWebEventReceived);
+    that.interactiveWindowHidden = entityListWindow.interactiveWindowHidden;
+
+    return that;
+};
diff --git a/scripts/simplifiedUI/system/libraries/entitySelectionTool.js b/scripts/simplifiedUI/system/libraries/entitySelectionTool.js
new file mode 100644
index 0000000000..3fdc1d6652
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/entitySelectionTool.js
@@ -0,0 +1,2925 @@
+//
+//  entitySelectionTool.js
+//  examples
+//
+//  Created by Brad hefta-Gaub on 10/1/14.
+//    Modified by Daniela Fontes * @DanielaFifo and Tiago Andrade @TagoWill on 4/7/2017
+//    Modified by David Back on 1/9/2018
+//  Copyright 2014 High Fidelity, Inc.
+//
+//  This script implements a class useful for building tools for editing entities.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+/* global SelectionManager, SelectionDisplay, grid, rayPlaneIntersection, rayPlaneIntersection2, pushCommandForSelections,
+   getMainTabletIDs, getControllerWorldLocation, TRIGGER_ON_VALUE */
+
+const SPACE_LOCAL = "local";
+const SPACE_WORLD = "world";
+const HIGHLIGHT_LIST_NAME = "editHandleHighlightList";
+
+Script.include([
+    "./controllers.js",
+    "./controllerDispatcherUtils.js",
+    "./utils.js"
+]);
+
+
+function deepCopy(v) {
+    return JSON.parse(JSON.stringify(v));
+}
+
+SelectionManager = (function() {
+    var that = {};
+
+    // FUNCTION: SUBSCRIBE TO UPDATE MESSAGES
+    function subscribeToUpdateMessages() {
+        Messages.subscribe("entityToolUpdates");
+        Messages.messageReceived.connect(handleEntitySelectionToolUpdates);
+    }
+
+    // FUNCTION: HANDLE ENTITY SELECTION TOOL UPDATES
+    function handleEntitySelectionToolUpdates(channel, message, sender) {
+        if (channel !== 'entityToolUpdates') {
+            return;
+        }
+        if (sender !== MyAvatar.sessionUUID) {
+            return;
+        }
+
+        var wantDebug = false;
+        var messageParsed;
+        try {
+            messageParsed = JSON.parse(message);
+        } catch (err) {
+            print("ERROR: entitySelectionTool.handleEntitySelectionToolUpdates - got malformed message");
+            return;
+        }
+
+        if (messageParsed.method === "selectEntity") {
+            if (!SelectionDisplay.triggered() || SelectionDisplay.triggeredHand === messageParsed.hand) {
+                if (wantDebug) {
+                    print("setting selection to " + messageParsed.entityID);
+                }
+                that.setSelections([messageParsed.entityID], that);
+            }
+        } else if (messageParsed.method === "clearSelection") {
+            if (!SelectionDisplay.triggered() || SelectionDisplay.triggeredHand === messageParsed.hand) {
+                that.clearSelections();
+            }
+        } else if (messageParsed.method === "pointingAt") {
+            if (messageParsed.hand === Controller.Standard.RightHand) {
+                that.pointingAtDesktopWindowRight = messageParsed.desktopWindow;
+                that.pointingAtTabletRight = messageParsed.tablet;
+            } else {
+                that.pointingAtDesktopWindowLeft = messageParsed.desktopWindow;
+                that.pointingAtTabletLeft = messageParsed.tablet;
+            }
+        }
+    }
+
+    subscribeToUpdateMessages();
+
+    // disabling this for now as it is causing rendering issues with the other handle overlays
+    /*
+    var COLOR_ORANGE_HIGHLIGHT = { red: 255, green: 99, blue: 9 };
+    var editHandleOutlineStyle = {
+        outlineUnoccludedColor: COLOR_ORANGE_HIGHLIGHT,
+        outlineOccludedColor: COLOR_ORANGE_HIGHLIGHT,
+        fillUnoccludedColor: COLOR_ORANGE_HIGHLIGHT,
+        fillOccludedColor: COLOR_ORANGE_HIGHLIGHT,
+        outlineUnoccludedAlpha: 1,
+        outlineOccludedAlpha: 0,
+        fillUnoccludedAlpha: 0,
+        fillOccludedAlpha: 0,
+        outlineWidth: 3,
+        isOutlineSmooth: true
+    };
+    Selection.enableListHighlight(HIGHLIGHT_LIST_NAME, editHandleOutlineStyle);
+    */
+
+    that.savedProperties = {};
+    that.selections = [];
+    var listeners = [];
+
+    that.localRotation = Quat.IDENTITY;
+    that.localPosition = Vec3.ZERO;
+    that.localDimensions = Vec3.ZERO;
+    that.localRegistrationPoint = Vec3.HALF;
+
+    that.worldRotation = Quat.IDENTITY;
+    that.worldPosition = Vec3.ZERO;
+    that.worldDimensions = Vec3.ZERO;
+    that.worldRegistrationPoint = Vec3.HALF;
+    that.centerPosition = Vec3.ZERO;
+    
+    that.pointingAtDesktopWindowLeft = false;
+    that.pointingAtDesktopWindowRight = false;
+    that.pointingAtTabletLeft = false;
+    that.pointingAtTabletRight = false;
+
+    that.saveProperties = function() {
+        that.savedProperties = {};
+        for (var i = 0; i < that.selections.length; i++) {
+            var entityID = that.selections[i];
+            that.savedProperties[entityID] = Entities.getEntityProperties(entityID);
+        }
+    };
+
+    that.addEventListener = function(func, thisContext) {
+        listeners.push({
+            callback: func,
+            thisContext: thisContext
+        });
+    };
+
+    that.hasSelection = function() {
+        return that.selections.length > 0;
+    };
+
+    that.setSelections = function(entityIDs, caller) {
+        that.selections = [];
+        for (var i = 0; i < entityIDs.length; i++) {
+            var entityID = entityIDs[i];
+            that.selections.push(entityID);
+            Selection.addToSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID);
+        }
+
+        that._update(true, caller);
+    };
+
+    that.addEntity = function(entityID, toggleSelection, caller) {
+        if (entityID) {
+            var idx = -1;
+            for (var i = 0; i < that.selections.length; i++) {
+                if (entityID === that.selections[i]) {
+                    idx = i;
+                    break;
+                }
+            }
+            if (idx === -1) {
+                that.selections.push(entityID);
+                Selection.addToSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID);
+            } else if (toggleSelection) {
+                that.selections.splice(idx, 1);
+                Selection.removeFromSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID);
+            }
+        }
+
+        that._update(true, caller);
+    };
+
+    function removeEntityByID(entityID) {
+        var idx = that.selections.indexOf(entityID);
+        if (idx >= 0) {
+            that.selections.splice(idx, 1);
+            Selection.removeFromSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID);
+        }
+    }
+
+    that.removeEntity = function (entityID, caller) {
+        removeEntityByID(entityID);
+        that._update(true, caller);
+    };
+
+    that.removeEntities = function(entityIDs, caller) {
+        for (var i = 0, length = entityIDs.length; i < length; i++) {
+            removeEntityByID(entityIDs[i]);
+        }
+        that._update(true, caller);
+    };
+
+    that.clearSelections = function(caller) {
+        that.selections = [];
+        that._update(true, caller);
+    };
+    
+    that.addChildrenEntities = function(parentEntityID, entityList, entityHostType) {
+        var wantDebug = false;
+        var children = Entities.getChildrenIDs(parentEntityID);
+        var entityHostTypes = Entities.getMultipleEntityProperties(children, 'entityHostType');
+        for (var i = 0; i < children.length; i++) {
+            var childID = children[i];
+
+            if (entityHostTypes[i].entityHostType !== entityHostType) {
+                if (wantDebug) {
+                    console.log("Skipping addition of entity " + childID + " with conflicting entityHostType: " +
+                        entityHostTypes[i].entityHostType + ", expected: " + entityHostType);
+                }
+                continue;
+            }
+
+            if (entityList.indexOf(childID) < 0) {
+                entityList.push(childID);
+            }
+            that.addChildrenEntities(childID, entityList, entityHostType);
+        }
+    };
+
+    // Determine if an entity is being grabbed.
+    // This is mostly a heuristic - there is no perfect way to know if an entity is being
+    // grabbed.
+    //
+    // @return {boolean} true if the given entity with `properties` is being grabbed by an avatar
+    function nonDynamicEntityIsBeingGrabbedByAvatar(properties) {
+        if (properties.dynamic || Uuid.isNull(properties.parentID)) {
+            return false;
+        }
+
+        var avatar = AvatarList.getAvatar(properties.parentID);
+        if (Uuid.isNull(avatar.sessionUUID)) {
+            return false;
+        }
+
+        var grabJointNames = [
+            'RightHand', 'LeftHand',
+            '_CONTROLLER_RIGHTHAND', '_CONTROLLER_LEFTHAND',
+            '_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND', '_CAMERA_RELATIVE_CONTROLLER_LEFTHAND',
+            '_FARGRAB_RIGHTHAND', '_FARGRAB_LEFTHAND', '_FARGRAB_MOUSE'
+        ];
+
+        for (var i = 0; i < grabJointNames.length; ++i) {
+            if (avatar.getJointIndex(grabJointNames[i]) === properties.parentJointIndex) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    var entityClipboard = {
+        entities: {}, // Map of id -> properties for copied entities
+        position: { x: 0, y: 0, z: 0 },
+        dimensions: { x: 0, y: 0, z: 0 },
+    };
+
+    that.duplicateSelection = function() {
+        var entitiesToDuplicate = [];
+        var duplicatedEntityIDs = [];
+        var duplicatedChildrenWithOldParents = [];
+        var originalEntityToNewEntityID = [];
+
+        SelectionManager.saveProperties();
+        
+        // build list of entities to duplicate by including any unselected children of selected parent entities
+        var originalEntityIDs = Object.keys(that.savedProperties);
+        var entityHostTypes = Entities.getMultipleEntityProperties(originalEntityIDs, 'entityHostType');
+        for (var i = 0; i < originalEntityIDs.length; i++) {
+            var originalEntityID = originalEntityIDs[i];
+            if (entitiesToDuplicate.indexOf(originalEntityID) === -1) {
+                entitiesToDuplicate.push(originalEntityID);
+            }
+            that.addChildrenEntities(originalEntityID, entitiesToDuplicate, entityHostTypes[i].entityHostType);
+        }
+        
+        // duplicate entities from above and store their original to new entity mappings and children needing re-parenting
+        for (var i = 0; i < entitiesToDuplicate.length; i++) {
+            var originalEntityID = entitiesToDuplicate[i];
+            var properties = that.savedProperties[originalEntityID];
+            if (properties === undefined) {
+                properties = Entities.getEntityProperties(originalEntityID);
+            }
+            if (!properties.locked && (!properties.avatarEntity || properties.owningAvatarID === MyAvatar.sessionUUID)) {
+                if (nonDynamicEntityIsBeingGrabbedByAvatar(properties)) {
+                    properties.parentID = null;
+                    properties.parentJointIndex = null;
+                    properties.localPosition = properties.position;
+                    properties.localRotation = properties.rotation;
+                }
+
+                properties.localVelocity = Vec3.ZERO;
+                properties.localAngularVelocity = Vec3.ZERO;
+
+                delete properties.actionData;
+                var newEntityID = Entities.addEntity(properties);
+
+                // Re-apply actions from the original entity
+                var actionIDs = Entities.getActionIDs(properties.id);
+                for (var j = 0; j < actionIDs.length; ++j) {
+                    var actionID = actionIDs[j];
+                    var actionArguments = Entities.getActionArguments(properties.id, actionID);
+                    if (actionArguments) {
+                        var type = actionArguments.type;
+                        if (type === 'hold' || type === 'far-grab') {
+                            continue;
+                        }
+                        delete actionArguments.ttl;
+                        Entities.addAction(type, newEntityID, actionArguments);
+                    }
+                }
+
+                duplicatedEntityIDs.push({
+                    entityID: newEntityID,
+                    properties: properties
+                });
+                if (properties.parentID !== Uuid.NULL) {
+                    duplicatedChildrenWithOldParents[newEntityID] = properties.parentID;
+                }
+                originalEntityToNewEntityID[originalEntityID] = newEntityID;
+            }
+        }
+        
+        // re-parent duplicated children to the duplicate entities of their original parents (if they were duplicated)
+        Object.keys(duplicatedChildrenWithOldParents).forEach(function(childIDNeedingNewParent) {
+            var originalParentID = duplicatedChildrenWithOldParents[childIDNeedingNewParent];
+            var newParentID = originalEntityToNewEntityID[originalParentID];
+            if (newParentID) {
+                Entities.editEntity(childIDNeedingNewParent, { parentID: newParentID });
+                for (var i = 0; i < duplicatedEntityIDs.length; i++) {
+                    var duplicatedEntity = duplicatedEntityIDs[i];
+                    if (duplicatedEntity.entityID === childIDNeedingNewParent) {
+                        duplicatedEntity.properties.parentID = newParentID;
+                    }
+                }
+            }
+        });
+        
+        return duplicatedEntityIDs;
+    };
+
+    // Create the entities in entityProperties, maintaining parent-child relationships.
+    // @param entityProperties {array} - Array of entity property objects
+    that.createEntities = function(entityProperties) {
+        var entitiesToCreate = [];
+        var createdEntityIDs = [];
+        var createdChildrenWithOldParents = [];
+        var originalEntityToNewEntityID = [];
+
+        that.saveProperties();
+
+        for (var i = 0; i < entityProperties.length; ++i) {
+            var properties = entityProperties[i];
+            if (properties.parentID in originalEntityToNewEntityID) {
+                properties.parentID = originalEntityToNewEntityID[properties.parentID];
+            } else {
+                delete properties.parentID;
+            }
+
+            delete properties.actionData;
+            var newEntityID = Entities.addEntity(properties);
+
+            if (newEntityID) {
+                createdEntityIDs.push({
+                    entityID: newEntityID,
+                    properties: properties
+                });
+                if (properties.parentID !== Uuid.NULL) {
+                    createdChildrenWithOldParents[newEntityID] = properties.parentID;
+                }
+                originalEntityToNewEntityID[properties.id] = newEntityID;
+                properties.id = newEntityID;
+            }
+        }
+
+        return createdEntityIDs;
+    };
+
+    that.cutSelectedEntities = function() {
+        that.copySelectedEntities();
+        deleteSelectedEntities();
+    };
+
+    that.copySelectedEntities = function() {
+        var entityProperties = Entities.getMultipleEntityProperties(that.selections);
+        var entityHostTypes = Entities.getMultipleEntityProperties(that.selections, 'entityHostType');
+        var entities = {};
+        entityProperties.forEach(function(props) {
+            entities[props.id] = props;
+        });
+
+        function appendChildren(entityID, entities, entityHostType) {
+            var wantDebug = false;
+            var childrenIDs = Entities.getChildrenIDs(entityID);
+            var entityHostTypes = Entities.getMultipleEntityProperties(childrenIDs, 'entityHostType');
+            for (var i = 0; i < childrenIDs.length; ++i) {
+                var id = childrenIDs[i];
+
+                if (entityHostTypes[i].entityHostType !== entityHostType) {
+                    if (wantDebug) {
+                        console.warn("Skipping addition of entity " + id + " with conflicting entityHostType: " +
+                            entityHostTypes[i].entityHostType + ", expected: " + entityHostType);
+                    }
+                    continue;
+                }
+
+                if (!(id in entities)) {
+                    entities[id] = Entities.getEntityProperties(id); 
+                    appendChildren(id, entities, entityHostType);
+                }
+            }
+        }
+
+        var len = entityProperties.length;
+        for (var i = 0; i < len; ++i) {
+            appendChildren(entityProperties[i].id, entities, entityHostTypes[i].entityHostType);
+        }
+
+        for (var id in entities) {
+            var parentID = entities[id].parentID;
+            entities[id].root = !(parentID in entities);
+        }
+
+        entityClipboard.entities = [];
+
+        var ids = Object.keys(entities);
+        while (ids.length > 0) {
+            // Go through all remaining entities.
+            // If an entity does not have a parent left, move it into the list
+            for (var i = 0; i < ids.length; ++i) {
+                var id = ids[i];
+                var parentID = entities[id].parentID;
+                if (parentID in entities) {
+                    continue;
+                }
+                entityClipboard.entities.push(entities[id]);
+                delete entities[id];
+            }
+            ids = Object.keys(entities);
+        }
+
+        // Calculate size
+        if (entityClipboard.entities.length === 0) {
+            entityClipboard.dimensions = { x: 0, y: 0, z: 0 };
+            entityClipboard.position = { x: 0, y: 0, z: 0 };
+        } else {
+            var properties = entityClipboard.entities;
+            var brn = properties[0].boundingBox.brn;
+            var tfl = properties[0].boundingBox.tfl;
+            for (var i = 1; i < properties.length; i++) {
+                var bb = properties[i].boundingBox;
+                brn.x = Math.min(bb.brn.x, brn.x);
+                brn.y = Math.min(bb.brn.y, brn.y);
+                brn.z = Math.min(bb.brn.z, brn.z);
+                tfl.x = Math.max(bb.tfl.x, tfl.x);
+                tfl.y = Math.max(bb.tfl.y, tfl.y);
+                tfl.z = Math.max(bb.tfl.z, tfl.z);
+            }
+            entityClipboard.dimensions = {
+                x: tfl.x - brn.x,
+                y: tfl.y - brn.y,
+                z: tfl.z - brn.z
+            };
+            entityClipboard.position = {
+                x: brn.x + entityClipboard.dimensions.x / 2,
+                y: brn.y + entityClipboard.dimensions.y / 2,
+                z: brn.z + entityClipboard.dimensions.z / 2
+            };
+        }
+    };
+
+    that.pasteEntities = function() {
+        var dimensions = entityClipboard.dimensions;
+        var maxDimension = Math.max(dimensions.x, dimensions.y, dimensions.z);
+        var pastePosition = getPositionToCreateEntity(maxDimension);
+        var deltaPosition = Vec3.subtract(pastePosition, entityClipboard.position);
+
+        var copiedProperties = [];
+        var ids = [];
+        entityClipboard.entities.forEach(function(originalProperties) {
+            var properties = deepCopy(originalProperties);
+            if (properties.root) {
+                properties.position = Vec3.sum(properties.position, deltaPosition);
+                delete properties.localPosition;
+            } else {
+                delete properties.position;
+            }
+            copiedProperties.push(properties);
+        });
+
+        var currentSelections = deepCopy(SelectionManager.selections);
+
+        function redo(copiedProperties) {
+            var created = that.createEntities(copiedProperties);
+            var ids = [];
+            for (var i = 0; i < created.length; ++i) {
+                ids.push(created[i].entityID);
+            }
+            SelectionManager.setSelections(ids);
+        }
+
+        function undo(copiedProperties) {
+            for (var i = 0; i < copiedProperties.length; ++i) {
+                Entities.deleteEntity(copiedProperties[i].id);
+            }
+            SelectionManager.setSelections(currentSelections);
+        }
+
+        redo(copiedProperties);
+        undoHistory.pushCommand(undo, copiedProperties, redo, copiedProperties);
+    };
+
+    that._update = function(selectionUpdated, caller) {
+        var properties = null;
+        if (that.selections.length === 0) {
+            that.localDimensions = null;
+            that.localPosition = null;
+            that.worldDimensions = null;
+            that.worldPosition = null;
+            that.worldRotation = null;
+        } else if (that.selections.length === 1) {
+            properties = Entities.getEntityProperties(that.selections[0],
+                ['dimensions', 'position', 'rotation', 'registrationPoint', 'boundingBox', 'type']);
+            that.localDimensions = properties.dimensions;
+            that.localPosition = properties.position;
+            that.localRotation = properties.rotation;
+            that.localRegistrationPoint = properties.registrationPoint;
+
+            that.worldDimensions = properties.boundingBox.dimensions;
+            that.worldPosition = properties.boundingBox.center;
+            that.worldRotation = Quat.IDENTITY;
+
+            that.entityType = properties.type;
+            
+            if (selectionUpdated) {
+                SelectionDisplay.useDesiredSpaceMode();
+            }
+        } else {
+            properties = Entities.getEntityProperties(that.selections[0], ['type', 'boundingBox']);
+
+            that.entityType = properties.type;
+
+            var brn = properties.boundingBox.brn;
+            var tfl = properties.boundingBox.tfl;
+
+            for (var i = 1; i < that.selections.length; i++) {
+                properties = Entities.getEntityProperties(that.selections[i], 'boundingBox');
+                var bb = properties.boundingBox;
+                brn.x = Math.min(bb.brn.x, brn.x);
+                brn.y = Math.min(bb.brn.y, brn.y);
+                brn.z = Math.min(bb.brn.z, brn.z);
+                tfl.x = Math.max(bb.tfl.x, tfl.x);
+                tfl.y = Math.max(bb.tfl.y, tfl.y);
+                tfl.z = Math.max(bb.tfl.z, tfl.z);
+            }
+
+            that.localRotation = null;
+            that.localDimensions = null;
+            that.localPosition = null;
+            that.worldDimensions = {
+                x: tfl.x - brn.x,
+                y: tfl.y - brn.y,
+                z: tfl.z - brn.z
+            };
+            that.worldRotation = Quat.IDENTITY;
+            that.worldPosition = {
+                x: brn.x + (that.worldDimensions.x / 2),
+                y: brn.y + (that.worldDimensions.y / 2),
+                z: brn.z + (that.worldDimensions.z / 2)
+            };
+
+            // For 1+ selections we can only modify selections in world space
+            SelectionDisplay.setSpaceMode(SPACE_WORLD, false);
+        }
+
+        for (var j = 0; j < listeners.length; j++) {
+            try {
+                listeners[j].callback.call(listeners[j].thisContext, selectionUpdated === true, caller);
+            } catch (e) {
+                print("ERROR: entitySelectionTool.update got exception: " + JSON.stringify(e));
+            }
+        }
+    };
+
+    return that;
+})();
+
+// Normalize degrees to be in the range (-180, 180)
+function normalizeDegrees(degrees) {
+    var maxDegrees = 360;
+    var halfMaxDegrees = maxDegrees / 2;
+    degrees = ((degrees + halfMaxDegrees) % maxDegrees) - halfMaxDegrees;
+    if (degrees <= -halfMaxDegrees) {
+        degrees += maxDegrees;
+    }
+    return degrees;
+}
+
+// SELECTION DISPLAY DEFINITION
+SelectionDisplay = (function() {
+    var that = {};
+
+    const COLOR_GREEN = { red: 31, green: 198, blue: 166 };
+    const COLOR_BLUE = { red: 0, green: 147, blue: 197 };
+    const COLOR_RED = { red: 226, green: 51, blue: 77 };
+    const COLOR_HOVER = { red: 227, green: 227, blue: 227 };
+    const COLOR_ROTATE_CURRENT_RING = { red: 255, green: 99, blue: 9 };
+    const COLOR_BOUNDING_EDGE = { red: 87, green: 87, blue: 87 };
+    const COLOR_SCALE_CUBE = { red: 106, green: 106, blue: 106 };
+    const COLOR_SCALE_CUBE_SELECTED = { red: 18, green: 18, blue: 18 };
+    const COLOR_DEBUG_PICK_PLANE = { red: 255, green: 255, blue: 255 };
+    const COLOR_DEBUG_PICK_PLANE_HIT = { red: 255, green: 165, blue: 0 };
+
+    const TRANSLATE_ARROW_CYLINDER_OFFSET = 0.1;
+    const TRANSLATE_ARROW_CYLINDER_CAMERA_DISTANCE_MULTIPLE = 0.005;
+    const TRANSLATE_ARROW_CYLINDER_Y_MULTIPLE = 7.5;
+    const TRANSLATE_ARROW_CONE_CAMERA_DISTANCE_MULTIPLE = 0.025;
+    const TRANSLATE_ARROW_CONE_OFFSET_CYLINDER_DIMENSION_MULTIPLE = 0.83;
+
+    const ROTATE_RING_CAMERA_DISTANCE_MULTIPLE = 0.15;
+    const ROTATE_CTRL_SNAP_ANGLE = 22.5;
+    const ROTATE_DEFAULT_SNAP_ANGLE = 1;
+    const ROTATE_DEFAULT_TICK_MARKS_ANGLE = 5;
+    const ROTATE_RING_IDLE_INNER_RADIUS = 0.92;
+    const ROTATE_RING_SELECTED_INNER_RADIUS = 0.9;
+
+    // These are multipliers for sizing the rotation degrees display while rotating an entity
+    const ROTATE_DISPLAY_DISTANCE_MULTIPLIER = 2;
+    const ROTATE_DISPLAY_SIZE_X_MULTIPLIER = 0.2;
+    const ROTATE_DISPLAY_SIZE_Y_MULTIPLIER = 0.09;
+    const ROTATE_DISPLAY_LINE_HEIGHT_MULTIPLIER = 0.07;
+
+    const STRETCH_CUBE_OFFSET = 0.06;
+    const STRETCH_CUBE_CAMERA_DISTANCE_MULTIPLE = 0.02;
+    const STRETCH_PANEL_WIDTH = 0.01;
+
+    const SCALE_OVERLAY_CAMERA_DISTANCE_MULTIPLE = 0.02;
+    const SCALE_DIMENSIONS_CAMERA_DISTANCE_MULTIPLE = 0.5;
+    
+    const BOUNDING_EDGE_OFFSET = 0.5;
+
+    const DUPLICATOR_OFFSET = { x: 0.9, y: -0.9, z: 0.9 };    
+    
+    const CTRL_KEY_CODE = 16777249;
+
+    const RAIL_AXIS_LENGTH = 10000;
+    
+    const NEGATE_VECTOR = -1;
+    const NO_HAND = -1;
+    
+    const DEBUG_PICK_PLANE_HIT_LIMIT = 200;
+    const DEBUG_PICK_PLANE_HIT_CAMERA_DISTANCE_MULTIPLE = 0.01;
+
+    const TRANSLATE_DIRECTION = {
+        X: 0,
+        Y: 1,
+        Z: 2
+    };
+
+    const STRETCH_DIRECTION = {
+        X: 0,
+        Y: 1,
+        Z: 2,
+        ALL: 3
+    };
+
+    const ROTATE_DIRECTION = {
+        PITCH: 0,
+        YAW: 1,
+        ROLL: 2
+    };
+
+    const INEDIT_STATUS_CHANNEL = "Hifi-InEdit-Status";
+
+    /**
+     * The current space mode, this could have been a forced space mode since we do not support multi selection while in
+     * local space mode.
+     * @type {string} - should only be set to SPACE_LOCAL or SPACE_WORLD
+     */
+    var spaceMode = SPACE_LOCAL;
+
+    /**
+     * The desired space mode, this is the user set space mode, which should be respected whenever it is possible. In the case
+     * of multi entity selection this space mode may differ from the actual spaceMode.
+     * @type {string} - should only be set to SPACE_LOCAL or SPACE_WORLD
+     */
+    var desiredSpaceMode = SPACE_LOCAL;
+
+    var overlayNames = [];
+    var lastControllerPoses = [
+        getControllerWorldLocation(Controller.Standard.LeftHand, true),
+        getControllerWorldLocation(Controller.Standard.RightHand, true)
+    ];
+
+    var worldRotationX;
+    var worldRotationY;
+    var worldRotationZ;
+    
+    var activeStretchCubePanelOffset = null;
+
+    var previousHandle = null;
+    var previousHandleHelper = null;
+    var previousHandleColor;
+
+    var ctrlPressed = false;
+
+    that.replaceCollisionsAfterStretch = false;
+
+    var handlePropertiesTranslateArrowCones = {
+        alpha: 1,
+        shape: "Cone",
+        solid: true,
+        visible: false,
+        ignorePickIntersection: true,
+        drawInFront: true
+    };
+    var handlePropertiesTranslateArrowCylinders = {
+        alpha: 1,
+        shape: "Cylinder",
+        solid: true,
+        visible: false,
+        ignorePickIntersection: true,
+        drawInFront: true
+    };
+    var handleTranslateXCone = Overlays.addOverlay("shape", handlePropertiesTranslateArrowCones);
+    var handleTranslateXCylinder = Overlays.addOverlay("shape", handlePropertiesTranslateArrowCylinders);
+    Overlays.editOverlay(handleTranslateXCone, { color: COLOR_RED });
+    Overlays.editOverlay(handleTranslateXCylinder, { color: COLOR_RED });
+    var handleTranslateYCone = Overlays.addOverlay("shape", handlePropertiesTranslateArrowCones);
+    var handleTranslateYCylinder = Overlays.addOverlay("shape", handlePropertiesTranslateArrowCylinders);
+    Overlays.editOverlay(handleTranslateYCone, { color: COLOR_GREEN });
+    Overlays.editOverlay(handleTranslateYCylinder, { color: COLOR_GREEN });
+    var handleTranslateZCone = Overlays.addOverlay("shape", handlePropertiesTranslateArrowCones);
+    var handleTranslateZCylinder = Overlays.addOverlay("shape", handlePropertiesTranslateArrowCylinders);
+    Overlays.editOverlay(handleTranslateZCone, { color: COLOR_BLUE });
+    Overlays.editOverlay(handleTranslateZCylinder, { color: COLOR_BLUE });
+
+    var handlePropertiesRotateRings = {
+        alpha: 1,
+        solid: true,
+        startAt: 0,
+        endAt: 360,
+        innerRadius: ROTATE_RING_IDLE_INNER_RADIUS,
+        majorTickMarksAngle: ROTATE_DEFAULT_TICK_MARKS_ANGLE,
+        majorTickMarksLength: 0.1,
+        visible: false,
+        ignorePickIntersection: true,
+        drawInFront: true
+    };
+    var handleRotatePitchRing = Overlays.addOverlay("circle3d", handlePropertiesRotateRings);
+    Overlays.editOverlay(handleRotatePitchRing, { 
+        color: COLOR_RED,
+        majorTickMarksColor: COLOR_RED
+    });
+    var handleRotateYawRing = Overlays.addOverlay("circle3d", handlePropertiesRotateRings);
+    Overlays.editOverlay(handleRotateYawRing, { 
+        color: COLOR_GREEN,
+        majorTickMarksColor: COLOR_GREEN
+    });
+    var handleRotateRollRing = Overlays.addOverlay("circle3d", handlePropertiesRotateRings);
+    Overlays.editOverlay(handleRotateRollRing, { 
+        color: COLOR_BLUE,
+        majorTickMarksColor: COLOR_BLUE
+    });
+
+    var handleRotateCurrentRing = Overlays.addOverlay("circle3d", {
+        alpha: 1,
+        color: COLOR_ROTATE_CURRENT_RING,
+        solid: true,
+        innerRadius: 0.9,
+        visible: false,
+        ignorePickIntersection: true,
+        drawInFront: true
+    });
+
+    var rotationDegreesDisplay = Overlays.addOverlay("text3d", {
+        text: "",
+        color: { red: 0, green: 0, blue: 0 },
+        backgroundColor: { red: 255, green: 255, blue: 255 },
+        alpha: 0.7,
+        backgroundAlpha: 0.7,
+        visible: false,
+        isFacingAvatar: true,
+        drawInFront: true,
+        ignorePickIntersection: true,
+        dimensions: { x: 0, y: 0 },
+        lineHeight: 0.0,
+        topMargin: 0,
+        rightMargin: 0,
+        bottomMargin: 0,
+        leftMargin: 0
+    });
+
+    var handlePropertiesStretchCubes = {
+        solid: true,
+        visible: false,
+        ignorePickIntersection: true,
+        drawInFront: true
+    };
+    var handleStretchXCube = Overlays.addOverlay("cube", handlePropertiesStretchCubes);
+    Overlays.editOverlay(handleStretchXCube, { color: COLOR_RED });
+    var handleStretchYCube = Overlays.addOverlay("cube", handlePropertiesStretchCubes);
+    Overlays.editOverlay(handleStretchYCube, { color: COLOR_GREEN });
+    var handleStretchZCube = Overlays.addOverlay("cube", handlePropertiesStretchCubes);
+    Overlays.editOverlay(handleStretchZCube, { color: COLOR_BLUE });
+
+    var handlePropertiesStretchPanel = {
+        alpha: 0.5,
+        solid: true,
+        visible: false,
+        ignorePickIntersection: true,
+        drawInFront: true
+    };
+    var handleStretchXPanel = Overlays.addOverlay("cube", handlePropertiesStretchPanel);
+    Overlays.editOverlay(handleStretchXPanel, { color: COLOR_RED });
+    var handleStretchYPanel = Overlays.addOverlay("cube", handlePropertiesStretchPanel);
+    Overlays.editOverlay(handleStretchYPanel, { color: COLOR_GREEN });
+    var handleStretchZPanel = Overlays.addOverlay("cube", handlePropertiesStretchPanel);
+    Overlays.editOverlay(handleStretchZPanel, { color: COLOR_BLUE });
+
+    var handleScaleCube = Overlays.addOverlay("cube", {
+        size: 0.025,
+        color: COLOR_SCALE_CUBE,
+        solid: true,
+        visible: false,
+        ignorePickIntersection: true,
+        drawInFront: true,
+        borderSize: 1.4
+    });
+
+    var handleBoundingBox = Overlays.addOverlay("cube", {
+        alpha: 1,
+        color: COLOR_BOUNDING_EDGE,
+        visible: false,
+        ignorePickIntersection: true,
+        drawInFront: true,
+        isSolid: false
+    });
+
+    var handleDuplicator = Overlays.addOverlay("cube", {
+        alpha: 1,
+        size: 0.05,
+        color: COLOR_GREEN,
+        solid: true,
+        visible: false,
+        ignorePickIntersection: true,
+        drawInFront: true,
+        borderSize: 1.4
+    });
+
+    // setting to 0 alpha for now to keep this hidden vs using visible false 
+    // because its used as the translate xz tool handle overlay
+    var selectionBox = Overlays.addOverlay("cube", {
+        size: 1,
+        color: COLOR_RED,
+        alpha: 0,
+        solid: false,
+        visible: false,
+        ignorePickIntersection: true,
+        dashed: false
+    });
+
+    // Handle for x-z translation of particle effect and light entities while inside the bounding box.
+    // Limitation: If multiple entities are selected, only the first entity's icon translates the selection.
+    var iconSelectionBox = Overlays.addOverlay("cube", {
+        size: 0.3, // Match entity icon size.
+        color: COLOR_RED,
+        alpha: 0,
+        solid: false,
+        visible: false,
+        ignorePickIntersection: true,
+        dashed: false
+    });
+
+    var xRailOverlay = Overlays.addOverlay("line3d", {
+        visible: false,
+        start: Vec3.ZERO,
+        end: Vec3.ZERO,
+        color: {
+            red: 255,
+            green: 0,
+            blue: 0
+        },
+        ignorePickIntersection: true // always ignore this
+    });
+    var yRailOverlay = Overlays.addOverlay("line3d", {
+        visible: false,
+        start: Vec3.ZERO,
+        end: Vec3.ZERO,
+        color: {
+            red: 0,
+            green: 255,
+            blue: 0
+        },
+        ignorePickIntersection: true // always ignore this
+    });
+    var zRailOverlay = Overlays.addOverlay("line3d", {
+        visible: false,
+        start: Vec3.ZERO,
+        end: Vec3.ZERO,
+        color: {
+            red: 0,
+            green: 0,
+            blue: 255
+        },
+        ignorePickIntersection: true // always ignore this
+    });
+
+    var allOverlays = [
+        handleTranslateXCone,
+        handleTranslateXCylinder,
+        handleTranslateYCone,
+        handleTranslateYCylinder,
+        handleTranslateZCone,
+        handleTranslateZCylinder,
+        handleRotatePitchRing,
+        handleRotateYawRing,
+        handleRotateRollRing,
+        handleRotateCurrentRing,
+        rotationDegreesDisplay,
+        handleStretchXCube,
+        handleStretchYCube,
+        handleStretchZCube,
+        handleStretchXPanel,
+        handleStretchYPanel,
+        handleStretchZPanel,
+        handleScaleCube,
+        handleBoundingBox,
+        handleDuplicator,
+        selectionBox,
+        iconSelectionBox,
+        xRailOverlay,
+        yRailOverlay,
+        zRailOverlay
+    ];
+
+    const nonLayeredOverlays = [selectionBox, iconSelectionBox];
+
+    var maximumHandleInAllOverlays = handleDuplicator;
+
+    overlayNames[handleTranslateXCone] = "handleTranslateXCone";
+    overlayNames[handleTranslateXCylinder] = "handleTranslateXCylinder";
+    overlayNames[handleTranslateYCone] = "handleTranslateYCone";
+    overlayNames[handleTranslateYCylinder] = "handleTranslateYCylinder";
+    overlayNames[handleTranslateZCone] = "handleTranslateZCone";
+    overlayNames[handleTranslateZCylinder] = "handleTranslateZCylinder";
+
+    overlayNames[handleRotatePitchRing] = "handleRotatePitchRing";
+    overlayNames[handleRotateYawRing] = "handleRotateYawRing";
+    overlayNames[handleRotateRollRing] = "handleRotateRollRing";
+    overlayNames[handleRotateCurrentRing] = "handleRotateCurrentRing";
+    overlayNames[rotationDegreesDisplay] = "rotationDegreesDisplay";
+
+    overlayNames[handleStretchXCube] = "handleStretchXCube";
+    overlayNames[handleStretchYCube] = "handleStretchYCube";
+    overlayNames[handleStretchZCube] = "handleStretchZCube";
+    overlayNames[handleStretchXPanel] = "handleStretchXPanel";
+    overlayNames[handleStretchYPanel] = "handleStretchYPanel";
+    overlayNames[handleStretchZPanel] = "handleStretchZPanel";
+
+    overlayNames[handleScaleCube] = "handleScaleCube";
+
+    overlayNames[handleBoundingBox] = "handleBoundingBox";
+
+    overlayNames[handleDuplicator] = "handleDuplicator";
+    overlayNames[selectionBox] = "selectionBox";
+    overlayNames[iconSelectionBox] = "iconSelectionBox";
+
+    var activeTool = null;
+    var handleTools = {};
+    
+    var debugPickPlaneEnabled = false;
+    var debugPickPlane = Overlays.addOverlay("shape", {
+        shape: "Quad",
+        alpha: 0.25,
+        color: COLOR_DEBUG_PICK_PLANE,
+        solid: true,
+        visible: false,
+        ignorePickIntersection: true,
+        drawInFront: false
+    });
+    var debugPickPlaneHits = [];
+
+    // We get mouseMoveEvents from the handControllers, via handControllerPointer.
+    // But we dont' get mousePressEvents.
+    that.triggerClickMapping = Controller.newMapping(Script.resolvePath('') + '-click');
+    that.triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press');
+    that.triggeredHand = NO_HAND;
+    that.pressedHand = NO_HAND;
+    that.editingHand = NO_HAND;
+    that.triggered = function() {
+        return that.triggeredHand !== NO_HAND;
+    };
+    function pointingAtDesktopWindowOrTablet(hand) {
+        var pointingAtDesktopWindow = (hand === Controller.Standard.RightHand && 
+                                       SelectionManager.pointingAtDesktopWindowRight) ||
+                                      (hand === Controller.Standard.LeftHand && 
+                                       SelectionManager.pointingAtDesktopWindowLeft);
+        var pointingAtTablet = (hand === Controller.Standard.RightHand && SelectionManager.pointingAtTabletRight) ||
+                               (hand === Controller.Standard.LeftHand && SelectionManager.pointingAtTabletLeft);
+        return pointingAtDesktopWindow || pointingAtTablet;
+    }
+    function makeClickHandler(hand) {
+        return function (clicked) {
+            // Don't allow both hands to trigger at the same time
+            if (that.triggered() && hand !== that.triggeredHand) {
+                return;
+            }
+            if (!that.triggered() && clicked && !pointingAtDesktopWindowOrTablet(hand)) {
+                that.triggeredHand = hand;
+                that.mousePressEvent({});
+            } else if (that.triggered() && !clicked) {
+                that.triggeredHand = NO_HAND;
+                that.mouseReleaseEvent({});
+            }
+        };
+    }
+    function makePressHandler(hand) {
+        return function (value) {
+            if (value >= TRIGGER_ON_VALUE && !that.triggered() && !pointingAtDesktopWindowOrTablet(hand)) {
+                that.pressedHand = hand;
+                that.updateHighlight({});
+            } else {
+                that.pressedHand = NO_HAND;
+                that.resetPreviousHandleColor();
+            }
+        }
+    }
+    that.triggerClickMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand));
+    that.triggerClickMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand));
+    that.triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand));
+    that.triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand));
+    that.enableTriggerMapping = function() {
+        that.triggerClickMapping.enable();
+        that.triggerPressMapping.enable();
+    };
+    that.disableTriggerMapping = function() {
+        that.triggerClickMapping.disable();
+        that.triggerPressMapping.disable();
+    };
+    Script.scriptEnding.connect(that.disableTriggerMapping);
+
+    // FUNCTION DEF(s): Intersection Check Helpers
+    function testRayIntersect(queryRay, overlayIncludes, overlayExcludes) {
+        var wantDebug = false;
+        if ((queryRay === undefined) || (queryRay === null)) {
+            if (wantDebug) {
+                print("testRayIntersect - EARLY EXIT -> queryRay is undefined OR null!");
+            }
+            return null;
+        }
+
+        // We want to first check the drawInFront overlays (i.e. the handles, but really everything except the selectionBoxes)
+        // so that you can click on them even when they're behind things
+        var overlayIncludesLayered = [];
+        var overlayIncludesNonLayered = [];
+        for (var i = 0; i < overlayIncludes.length; i++) {
+            var value = overlayIncludes[i];
+            var contains = false;
+            for (var j = 0; j < nonLayeredOverlays.length; j++) {
+                if (nonLayeredOverlays[j] === value) {
+                    contains = true;
+                    break;
+                }
+            }
+            if (contains) {
+                overlayIncludesNonLayered.push(value);
+            } else {
+                overlayIncludesLayered.push(value);
+            }
+        }
+
+        var intersectObj = Overlays.findRayIntersection(queryRay, true, overlayIncludesLayered, overlayExcludes);
+
+        if (!intersectObj.intersects && overlayIncludesNonLayered.length > 0) {
+            intersectObj = Overlays.findRayIntersection(queryRay, true, overlayIncludesNonLayered, overlayExcludes);
+        }
+
+        if (wantDebug) {
+            if (!overlayIncludes) {
+                print("testRayIntersect - no overlayIncludes provided.");
+            }
+            if (!overlayExcludes) {
+                print("testRayIntersect - no overlayExcludes provided.");
+            }
+            print("testRayIntersect - Hit: " + intersectObj.intersects);
+            print("    intersectObj.overlayID:" + intersectObj.overlayID + "[" + overlayNames[intersectObj.overlayID] + "]");
+            print("        OverlayName: " + overlayNames[intersectObj.overlayID]);
+            print("    intersectObj.distance:" + intersectObj.distance);
+            print("    intersectObj.face:" + intersectObj.face);
+            Vec3.print("    intersectObj.intersection:", intersectObj.intersection);
+        }
+
+        return intersectObj;
+    }
+
+    function isPointInsideBox(point, box) {
+        var position = Vec3.subtract(point, box.position);
+        position = Vec3.multiplyQbyV(Quat.inverse(box.rotation), position);
+        return Math.abs(position.x) <= box.dimensions.x / 2 && Math.abs(position.y) <= box.dimensions.y / 2
+            && Math.abs(position.z) <= box.dimensions.z / 2;
+    }
+    
+    that.isEditHandle = function(overlayID) {
+        var overlayIndex = allOverlays.indexOf(overlayID);
+        var maxHandleIndex = allOverlays.indexOf(maximumHandleInAllOverlays);
+        return overlayIndex >= 0 && overlayIndex <= maxHandleIndex;
+    };
+
+    // FUNCTION: MOUSE PRESS EVENT
+    that.mousePressEvent = function (event) {
+        var wantDebug = false;
+        if (wantDebug) {
+            print("=============== eST::MousePressEvent BEG =======================");
+        }
+        if (!event.isLeftButton && !that.triggered()) {
+            // EARLY EXIT-(if another mouse button than left is pressed ignore it)
+            return false;
+        }
+
+        var pickRay = generalComputePickRay(event.x, event.y);
+        // TODO_Case6491:  Move this out to setup just to make it once
+        var interactiveOverlays = getMainTabletIDs();
+        for (var key in handleTools) {
+            if (handleTools.hasOwnProperty(key)) {
+                interactiveOverlays.push(key);
+            }
+        }
+
+        // Start with unknown mode, in case no tool can handle this.
+        activeTool = null;
+
+        var results = testRayIntersect(pickRay, interactiveOverlays);
+        if (results.intersects) {
+            var hitOverlayID = results.overlayID;
+            if ((HMD.tabletID && hitOverlayID === HMD.tabletID) || (HMD.tabletScreenID && hitOverlayID === HMD.tabletScreenID)
+                || (HMD.homeButtonID && hitOverlayID === HMD.homeButtonID)) {
+                // EARLY EXIT-(mouse clicks on the tablet should override the edit affordances)
+                return false;
+            }
+
+            var hitTool = handleTools[ hitOverlayID ];
+            if (hitTool) {
+                activeTool = hitTool;
+                that.clearDebugPickPlane();
+                if (activeTool.onBegin) {
+                    that.editingHand = that.triggeredHand;
+                    Messages.sendLocalMessage(INEDIT_STATUS_CHANNEL, JSON.stringify({
+                        method: "editing",
+                        hand: that.editingHand === Controller.Standard.LeftHand ? LEFT_HAND : RIGHT_HAND,
+                        editing: true
+                    }));
+                    activeTool.onBegin(event, pickRay, results);
+                } else {
+                    print("ERROR: entitySelectionTool.mousePressEvent - ActiveTool(" + activeTool.mode + ") missing onBegin");
+                }
+            } else {
+                print("ERROR: entitySelectionTool.mousePressEvent - Hit unexpected object, check interactiveOverlays");
+            }// End_if (hitTool)
+        }// End_If(results.intersects)
+
+        if (wantDebug) {
+            print("    DisplayMode: " + getMode());
+            print("=============== eST::MousePressEvent END =======================");
+        }
+
+        // If mode is known then we successfully handled this;
+        // otherwise, we're missing a tool.
+        return activeTool;
+    };
+
+    that.resetPreviousHandleColor = function() {
+        if (previousHandle !== null) {
+            Overlays.editOverlay(previousHandle, { color: previousHandleColor });
+            previousHandle = null;
+        }
+        if (previousHandleHelper !== null) {
+            Overlays.editOverlay(previousHandleHelper, { color: previousHandleColor });
+            previousHandleHelper = null;
+        }
+    };
+
+    that.getHandleHelper = function(overlay) {
+        if (overlay === handleTranslateXCone) {
+            return handleTranslateXCylinder;
+        } else if (overlay === handleTranslateXCylinder) {
+            return handleTranslateXCone;
+        } else if (overlay === handleTranslateYCone) {
+            return handleTranslateYCylinder;
+        } else if (overlay === handleTranslateYCylinder) {
+            return handleTranslateYCone;
+        } else if (overlay === handleTranslateZCone) {
+            return handleTranslateZCylinder;
+        } else if (overlay === handleTranslateZCylinder) {
+            return handleTranslateZCone;
+        }
+        return Uuid.NULL;
+    };
+    
+    that.updateHighlight = function(event) {
+        // if no tool is active, then just look for handles to highlight...
+        var pickRay = generalComputePickRay(event.x, event.y);        
+        var result = testRayIntersect(pickRay, allOverlays);
+        var pickedColor;
+        var highlightNeeded = false;
+
+        if (result.intersects) {
+            switch (result.overlayID) {
+                case handleTranslateXCone:
+                case handleTranslateXCylinder:
+                case handleRotatePitchRing:
+                case handleStretchXCube:
+                    pickedColor = COLOR_RED;
+                    highlightNeeded = true;
+                    break;
+                case handleTranslateYCone:
+                case handleTranslateYCylinder:
+                case handleRotateYawRing:
+                case handleStretchYCube:
+                    pickedColor = COLOR_GREEN;
+                    highlightNeeded = true;
+                    break;
+                case handleTranslateZCone:
+                case handleTranslateZCylinder:
+                case handleRotateRollRing:
+                case handleStretchZCube:
+                    pickedColor = COLOR_BLUE;
+                    highlightNeeded = true;
+                    break;
+                case handleScaleCube:
+                    pickedColor = COLOR_SCALE_CUBE;
+                    highlightNeeded = true;
+                    break;
+                default:
+                    that.resetPreviousHandleColor();
+                    break;
+            }
+
+            if (highlightNeeded) {
+                that.resetPreviousHandleColor();
+                Overlays.editOverlay(result.overlayID, { color: COLOR_HOVER });
+                previousHandle = result.overlayID;
+                previousHandleHelper = that.getHandleHelper(result.overlayID);
+                if (previousHandleHelper !== null) {
+                    Overlays.editOverlay(previousHandleHelper, { color: COLOR_HOVER });
+                }
+                previousHandleColor = pickedColor;
+            }
+
+        } else {
+            that.resetPreviousHandleColor();
+        }
+    };
+
+    // FUNCTION: MOUSE MOVE EVENT
+    var lastMouseEvent = null;
+    that.mouseMoveEvent = function(event) {
+        var wantDebug = false;
+        if (wantDebug) {
+            print("=============== eST::MouseMoveEvent BEG =======================");
+        }
+        lastMouseEvent = event;
+        if (activeTool) {
+            if (wantDebug) {
+                print("    Trigger ActiveTool(" + activeTool.mode + ")'s onMove");
+            }
+            activeTool.onMove(event);
+
+            if (wantDebug) {
+                print("    Trigger SelectionManager::update");
+            }
+            SelectionManager._update(false, that);
+
+            if (wantDebug) {
+                print("=============== eST::MouseMoveEvent END =======================");
+            }
+            // EARLY EXIT--(Move handled via active tool)
+            return true;
+        }
+
+        that.updateHighlight(event);
+        
+        if (wantDebug) {
+            print("=============== eST::MouseMoveEvent END =======================");
+        }
+        return false;
+    };
+
+    // FUNCTION: MOUSE RELEASE EVENT
+    that.mouseReleaseEvent = function(event) {
+        var wantDebug = false;
+        if (wantDebug) {
+            print("=============== eST::MouseReleaseEvent BEG =======================");
+        }
+        var showHandles = false;
+        if (activeTool) {
+            if (activeTool.onEnd) {
+                if (wantDebug) {
+                    print("    Triggering ActiveTool(" + activeTool.mode + ")'s onEnd");
+                }
+                Messages.sendLocalMessage(INEDIT_STATUS_CHANNEL, JSON.stringify({
+                    method: "editing",
+                    hand: that.editingHand === Controller.Standard.LeftHand ? LEFT_HAND : RIGHT_HAND,
+                    editing: false
+                }));
+                that.editingHand = NO_HAND;
+                activeTool.onEnd(event);
+            } else if (wantDebug) {
+                print("    ActiveTool(" + activeTool.mode + ")'s missing onEnd");
+            }
+        }
+
+        showHandles = activeTool; // base on prior tool value
+        activeTool = null;
+
+        // if something is selected, then reset the "original" properties for any potential next click+move operation
+        if (SelectionManager.hasSelection()) {
+            if (showHandles) {
+                if (wantDebug) {
+                    print("    Triggering that.select");
+                }
+                that.select(SelectionManager.selections[0], event);
+            }
+        }
+
+        if (wantDebug) {
+            print("=============== eST::MouseReleaseEvent END =======================");
+        }
+    };
+
+    // Control key remains active only while key is held down
+    that.keyReleaseEvent = function(event) {
+        if (event.key === CTRL_KEY_CODE) {
+            ctrlPressed = false;
+            that.updateActiveRotateRing();
+        }
+        that.updateLastMouseEvent(event);
+    };
+
+    // Triggers notification on specific key driven events
+    that.keyPressEvent = function(event) {
+        if (event.key === CTRL_KEY_CODE) {
+            ctrlPressed = true;
+            that.updateActiveRotateRing();
+        }
+        that.updateLastMouseEvent(event);
+    };
+    
+    that.updateLastMouseEvent = function(event) {
+        if (activeTool && lastMouseEvent !== null) { 
+            var change = lastMouseEvent.isShifted !== event.isShifted || lastMouseEvent.isMeta !== event.isMeta ||
+                         lastMouseEvent.isControl !== event.isControl || lastMouseEvent.isAlt !== event.isAlt;
+            lastMouseEvent.isShifted = event.isShifted; 
+            lastMouseEvent.isMeta = event.isMeta;   
+            lastMouseEvent.isControl = event.isControl; 
+            lastMouseEvent.isAlt = event.isAlt;
+            if (change) {
+                activeTool.onMove(lastMouseEvent);
+            }           
+        }
+    };
+
+    // NOTE: mousePressEvent and mouseMoveEvent from the main script should call us., so we don't hook these:
+    //       Controller.mousePressEvent.connect(that.mousePressEvent);
+    //       Controller.mouseMoveEvent.connect(that.mouseMoveEvent);
+    Controller.mouseReleaseEvent.connect(that.mouseReleaseEvent);
+    Controller.keyPressEvent.connect(that.keyPressEvent);
+    Controller.keyReleaseEvent.connect(that.keyReleaseEvent);
+
+    that.checkControllerMove = function() {
+        if (SelectionManager.hasSelection()) {
+            var controllerPose = getControllerWorldLocation(that.triggeredHand, true);
+            var hand = (that.triggeredHand === Controller.Standard.LeftHand) ? 0 : 1;
+            if (controllerPose.valid && lastControllerPoses[hand].valid && that.triggered()) {
+                if (!Vec3.equal(controllerPose.position, lastControllerPoses[hand].position) ||
+                    !Vec3.equal(controllerPose.rotation, lastControllerPoses[hand].rotation)) {
+                    that.mouseMoveEvent({});
+                }
+            }
+            lastControllerPoses[hand] = controllerPose;
+        }
+    };
+
+    function controllerComputePickRay() {
+        var hand = that.triggered() ? that.triggeredHand : that.pressedHand;
+        var controllerPose = getControllerWorldLocation(hand, true);
+        if (controllerPose.valid) {
+            var controllerPosition = controllerPose.translation;
+            // This gets point direction right, but if you want general quaternion it would be more complicated:
+            var controllerDirection = Quat.getUp(controllerPose.rotation);
+            return {origin: controllerPosition, direction: controllerDirection};
+        }
+    }
+
+    function generalComputePickRay(x, y) {
+        return controllerComputePickRay() || Camera.computePickRay(x, y);
+    }
+    
+    function getControllerAvatarFramePositionFromPickRay(pickRay) {
+        var controllerPosition = Vec3.subtract(pickRay.origin, MyAvatar.position);
+        controllerPosition = Vec3.multiplyQbyV(Quat.inverse(MyAvatar.orientation), controllerPosition);
+        return controllerPosition;
+    }
+
+    function getDistanceToCamera(position) {
+        var cameraPosition = Camera.getPosition();
+        var toCameraDistance = Vec3.length(Vec3.subtract(cameraPosition, position));
+        return toCameraDistance;
+    }
+    
+    function usePreviousPickRay(pickRayDirection, previousPickRayDirection, normal) {
+        return (Vec3.dot(pickRayDirection, normal) > 0 && Vec3.dot(previousPickRayDirection, normal) < 0) ||
+               (Vec3.dot(pickRayDirection, normal) < 0 && Vec3.dot(previousPickRayDirection, normal) > 0);
+    }
+
+    // @return string - The mode of the currently active tool;
+    //                  otherwise, "UNKNOWN" if there's no active tool.
+    function getMode() {
+        return (activeTool ? activeTool.mode : "UNKNOWN");
+    }
+
+    that.cleanup = function() {
+        for (var i = 0; i < allOverlays.length; i++) {
+            Overlays.deleteOverlay(allOverlays[i]);
+        }
+        that.clearDebugPickPlane();
+    };
+
+    that.select = function(entityID, event) {
+        var properties = Entities.getEntityProperties(SelectionManager.selections[0]);
+
+        if (event !== false) {
+            var wantDebug = false;
+            if (wantDebug) {
+                print("select() with EVENT...... ");
+                print("                event.y:" + event.y);
+                Vec3.print("       current position:", properties.position);
+            }
+        }
+
+        that.updateHandles();
+    };
+
+
+    /**
+     * This callback is used for spaceMode changes.
+     * @callback spaceModeChangedCallback
+     * @param {string} spaceMode
+     */
+
+    /**
+     * set this property with a callback to keep track of spaceMode changes.
+     * @type {spaceModeChangedCallback}
+     */
+    that.onSpaceModeChange = null;
+
+    // FUNCTION: SET SPACE MODE
+    that.setSpaceMode = function(newSpaceMode, isDesiredChange) {
+        var wantDebug = false;
+        if (wantDebug) {
+            print("======> SetSpaceMode called. ========");
+        }
+
+        if (spaceMode !== newSpaceMode) {
+            if (wantDebug) {
+                print("    Updating SpaceMode From: " + spaceMode + " To: " + newSpaceMode);
+            }
+            if (isDesiredChange) {
+                desiredSpaceMode = newSpaceMode;
+            }
+            spaceMode = newSpaceMode;
+
+            if (that.onSpaceModeChange !== null) {
+                that.onSpaceModeChange(newSpaceMode);
+            }
+
+            that.updateHandles();
+        } else if (wantDebug) {
+            print("WARNING: entitySelectionTool.setSpaceMode - Can't update SpaceMode. CurrentMode: " + 
+                  spaceMode + " DesiredMode: " + newSpaceMode);
+        }
+        if (wantDebug) {
+            print("====== SetSpaceMode called. <========");
+        }
+    };
+
+    // FUNCTION: TOGGLE SPACE MODE
+    that.toggleSpaceMode = function() {
+        var wantDebug = false;
+        if (wantDebug) {
+            print("========> ToggleSpaceMode called. =========");
+        }
+        if ((spaceMode === SPACE_WORLD) && (SelectionManager.selections.length > 1)) {
+            if (wantDebug) {
+                print("Local space editing is not available with multiple selections");
+            }
+            return;
+        }
+        if (wantDebug) {
+            print("PreToggle: " + spaceMode);
+        }
+        that.setSpaceMode((spaceMode === SPACE_LOCAL) ? SPACE_WORLD : SPACE_LOCAL, true);
+        if (wantDebug) {
+            print("PostToggle: " + spaceMode);        
+            print("======== ToggleSpaceMode called. <=========");
+        }
+    };
+
+    /**
+     * Switches the display mode back to the set desired display mode
+     */
+    that.useDesiredSpaceMode = function() {
+        var wantDebug = false;
+        if (wantDebug) {
+            print("========> UseDesiredSpaceMode called. =========");
+        }
+        that.setSpaceMode(desiredSpaceMode, false);
+        if (wantDebug) {
+            print("PostToggle: " + spaceMode);
+            print("======== UseDesiredSpaceMode called. <=========");
+        }
+    };
+
+    /**
+     * Get the currently set SpaceMode
+     * @returns {string} spaceMode
+     */
+    that.getSpaceMode = function() {
+        return spaceMode;
+    };
+
+    function addHandleTool(overlay, tool) {
+        handleTools[overlay] = tool;
+        return tool;
+    }
+
+    // @param: toolHandle:  The overlayID associated with the tool
+    //         that correlates to the tool you wish to query.
+    // @note: If toolHandle is null or undefined then activeTool
+    //        will be checked against those values as opposed to
+    //        the tool registered under toolHandle.  Null & Undefined 
+    //        are treated as separate values.
+    // @return: bool - Indicates if the activeTool is that queried.
+    function isActiveTool(toolHandle) {
+        if (!toolHandle) {
+            // Allow isActiveTool(null) and similar to return true if there's
+            // no active tool
+            return (activeTool === toolHandle);
+        }
+
+        if (!handleTools.hasOwnProperty(toolHandle)) {
+            print("WARNING: entitySelectionTool.isActiveTool - Encountered unknown grabberToolHandle: " + 
+                  toolHandle + ". Tools should be registered via addHandleTool.");
+            // EARLY EXIT
+            return false;
+        }
+
+        return (activeTool === handleTools[ toolHandle ]);
+    }
+
+    // FUNCTION: UPDATE HANDLES
+    that.updateHandles = function() {
+        var wantDebug = false;
+        if (wantDebug) {
+            print("======> Update Handles =======");
+            print("    Selections Count: " + SelectionManager.selections.length);
+            print("    SpaceMode: " + spaceMode);
+            print("    DisplayMode: " + getMode());
+        }
+        
+        if (SelectionManager.selections.length === 0) {
+            that.setOverlaysVisible(false);
+            that.clearDebugPickPlane();
+            return;
+        }
+
+        if (SelectionManager.hasSelection()) {
+            var position = SelectionManager.worldPosition;
+            var rotation = spaceMode === SPACE_LOCAL ? SelectionManager.localRotation : SelectionManager.worldRotation;
+            var dimensions = spaceMode === SPACE_LOCAL ? SelectionManager.localDimensions : SelectionManager.worldDimensions;
+            var rotationInverse = Quat.inverse(rotation);
+            var toCameraDistance = getDistanceToCamera(position);
+
+            var rotationDegrees = 90;
+            var localRotationX = Quat.fromPitchYawRollDegrees(0, 0, -rotationDegrees);
+            var rotationX = Quat.multiply(rotation, localRotationX);
+            worldRotationX = rotationX;
+            var localRotationY = Quat.fromPitchYawRollDegrees(0, rotationDegrees, 0);
+            var rotationY = Quat.multiply(rotation, localRotationY);
+            worldRotationY = rotationY;
+            var localRotationZ = Quat.fromPitchYawRollDegrees(rotationDegrees, 0, 0);
+            var rotationZ = Quat.multiply(rotation, localRotationZ);
+            worldRotationZ = rotationZ;
+            
+            var selectionBoxGeometry = {
+                position: position,
+                rotation: rotation,
+                dimensions: dimensions
+            };
+            var isCameraInsideBox = isPointInsideBox(Camera.position, selectionBoxGeometry);
+            
+            // in HMD if outside the bounding box clamp the overlays to the bounding box for now so lasers can hit them
+            var maxHandleDimension = 0;
+            if (HMD.active && !isCameraInsideBox) {
+                maxHandleDimension = Math.max(dimensions.x, dimensions.y, dimensions.z);
+            }
+
+            // UPDATE ROTATION RINGS
+            // rotateDimension is used as the base dimension for all overlays
+            var rotateDimension = Math.max(maxHandleDimension, toCameraDistance * ROTATE_RING_CAMERA_DISTANCE_MULTIPLE);
+            var rotateDimensions = { x: rotateDimension, y: rotateDimension, z: rotateDimension };
+            if (!isActiveTool(handleRotatePitchRing)) {
+                Overlays.editOverlay(handleRotatePitchRing, { 
+                    position: position, 
+                    rotation: rotationY,
+                    dimensions: rotateDimensions,
+                    majorTickMarksAngle: ROTATE_DEFAULT_TICK_MARKS_ANGLE
+                });
+            }
+            if (!isActiveTool(handleRotateYawRing)) {
+                Overlays.editOverlay(handleRotateYawRing, { 
+                    position: position, 
+                    rotation: rotationZ,
+                    dimensions: rotateDimensions,
+                    majorTickMarksAngle: ROTATE_DEFAULT_TICK_MARKS_ANGLE
+                });
+            }
+            if (!isActiveTool(handleRotateRollRing)) {
+                Overlays.editOverlay(handleRotateRollRing, { 
+                    position: position, 
+                    rotation: rotationX,
+                    dimensions: rotateDimensions,
+                    majorTickMarksAngle: ROTATE_DEFAULT_TICK_MARKS_ANGLE
+                });
+            }
+            Overlays.editOverlay(handleRotateCurrentRing, { dimensions: rotateDimensions });
+            that.updateActiveRotateRing();
+
+            // UPDATE TRANSLATION ARROWS
+            var arrowCylinderDimension = rotateDimension * TRANSLATE_ARROW_CYLINDER_CAMERA_DISTANCE_MULTIPLE / 
+                                                           ROTATE_RING_CAMERA_DISTANCE_MULTIPLE;
+            var arrowCylinderDimensions = { 
+                x: arrowCylinderDimension, 
+                y: arrowCylinderDimension * TRANSLATE_ARROW_CYLINDER_Y_MULTIPLE, 
+                z: arrowCylinderDimension 
+            };
+            var arrowConeDimension = rotateDimension * TRANSLATE_ARROW_CONE_CAMERA_DISTANCE_MULTIPLE / 
+                                                       ROTATE_RING_CAMERA_DISTANCE_MULTIPLE;
+            var arrowConeDimensions = { x: arrowConeDimension, y: arrowConeDimension, z: arrowConeDimension };
+            var arrowCylinderOffset = rotateDimension * TRANSLATE_ARROW_CYLINDER_OFFSET / ROTATE_RING_CAMERA_DISTANCE_MULTIPLE;
+            var arrowConeOffset = arrowCylinderDimensions.y * TRANSLATE_ARROW_CONE_OFFSET_CYLINDER_DIMENSION_MULTIPLE;
+            var cylinderXPosition = { x: arrowCylinderOffset, y: 0, z: 0 };
+            cylinderXPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, cylinderXPosition));
+            Overlays.editOverlay(handleTranslateXCylinder, { 
+                position: cylinderXPosition, 
+                rotation: rotationX,
+                dimensions: arrowCylinderDimensions
+            });
+            var cylinderXOffset = Vec3.subtract(cylinderXPosition, position);
+            var coneXPosition = Vec3.sum(cylinderXPosition, Vec3.multiply(Vec3.normalize(cylinderXOffset), arrowConeOffset));
+            Overlays.editOverlay(handleTranslateXCone, { 
+                position: coneXPosition, 
+                rotation: rotationX,
+                dimensions: arrowConeDimensions
+            });
+            var cylinderYPosition = { x: 0, y: arrowCylinderOffset, z: 0 };
+            cylinderYPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, cylinderYPosition));
+            Overlays.editOverlay(handleTranslateYCylinder, { 
+                position: cylinderYPosition, 
+                rotation: rotationY,
+                dimensions: arrowCylinderDimensions
+            });
+            var cylinderYOffset = Vec3.subtract(cylinderYPosition, position);
+            var coneYPosition = Vec3.sum(cylinderYPosition, Vec3.multiply(Vec3.normalize(cylinderYOffset), arrowConeOffset));
+            Overlays.editOverlay(handleTranslateYCone, { 
+                position: coneYPosition, 
+                rotation: rotationY,
+                dimensions: arrowConeDimensions
+            });
+            var cylinderZPosition = { x: 0, y: 0, z: arrowCylinderOffset };
+            cylinderZPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, cylinderZPosition));
+            Overlays.editOverlay(handleTranslateZCylinder, { 
+                position: cylinderZPosition, 
+                rotation: rotationZ,
+                dimensions: arrowCylinderDimensions
+            });
+            var cylinderZOffset = Vec3.subtract(cylinderZPosition, position);
+            var coneZPosition = Vec3.sum(cylinderZPosition, Vec3.multiply(Vec3.normalize(cylinderZOffset), arrowConeOffset));
+            Overlays.editOverlay(handleTranslateZCone, { 
+                position: coneZPosition, 
+                rotation: rotationZ,
+                dimensions: arrowConeDimensions
+            });
+
+            // UPDATE SCALE CUBE
+            var scaleCubeRotation = spaceMode === SPACE_LOCAL ? rotation : Quat.IDENTITY;            
+            var scaleCubeDimension = rotateDimension * SCALE_OVERLAY_CAMERA_DISTANCE_MULTIPLE / 
+                                                       ROTATE_RING_CAMERA_DISTANCE_MULTIPLE;
+            var scaleCubeDimensions = { x: scaleCubeDimension, y: scaleCubeDimension, z: scaleCubeDimension };
+            Overlays.editOverlay(handleScaleCube, { 
+                position: position, 
+                rotation: scaleCubeRotation,
+                dimensions: scaleCubeDimensions
+            });
+
+            // UPDATE BOUNDING BOX
+            Overlays.editOverlay(handleBoundingBox, {
+                position: position,
+                rotation: rotation,
+                dimensions: dimensions
+            });
+
+            // UPDATE STRETCH HIGHLIGHT PANELS
+            var edgeOffsetX = BOUNDING_EDGE_OFFSET * dimensions.x;
+            var edgeOffsetY = BOUNDING_EDGE_OFFSET * dimensions.y;
+            var edgeOffsetZ = BOUNDING_EDGE_OFFSET * dimensions.z;
+            var RBFPosition = { x: edgeOffsetX, y: -edgeOffsetY, z: edgeOffsetZ };
+            RBFPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, RBFPosition));
+            var RTFPosition = { x: edgeOffsetX, y: edgeOffsetY, z: edgeOffsetZ };
+            RTFPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, RTFPosition));
+            var LTNPosition = { x: -edgeOffsetX, y: edgeOffsetY, z: -edgeOffsetZ };
+            LTNPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, LTNPosition));
+            var RTNPosition = { x: edgeOffsetX, y: edgeOffsetY, z: -edgeOffsetZ };
+            RTNPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, RTNPosition));
+
+            var RBFPositionRotated = Vec3.multiplyQbyV(rotationInverse, RBFPosition);
+            var RTFPositionRotated = Vec3.multiplyQbyV(rotationInverse, RTFPosition);
+            var LTNPositionRotated = Vec3.multiplyQbyV(rotationInverse, LTNPosition);
+            var RTNPositionRotated = Vec3.multiplyQbyV(rotationInverse, RTNPosition);
+            var stretchPanelXDimensions = Vec3.subtract(RTNPositionRotated, RBFPositionRotated);
+            var tempY = Math.abs(stretchPanelXDimensions.y);
+            stretchPanelXDimensions.x = STRETCH_PANEL_WIDTH;
+            stretchPanelXDimensions.y = Math.abs(stretchPanelXDimensions.z);
+            stretchPanelXDimensions.z = tempY;
+            var stretchPanelXPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, { x: dimensions.x / 2, y: 0, z: 0 }));
+            Overlays.editOverlay(handleStretchXPanel, { 
+                position: stretchPanelXPosition, 
+                rotation: rotationZ,
+                dimensions: stretchPanelXDimensions
+            });
+            var stretchPanelYDimensions = Vec3.subtract(LTNPositionRotated, RTFPositionRotated);
+            var tempX = Math.abs(stretchPanelYDimensions.x);
+            stretchPanelYDimensions.x = Math.abs(stretchPanelYDimensions.z);
+            stretchPanelYDimensions.y = STRETCH_PANEL_WIDTH;
+            stretchPanelYDimensions.z = tempX;
+            var stretchPanelYPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, { x: 0, y: dimensions.y / 2, z: 0 }));
+            Overlays.editOverlay(handleStretchYPanel, { 
+                position: stretchPanelYPosition, 
+                rotation: rotationY,
+                dimensions: stretchPanelYDimensions
+            });
+            var stretchPanelZDimensions = Vec3.subtract(LTNPositionRotated, RBFPositionRotated);
+            tempX = Math.abs(stretchPanelZDimensions.x);
+            stretchPanelZDimensions.x = Math.abs(stretchPanelZDimensions.y);
+            stretchPanelZDimensions.y = tempX;
+            stretchPanelZDimensions.z = STRETCH_PANEL_WIDTH;
+            var stretchPanelZPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, { x: 0, y: 0, z: dimensions.z / 2 }));
+            Overlays.editOverlay(handleStretchZPanel, { 
+                position: stretchPanelZPosition, 
+                rotation: rotationX,
+                dimensions: stretchPanelZDimensions
+            });
+
+            // UPDATE STRETCH CUBES
+            var stretchCubeDimension = rotateDimension * STRETCH_CUBE_CAMERA_DISTANCE_MULTIPLE / 
+                                                           ROTATE_RING_CAMERA_DISTANCE_MULTIPLE;
+            var stretchCubeDimensions = { x: stretchCubeDimension, y: stretchCubeDimension, z: stretchCubeDimension };
+            var stretchCubeOffset = rotateDimension * STRETCH_CUBE_OFFSET / ROTATE_RING_CAMERA_DISTANCE_MULTIPLE;
+            var stretchXPosition, stretchYPosition, stretchZPosition;
+            if (isActiveTool(handleStretchXCube)) {
+                stretchXPosition = Vec3.subtract(stretchPanelXPosition, activeStretchCubePanelOffset);
+            } else {
+                stretchXPosition = { x: stretchCubeOffset, y: 0, z: 0 };
+                stretchXPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, stretchXPosition));
+            }
+            if (isActiveTool(handleStretchYCube)) {
+                stretchYPosition = Vec3.subtract(stretchPanelYPosition, activeStretchCubePanelOffset);
+            } else {
+                stretchYPosition = { x: 0, y: stretchCubeOffset, z: 0 };
+                stretchYPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, stretchYPosition));
+            }
+            if (isActiveTool(handleStretchZCube)) {
+                stretchZPosition = Vec3.subtract(stretchPanelZPosition, activeStretchCubePanelOffset);
+            } else {
+                stretchZPosition = { x: 0, y: 0, z: stretchCubeOffset };
+                stretchZPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, stretchZPosition));
+            }
+            Overlays.editOverlay(handleStretchXCube, { 
+                position: stretchXPosition, 
+                rotation: rotationX,
+                dimensions: stretchCubeDimensions 
+            });
+            Overlays.editOverlay(handleStretchYCube, { 
+                position: stretchYPosition, 
+                rotation: rotationY,
+                dimensions: stretchCubeDimensions 
+            });
+            Overlays.editOverlay(handleStretchZCube, { 
+                position: stretchZPosition,
+                rotation: rotationZ,
+                dimensions: stretchCubeDimensions 
+            });
+
+            // UPDATE SELECTION BOX (CURRENTLY INVISIBLE WITH 0 ALPHA FOR TRANSLATE XZ TOOL)
+            var inModeRotate = isActiveTool(handleRotatePitchRing) || 
+                               isActiveTool(handleRotateYawRing) || 
+                               isActiveTool(handleRotateRollRing);
+            selectionBoxGeometry.visible = !inModeRotate && !isCameraInsideBox;
+            selectionBoxGeometry.ignorePickIntersection = !selectionBoxGeometry.visible;
+            Overlays.editOverlay(selectionBox, selectionBoxGeometry);
+
+            // UPDATE ICON TRANSLATE HANDLE
+            if (SelectionManager.entityType === "ParticleEffect" || SelectionManager.entityType === "Light") {
+                var iconSelectionBoxGeometry = {
+                    position: position,
+                    rotation: rotation
+                };
+                iconSelectionBoxGeometry.visible = !inModeRotate && isCameraInsideBox;
+                iconSelectionBoxGeometry.ignorePickIntersection = !iconSelectionBoxGeometry.visible;
+                Overlays.editOverlay(iconSelectionBox, iconSelectionBoxGeometry);
+            } else {
+                Overlays.editOverlay(iconSelectionBox, {
+                    visible: false,
+                    ignorePickIntersection: true
+                });
+            }
+
+            // UPDATE DUPLICATOR (CURRENTLY HIDDEN FOR NOW)
+            var handleDuplicatorOffset = { 
+                x: DUPLICATOR_OFFSET.x * dimensions.x, 
+                y: DUPLICATOR_OFFSET.y * dimensions.y, 
+                z: DUPLICATOR_OFFSET.z * dimensions.z 
+            };
+            var handleDuplicatorPos = Vec3.sum(position, Vec3.multiplyQbyV(rotation, handleDuplicatorOffset));
+            Overlays.editOverlay(handleDuplicator, {
+                position: handleDuplicatorPos,
+                rotation: rotation,
+                dimensions: scaleCubeDimensions
+            });
+        }
+
+        that.setHandleTranslateXVisible(!activeTool || isActiveTool(handleTranslateXCone) || 
+                                                       isActiveTool(handleTranslateXCylinder));
+        that.setHandleTranslateYVisible(!activeTool || isActiveTool(handleTranslateYCone) || 
+                                                       isActiveTool(handleTranslateYCylinder));
+        that.setHandleTranslateZVisible(!activeTool || isActiveTool(handleTranslateZCone) || 
+                                                       isActiveTool(handleTranslateZCylinder));
+        that.setHandleRotatePitchVisible(!activeTool || isActiveTool(handleRotatePitchRing));
+        that.setHandleRotateYawVisible(!activeTool || isActiveTool(handleRotateYawRing));
+        that.setHandleRotateRollVisible(!activeTool || isActiveTool(handleRotateRollRing));
+
+        var showScaleStretch = !activeTool && SelectionManager.selections.length === 1 && spaceMode === SPACE_LOCAL;
+        that.setHandleStretchXVisible(showScaleStretch || isActiveTool(handleStretchXCube));
+        that.setHandleStretchYVisible(showScaleStretch || isActiveTool(handleStretchYCube));
+        that.setHandleStretchZVisible(showScaleStretch || isActiveTool(handleStretchZCube));
+        that.setHandleScaleVisible(showScaleStretch || isActiveTool(handleScaleCube));
+
+        var showOutlineForZone = (SelectionManager.selections.length === 1 && 
+                                    typeof SelectionManager.savedProperties[SelectionManager.selections[0]] !== "undefined" &&
+                                    SelectionManager.savedProperties[SelectionManager.selections[0]].type === "Zone");
+        that.setHandleBoundingBoxVisible(showOutlineForZone || (!isActiveTool(handleRotatePitchRing) &&
+                                                              !isActiveTool(handleRotateYawRing) &&
+                                                              !isActiveTool(handleRotateRollRing)));
+
+        // keep duplicator always hidden for now since you can hold Alt to duplicate while
+        // translating an entity - we may bring duplicator back for HMD only later
+        // that.setHandleDuplicatorVisible(!activeTool || isActiveTool(handleDuplicator));
+
+        if (wantDebug) {
+            print("====== Update Handles <=======");
+        }
+    };
+    Script.update.connect(that.updateHandles);
+
+    // FUNCTION: UPDATE ACTIVE ROTATE RING
+    that.updateActiveRotateRing = function() {
+        var activeRotateRing = null;
+        if (isActiveTool(handleRotatePitchRing)) {
+            activeRotateRing = handleRotatePitchRing;
+        } else if (isActiveTool(handleRotateYawRing)) {
+            activeRotateRing = handleRotateYawRing;
+        } else if (isActiveTool(handleRotateRollRing)) {
+            activeRotateRing = handleRotateRollRing;
+        }
+        if (activeRotateRing !== null) {
+            var tickMarksAngle = ctrlPressed ? ROTATE_CTRL_SNAP_ANGLE : ROTATE_DEFAULT_TICK_MARKS_ANGLE;
+            Overlays.editOverlay(activeRotateRing, { majorTickMarksAngle: tickMarksAngle });
+        }
+    };
+
+    // FUNCTION: SET OVERLAYS VISIBLE
+    that.setOverlaysVisible = function(isVisible) {
+        for (var i = 0, length = allOverlays.length; i < length; i++) {
+            Overlays.editOverlay(allOverlays[i], { visible: isVisible, ignorePickIntersection: !isVisible });
+        }
+    };
+
+    // FUNCTION: SET HANDLE TRANSLATE VISIBLE
+    that.setHandleTranslateVisible = function(isVisible) {
+        that.setHandleTranslateXVisible(isVisible);
+        that.setHandleTranslateYVisible(isVisible);
+        that.setHandleTranslateZVisible(isVisible);
+    };
+
+    that.setHandleTranslateXVisible = function(isVisible) {
+        Overlays.editOverlay(handleTranslateXCone, { visible: isVisible, ignorePickIntersection: !isVisible });
+        Overlays.editOverlay(handleTranslateXCylinder, { visible: isVisible, ignorePickIntersection: !isVisible });
+    };
+
+    that.setHandleTranslateYVisible = function(isVisible) {
+        Overlays.editOverlay(handleTranslateYCone, { visible: isVisible, ignorePickIntersection: !isVisible });
+        Overlays.editOverlay(handleTranslateYCylinder, { visible: isVisible, ignorePickIntersection: !isVisible });
+    };
+
+    that.setHandleTranslateZVisible = function(isVisible) {
+        Overlays.editOverlay(handleTranslateZCone, { visible: isVisible, ignorePickIntersection: !isVisible });
+        Overlays.editOverlay(handleTranslateZCylinder, { visible: isVisible, ignorePickIntersection: !isVisible });
+    };
+
+    // FUNCTION: SET HANDLE ROTATE VISIBLE
+    that.setHandleRotateVisible = function(isVisible) {
+        that.setHandleRotatePitchVisible(isVisible);
+        that.setHandleRotateYawVisible(isVisible);
+        that.setHandleRotateRollVisible(isVisible);
+    };
+
+    that.setHandleRotatePitchVisible = function(isVisible) {
+        Overlays.editOverlay(handleRotatePitchRing, { visible: isVisible, ignorePickIntersection: !isVisible });
+    };
+
+    that.setHandleRotateYawVisible = function(isVisible) {
+        Overlays.editOverlay(handleRotateYawRing, { visible: isVisible, ignorePickIntersection: !isVisible });
+    };
+
+    that.setHandleRotateRollVisible = function(isVisible) {
+        Overlays.editOverlay(handleRotateRollRing, { visible: isVisible, ignorePickIntersection: !isVisible });
+    };
+
+    // FUNCTION: SET HANDLE STRETCH VISIBLE
+    that.setHandleStretchVisible = function(isVisible) {
+        that.setHandleStretchXVisible(isVisible);
+        that.setHandleStretchYVisible(isVisible);
+        that.setHandleStretchZVisible(isVisible);
+    };
+
+    that.setHandleStretchXVisible = function(isVisible) {
+        Overlays.editOverlay(handleStretchXCube, { visible: isVisible, ignorePickIntersection: !isVisible });
+    };
+
+    that.setHandleStretchYVisible = function(isVisible) {
+        Overlays.editOverlay(handleStretchYCube, { visible: isVisible, ignorePickIntersection: !isVisible });
+    };
+
+    that.setHandleStretchZVisible = function(isVisible) {
+        Overlays.editOverlay(handleStretchZCube, { visible: isVisible, ignorePickIntersection: !isVisible });
+    };
+    
+    // FUNCTION: SET HANDLE SCALE VISIBLE
+    that.setHandleScaleVisible = function(isVisible) {
+        that.setHandleScaleVisible(isVisible);
+        that.setHandleBoundingBoxVisible(isVisible);
+    };
+
+    that.setHandleScaleVisible = function(isVisible) {
+        Overlays.editOverlay(handleScaleCube, { visible: isVisible, ignorePickIntersection: !isVisible });
+    };
+
+    that.setHandleBoundingBoxVisible = function(isVisible) {
+        Overlays.editOverlay(handleBoundingBox, { visible: isVisible, ignorePickIntersection: true });
+    };
+
+    // FUNCTION: SET HANDLE DUPLICATOR VISIBLE
+    that.setHandleDuplicatorVisible = function(isVisible) {
+        Overlays.editOverlay(handleDuplicator, { visible: isVisible, ignorePickIntersection: !isVisible });
+    };
+
+    // FUNCTION: DEBUG PICK PLANE
+    that.showDebugPickPlane = function(pickPlanePosition, pickPlaneNormal) {
+        var planePlusNormal = Vec3.sum(pickPlanePosition, pickPlaneNormal);
+        var rotation = Quat.lookAtSimple(planePlusNormal, pickPlanePosition);
+        var dimensionXZ = getDistanceToCamera(pickPlanePosition) * 1.25;   
+        var dimensions = { x:dimensionXZ, y:dimensionXZ, z:STRETCH_PANEL_WIDTH };
+        Overlays.editOverlay(debugPickPlane, {
+            position: pickPlanePosition,
+            rotation: rotation,
+            dimensions: dimensions,
+            visible: true
+        });
+    };
+    
+    that.showDebugPickPlaneHit = function(pickHitPosition) {
+        var dimension = getDistanceToCamera(pickHitPosition) * DEBUG_PICK_PLANE_HIT_CAMERA_DISTANCE_MULTIPLE;
+        var pickPlaneHit = Overlays.addOverlay("shape", {
+            alpha: 0.5,
+            shape: "Sphere",
+            solid: true,
+            visible: true,
+            ignorePickIntersection: true,
+            drawInFront: false,
+            color: COLOR_DEBUG_PICK_PLANE_HIT,
+            position: pickHitPosition,
+            dimensions: { x: dimension, y: dimension, z: dimension }
+        });
+        debugPickPlaneHits.push(pickPlaneHit);
+        if (debugPickPlaneHits.length > DEBUG_PICK_PLANE_HIT_LIMIT) {
+            var removedPickPlaneHit = debugPickPlaneHits.shift();
+            Overlays.deleteOverlay(removedPickPlaneHit);
+        }
+    };
+    
+    that.clearDebugPickPlane = function() {
+        Overlays.editOverlay(debugPickPlane, { visible: false });
+        for (var i = 0; i < debugPickPlaneHits.length; i++) {
+            Overlays.deleteOverlay(debugPickPlaneHits[i]);
+        }
+        debugPickPlaneHits = [];
+    };
+    
+    // TOOL DEFINITION: HANDLE TRANSLATE XZ TOOL
+    function addHandleTranslateXZTool(overlay, mode, doDuplicate) {
+        var initialPick = null;
+        var isConstrained = false;
+        var constrainMajorOnly = false;
+        var startPosition = null;
+        var duplicatedEntityIDs = null;
+        var pickPlanePosition = null;
+        var pickPlaneNormal = { x: 0, y: 1, z: 0 };
+        var greatestDimension = 0.0;
+        var startingDistance = 0.0;
+        var startingElevation = 0.0;
+        addHandleTool(overlay, {
+            mode: mode,
+            onBegin: function(event, pickRay, pickResult) {
+                var wantDebug = false;
+                if (wantDebug) {
+                    print("================== TRANSLATE_XZ(Beg) -> =======================");
+                    Vec3.print("    pickRay", pickRay);
+                    Vec3.print("    pickRay.origin", pickRay.origin);
+                    Vec3.print("    pickResult.intersection", pickResult.intersection);
+                }
+
+                // Duplicate entities if alt is pressed.  This will make a
+                // copy of the selected entities and move the _original_ entities, not
+                // the new ones.
+                if (event.isAlt || doDuplicate) {
+                    duplicatedEntityIDs = SelectionManager.duplicateSelection();
+                    var ids = [];
+                    for (var i = 0; i < duplicatedEntityIDs.length; ++i) {
+                        ids.push(duplicatedEntityIDs[i].entityID);
+                    }
+                    SelectionManager.setSelections(ids);
+                } else {
+                    duplicatedEntityIDs = null;
+                }
+
+                SelectionManager.saveProperties();
+                that.resetPreviousHandleColor();
+
+                that.setHandleTranslateVisible(false);
+                that.setHandleRotateVisible(false);
+                that.setHandleScaleVisible(false);
+                that.setHandleStretchVisible(false);
+                that.setHandleDuplicatorVisible(false);
+
+                startPosition = SelectionManager.worldPosition;
+                pickPlanePosition = pickResult.intersection;
+                greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, 
+                                                      SelectionManager.worldDimensions.y),
+                                                      SelectionManager.worldDimensions.z);
+                startingDistance = Vec3.distance(pickRay.origin, SelectionManager.position);
+                startingElevation = this.elevation(pickRay.origin, pickPlanePosition);
+                if (wantDebug) {
+                    print("    longest dimension: " + greatestDimension);
+                    print("    starting distance: " + startingDistance);
+                    print("    starting elevation: " + startingElevation);
+                }
+
+                initialPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal);
+                
+                if (debugPickPlaneEnabled) {
+                    that.showDebugPickPlane(pickPlanePosition, pickPlaneNormal);
+                    that.showDebugPickPlaneHit(initialPick);
+                }
+
+                isConstrained = false;
+                if (wantDebug) {
+                    print("================== TRANSLATE_XZ(End) <- =======================");
+                }
+            },
+            onEnd: function(event, reason) {
+                pushCommandForSelections(duplicatedEntityIDs);
+                if (isConstrained) {
+                    Overlays.editOverlay(xRailOverlay, {
+                        visible: false,
+                        ignorePickIntersection: true
+                    });
+                    Overlays.editOverlay(zRailOverlay, {
+                        visible: false,
+                        ignorePickIntersection: true
+                    });
+                }
+            },
+            elevation: function(origin, intersection) {
+                return (origin.y - intersection.y) / Vec3.distance(origin, intersection);
+            },
+            onMove: function(event) {
+                var wantDebug = false;
+                var pickRay = generalComputePickRay(event.x, event.y);
+
+                var newPick = rayPlaneIntersection2(pickRay, pickPlanePosition, pickPlaneNormal);
+
+                // If the pick ray doesn't hit the pick plane in this direction, do nothing.
+                // this will happen when someone drags across the horizon from the side they started on.
+                if (!newPick) {
+                    if (wantDebug) {
+                        print("    "+ mode + "Pick ray does not intersect XZ plane.");
+                    }
+                    
+                    // EARLY EXIT--(Invalid ray detected.)
+                    return;
+                }
+                
+                if (debugPickPlaneEnabled) {
+                    that.showDebugPickPlaneHit(newPick);
+                }
+
+                var vector = Vec3.subtract(newPick, initialPick);
+
+                // If the mouse is too close to the horizon of the pick plane, stop moving
+                var MIN_ELEVATION = 0.02; //  largest dimension of object divided by distance to it
+                var elevation = this.elevation(pickRay.origin, newPick);
+                if (wantDebug) {
+                    print("Start Elevation: " + startingElevation + ", elevation: " + elevation);
+                }
+                if ((startingElevation > 0.0 && elevation < MIN_ELEVATION) ||
+                    (startingElevation < 0.0 && elevation > -MIN_ELEVATION)) {
+                    if (wantDebug) {
+                        print("    "+ mode + " - too close to horizon!");
+                    }
+
+                    // EARLY EXIT--(Don't proceed past the reached limit.)
+                    return;
+                }
+
+                //  If the angular size of the object is too small, stop moving
+                var MIN_ANGULAR_SIZE = 0.01; //  Radians
+                if (greatestDimension > 0) {
+                    var angularSize = Math.atan(greatestDimension / Vec3.distance(pickRay.origin, newPick));
+                    if (wantDebug) {
+                        print("Angular size = " + angularSize);
+                    }
+                    if (angularSize < MIN_ANGULAR_SIZE) {
+                        return;
+                    }
+                }
+
+                // If shifted, constrain to one axis
+                if (event.isShifted) {
+                    if (Math.abs(vector.x) > Math.abs(vector.z)) {
+                        vector.z = 0;
+                    } else {
+                        vector.x = 0;
+                    }
+                    if (!isConstrained) {
+                        var xStart = Vec3.sum(startPosition, {
+                            x: -RAIL_AXIS_LENGTH,
+                            y: 0,
+                            z: 0
+                        });
+                        var xEnd = Vec3.sum(startPosition, {
+                            x: RAIL_AXIS_LENGTH,
+                            y: 0,
+                            z: 0
+                        });
+                        var zStart = Vec3.sum(startPosition, {
+                            x: 0,
+                            y: 0,
+                            z: -RAIL_AXIS_LENGTH
+                        });
+                        var zEnd = Vec3.sum(startPosition, {
+                            x: 0,
+                            y: 0,
+                            z: RAIL_AXIS_LENGTH
+                        });
+                        Overlays.editOverlay(xRailOverlay, {
+                            start: xStart,
+                            end: xEnd,
+                            visible: true,
+                            ignorePickIntersection: true
+                        });
+                        Overlays.editOverlay(zRailOverlay, {
+                            start: zStart,
+                            end: zEnd,
+                            visible: true,
+                            ignorePickIntersection: true
+                        });
+                        isConstrained = true;
+                    }
+                } else {
+                    if (isConstrained) {
+                        Overlays.editOverlay(xRailOverlay, {
+                            visible: false,
+                            ignorePickIntersection: true
+                        });
+                        Overlays.editOverlay(zRailOverlay, {
+                            visible: false,
+                            ignorePickIntersection: true
+                        });
+                        isConstrained = false;
+                    }
+                }
+
+                constrainMajorOnly = event.isControl;
+                var negateAndHalve = -0.5;
+                var cornerPosition = Vec3.sum(startPosition, Vec3.multiply(negateAndHalve, SelectionManager.worldDimensions));
+                vector = Vec3.subtract(
+                    grid.snapToGrid(Vec3.sum(cornerPosition, vector), constrainMajorOnly),
+                    cornerPosition);
+
+                // editing a parent will cause all the children to automatically follow along, so don't
+                // edit any entity who has an ancestor in SelectionManager.selections
+                var toMove = SelectionManager.selections.filter(function (selection) {
+                    if (SelectionManager.selections.indexOf(SelectionManager.savedProperties[selection].parentID) >= 0) {
+                        return false; // a parent is also being moved, so don't issue an edit for this entity
+                    } else {
+                        return true;
+                    }
+                });
+
+                for (var i = 0; i < toMove.length; i++) {
+                    var properties = SelectionManager.savedProperties[toMove[i]];
+                    if (!properties) {
+                        continue;
+                    }
+                    var newPosition = Vec3.sum(properties.position, {
+                        x: vector.x,
+                        y: 0,
+                        z: vector.z
+                    });
+                    Entities.editEntity(toMove[i], {
+                        position: newPosition
+                    });
+
+                    if (wantDebug) {
+                        print("translateXZ... ");
+                        Vec3.print("                 vector:", vector);
+                        Vec3.print("            newPosition:", properties.position);
+                        Vec3.print("            newPosition:", newPosition);
+                    }
+                }
+
+                SelectionManager._update(false, this);
+            }
+        });
+    }
+
+    // TOOL DEFINITION: HANDLE TRANSLATE TOOL    
+    function addHandleTranslateTool(overlay, mode, direction) {
+        var pickPlanePosition = null;
+        var pickPlaneNormal = null;
+        var initialPick = null;
+        var projectionVector = null;
+        var previousPickRay = null;
+        var rotation = null;
+        addHandleTool(overlay, {
+            mode: mode,
+            onBegin: function(event, pickRay, pickResult) {
+                // Duplicate entities if alt is pressed.  This will make a
+                // copy of the selected entities and move the _original_ entities, not
+                // the new ones.
+                if (event.isAlt) {
+                    duplicatedEntityIDs = SelectionManager.duplicateSelection();
+                    var ids = [];
+                    for (var i = 0; i < duplicatedEntityIDs.length; ++i) {
+                        ids.push(duplicatedEntityIDs[i].entityID);
+                    }
+                    SelectionManager.setSelections(ids);
+                } else {
+                    duplicatedEntityIDs = null;
+                }
+
+                var axisVector;
+                if (direction === TRANSLATE_DIRECTION.X) {
+                    axisVector = { x: 1, y: 0, z: 0 };
+                } else if (direction === TRANSLATE_DIRECTION.Y) {
+                    axisVector = { x: 0, y: 1, z: 0 };
+                } else if (direction === TRANSLATE_DIRECTION.Z) {
+                    axisVector = { x: 0, y: 0, z: 1 };
+                }
+                
+                rotation = spaceMode === SPACE_LOCAL ? SelectionManager.localRotation : SelectionManager.worldRotation;
+                axisVector = Vec3.multiplyQbyV(rotation, axisVector);
+                pickPlaneNormal = Vec3.cross(Vec3.cross(pickRay.direction, axisVector), axisVector);
+                pickPlanePosition = SelectionManager.worldPosition;
+                initialPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal);
+    
+                SelectionManager.saveProperties();
+                that.resetPreviousHandleColor();
+
+                that.setHandleTranslateXVisible(direction === TRANSLATE_DIRECTION.X);
+                that.setHandleTranslateYVisible(direction === TRANSLATE_DIRECTION.Y);
+                that.setHandleTranslateZVisible(direction === TRANSLATE_DIRECTION.Z);
+                that.setHandleRotateVisible(false);
+                that.setHandleStretchVisible(false);
+                that.setHandleScaleVisible(false);
+                that.setHandleDuplicatorVisible(false);
+                
+                previousPickRay = pickRay;
+                
+                if (debugPickPlaneEnabled) {
+                    that.showDebugPickPlane(pickPlanePosition, pickPlaneNormal);
+                    that.showDebugPickPlaneHit(initialPick);
+                }
+            },
+            onEnd: function(event, reason) {
+                pushCommandForSelections(duplicatedEntityIDs);
+            },
+            onMove: function(event) {
+                var pickRay = generalComputePickRay(event.x, event.y);
+                
+                // Use previousPickRay if new pickRay will cause resulting rayPlaneIntersection values to wrap around
+                if (usePreviousPickRay(pickRay.direction, previousPickRay.direction, pickPlaneNormal)) {
+                    pickRay = previousPickRay;
+                }
+    
+                var newPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal);
+                if (debugPickPlaneEnabled) {
+                    that.showDebugPickPlaneHit(newPick);
+                }
+                
+                var vector = Vec3.subtract(newPick, initialPick);
+                
+                if (direction === TRANSLATE_DIRECTION.X) {
+                    projectionVector = { x: 1, y: 0, z: 0 };
+                } else if (direction === TRANSLATE_DIRECTION.Y) {
+                    projectionVector = { x: 0, y: 1, z: 0 };
+                } else if (direction === TRANSLATE_DIRECTION.Z) {
+                    projectionVector = { x: 0, y: 0, z: 1 };
+                }
+                projectionVector = Vec3.multiplyQbyV(rotation, projectionVector);
+
+                var dotVector = Vec3.dot(vector, projectionVector);
+                vector = Vec3.multiply(dotVector, projectionVector);
+                var gridOrigin = grid.getOrigin();
+                vector = Vec3.subtract(grid.snapToGrid(Vec3.sum(vector, gridOrigin)), gridOrigin);
+                
+                var wantDebug = false;
+                if (wantDebug) {
+                    print("translateUpDown... ");
+                    print("                event.y:" + event.y);
+                    Vec3.print("        newIntersection:", newIntersection);
+                    Vec3.print("                 vector:", vector);
+                }
+
+                // editing a parent will cause all the children to automatically follow along, so don't
+                // edit any entity who has an ancestor in SelectionManager.selections
+                var toMove = SelectionManager.selections.filter(function (selection) {
+                    if (SelectionManager.selections.indexOf(SelectionManager.savedProperties[selection].parentID) >= 0) {
+                        return false; // a parent is also being moved, so don't issue an edit for this entity
+                    } else {
+                        return true;
+                    }
+                });
+
+                for (var i = 0; i < toMove.length; i++) {
+                    var id = toMove[i];
+                    var properties = SelectionManager.savedProperties[id];
+                    var newPosition = Vec3.sum(properties.position, vector);
+                    Entities.editEntity(id, { position: newPosition });
+                }
+                
+                previousPickRay = pickRay;
+    
+                SelectionManager._update(false, this);
+            }
+        });
+    }
+
+    // TOOL DEFINITION: HANDLE STRETCH TOOL   
+    function addHandleStretchTool(overlay, mode, directionEnum) {
+        var initialPick = null;
+        var initialPosition = null;
+        var initialDimensions = null;
+        var rotation = null;
+        var registrationPoint = null;
+        var pickPlanePosition = null;
+        var pickPlaneNormal = null;
+        var previousPickRay = null;
+        var directionVector = null;
+        var axisVector = null;
+        var signs = null;
+        var mask = null;
+        var stretchPanel = null;
+        var handleStretchCube = null;
+        var deltaPivot = null;
+        addHandleTool(overlay, {
+            mode: mode,
+            onBegin: function(event, pickRay, pickResult) {             
+                if (directionEnum === STRETCH_DIRECTION.X) {
+                    stretchPanel = handleStretchXPanel;
+                    handleStretchCube = handleStretchXCube;
+                    directionVector = { x: -1, y: 0, z: 0 };
+                } else if (directionEnum === STRETCH_DIRECTION.Y) {
+                    stretchPanel = handleStretchYPanel;
+                    handleStretchCube = handleStretchYCube;
+                    directionVector = { x: 0, y: -1, z: 0 };
+                } else if (directionEnum === STRETCH_DIRECTION.Z) {
+                    stretchPanel = handleStretchZPanel;
+                    handleStretchCube = handleStretchZCube;
+                    directionVector = { x: 0, y: 0, z: -1 };
+                }
+                
+                rotation = SelectionManager.localRotation;
+                initialPosition = SelectionManager.localPosition;
+                initialDimensions = SelectionManager.localDimensions;
+                registrationPoint = SelectionManager.localRegistrationPoint;
+                
+                axisVector = Vec3.multiply(NEGATE_VECTOR, directionVector);
+                axisVector = Vec3.multiplyQbyV(rotation, axisVector);
+                
+                signs = {
+                    x: directionVector.x < 0 ? -1 : (directionVector.x > 0 ? 1 : 0),
+                    y: directionVector.y < 0 ? -1 : (directionVector.y > 0 ? 1 : 0),
+                    z: directionVector.z < 0 ? -1 : (directionVector.z > 0 ? 1 : 0)
+                };
+                mask = {
+                    x: Math.abs(directionVector.x) > 0 ? 1 : 0,
+                    y: Math.abs(directionVector.y) > 0 ? 1 : 0,
+                    z: Math.abs(directionVector.z) > 0 ? 1 : 0
+                };
+                
+                var pivot = directionVector;
+                var offset = Vec3.multiply(directionVector, NEGATE_VECTOR);
+                
+                // Modify range of registrationPoint to be [-0.5, 0.5]
+                var centeredRP = Vec3.subtract(registrationPoint, {
+                    x: 0.5,
+                    y: 0.5,
+                    z: 0.5
+                });
+
+                // Scale pivot to be in the same range as registrationPoint
+                var scaledPivot = Vec3.multiply(0.5, pivot);
+                deltaPivot = Vec3.subtract(centeredRP, scaledPivot);
+
+                var scaledOffset = Vec3.multiply(0.5, offset);
+
+                // Offset from the registration point
+                var offsetRP = Vec3.subtract(scaledOffset, centeredRP);
+
+                // Scaled offset in world coordinates
+                var scaledOffsetWorld = Vec3.multiplyVbyV(initialDimensions, offsetRP);
+                
+                pickPlaneNormal = Vec3.cross(Vec3.cross(pickRay.direction, axisVector), axisVector);
+                pickPlanePosition = Vec3.sum(initialPosition, Vec3.multiplyQbyV(rotation, scaledOffsetWorld));
+                initialPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal);
+
+                that.setHandleTranslateVisible(false);
+                that.setHandleRotateVisible(false);
+                that.setHandleScaleVisible(true);
+                that.setHandleStretchXVisible(directionEnum === STRETCH_DIRECTION.X);
+                that.setHandleStretchYVisible(directionEnum === STRETCH_DIRECTION.Y);
+                that.setHandleStretchZVisible(directionEnum === STRETCH_DIRECTION.Z);
+                that.setHandleDuplicatorVisible(false);
+            
+                SelectionManager.saveProperties();
+                that.resetPreviousHandleColor();
+
+                var collisionToRemove = "myAvatar";
+                var properties = Entities.getEntityProperties(SelectionManager.selections[0]);
+                if (properties.collidesWith.indexOf(collisionToRemove) > -1) {
+                    var newCollidesWith = properties.collidesWith.replace(collisionToRemove, "");
+                    Entities.editEntity(SelectionManager.selections[0], {collidesWith: newCollidesWith});
+                    that.replaceCollisionsAfterStretch = true;
+                }
+
+                if (stretchPanel !== null) {
+                    Overlays.editOverlay(stretchPanel, { visible: true, ignorePickIntersection: false });
+                }
+                var stretchCubePosition = Overlays.getProperty(handleStretchCube, "position");
+                var stretchPanelPosition = Overlays.getProperty(stretchPanel, "position");
+                activeStretchCubePanelOffset = Vec3.subtract(stretchPanelPosition, stretchCubePosition);
+                
+                previousPickRay = pickRay;
+
+                if (debugPickPlaneEnabled) {
+                    that.showDebugPickPlane(pickPlanePosition, pickPlaneNormal);
+                    that.showDebugPickPlaneHit(initialPick);
+                }
+            },
+            onEnd: function(event, reason) {                
+                if (that.replaceCollisionsAfterStretch) {
+                    var newCollidesWith = SelectionManager.savedProperties[SelectionManager.selections[0]].collidesWith;
+                    Entities.editEntity(SelectionManager.selections[0], {collidesWith: newCollidesWith});
+                    that.replaceCollisionsAfterStretch = false;
+                }
+                
+                if (stretchPanel !== null) {
+                    Overlays.editOverlay(stretchPanel, { visible: false, ignorePickIntersection: true });
+                }
+                activeStretchCubePanelOffset = null;
+                
+                pushCommandForSelections();
+            },
+            onMove: function(event) {            
+                var pickRay = generalComputePickRay(event.x, event.y);
+                
+                // Use previousPickRay if new pickRay will cause resulting rayPlaneIntersection values to wrap around
+                if (usePreviousPickRay(pickRay.direction, previousPickRay.direction, pickPlaneNormal)) {
+                    pickRay = previousPickRay;
+                }
+                
+                var newPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal);
+                if (debugPickPlaneEnabled) {
+                    that.showDebugPickPlaneHit(newPick);
+                }
+                
+                var changeInDimensions = Vec3.subtract(newPick, initialPick);
+                var dotVector = Vec3.dot(changeInDimensions, axisVector);
+                changeInDimensions = Vec3.multiply(dotVector, axisVector);
+                changeInDimensions = Vec3.multiplyQbyV(Quat.inverse(rotation), changeInDimensions);
+                changeInDimensions = Vec3.multiplyVbyV(mask, changeInDimensions);
+                changeInDimensions = grid.snapToSpacing(changeInDimensions);
+                changeInDimensions = Vec3.multiply(NEGATE_VECTOR, Vec3.multiplyVbyV(signs, changeInDimensions));    
+
+                var newDimensions = Vec3.sum(initialDimensions, changeInDimensions);
+
+                var minimumDimension = Entities.getPropertyInfo("dimensions").minimum; 
+                if (newDimensions.x < minimumDimension) {
+                    newDimensions.x = minimumDimension;
+                    changeInDimensions.x = minimumDimension - initialDimensions.x;
+                }
+                if (newDimensions.y < minimumDimension) {
+                    newDimensions.y = minimumDimension;
+                    changeInDimensions.y = minimumDimension - initialDimensions.y;
+                }
+                if (newDimensions.z < minimumDimension) {
+                    newDimensions.z = minimumDimension;
+                    changeInDimensions.z = minimumDimension - initialDimensions.z;
+                }
+
+                var changeInPosition = Vec3.multiplyQbyV(rotation, Vec3.multiplyVbyV(deltaPivot, changeInDimensions));
+                var newPosition = Vec3.sum(initialPosition, changeInPosition);
+        
+                Entities.editEntity(SelectionManager.selections[0], {
+                    position: newPosition,
+                    dimensions: newDimensions
+                });
+                    
+                var wantDebug = false;
+                if (wantDebug) {
+                    print(mode);
+                    Vec3.print("            changeInDimensions:", changeInDimensions);
+                    Vec3.print("                 newDimensions:", newDimensions);
+                    Vec3.print("              changeInPosition:", changeInPosition);
+                    Vec3.print("                   newPosition:", newPosition);
+                }
+                
+                previousPickRay = pickRay;
+        
+                SelectionManager._update(false, this);
+            }
+        });
+    }
+
+    // TOOL DEFINITION: HANDLE SCALE TOOL   
+    function addHandleScaleTool(overlay, mode) {
+        var initialPick = null;
+        var initialPosition = null;
+        var initialDimensions = null;
+        var pickPlanePosition = null;
+        var pickPlaneNormal = null;
+        var previousPickRay = null;     
+        addHandleTool(overlay, {
+            mode: mode,
+            onBegin: function(event, pickRay, pickResult) {
+                initialPosition = SelectionManager.localPosition;
+                initialDimensions = SelectionManager.localDimensions;               
+                
+                pickPlanePosition = initialPosition;                
+                pickPlaneNormal = Vec3.subtract(pickRay.origin, pickPlanePosition);
+                initialPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal);
+
+                that.setHandleTranslateVisible(false);
+                that.setHandleRotateVisible(false);
+                that.setHandleScaleVisible(true);
+                that.setHandleStretchVisible(false);
+                that.setHandleDuplicatorVisible(false);
+            
+                SelectionManager.saveProperties();
+                that.resetPreviousHandleColor();
+
+                var collisionToRemove = "myAvatar";
+                var properties = Entities.getEntityProperties(SelectionManager.selections[0]);
+                if (properties.collidesWith.indexOf(collisionToRemove) > -1) {
+                    var newCollidesWith = properties.collidesWith.replace(collisionToRemove, "");
+                    Entities.editEntity(SelectionManager.selections[0], {collidesWith: newCollidesWith});
+                    that.replaceCollisionsAfterStretch = true;
+                }
+
+                previousPickRay = pickRay;
+                
+                if (debugPickPlaneEnabled) {
+                    that.showDebugPickPlane(pickPlanePosition, pickPlaneNormal);
+                    that.showDebugPickPlaneHit(initialPick);
+                }
+            },
+            onEnd: function(event, reason) {                
+                if (that.replaceCollisionsAfterStretch) {
+                    var newCollidesWith = SelectionManager.savedProperties[SelectionManager.selections[0]].collidesWith;
+                    Entities.editEntity(SelectionManager.selections[0], {collidesWith: newCollidesWith});
+                    that.replaceCollisionsAfterStretch = false;
+                }
+                
+                pushCommandForSelections();
+            },
+            onMove: function(event) {            
+                var pickRay = generalComputePickRay(event.x, event.y);
+                
+                // Use previousPickRay if new pickRay will cause resulting rayPlaneIntersection values to wrap around
+                if (usePreviousPickRay(pickRay.direction, previousPickRay.direction, pickPlaneNormal)) {
+                    pickRay = previousPickRay;
+                }
+                
+                var newPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal);
+                if (debugPickPlaneEnabled) {
+                    that.showDebugPickPlaneHit(newPick);
+                }
+                
+                var toCameraDistance = getDistanceToCamera(initialPosition);  
+                var dimensionsMultiple = toCameraDistance * SCALE_DIMENSIONS_CAMERA_DISTANCE_MULTIPLE;
+                var changeInDimensions = Vec3.subtract(newPick, initialPick);                   
+                changeInDimensions = Vec3.multiplyQbyV(Quat.inverse(Camera.orientation), changeInDimensions);
+                changeInDimensions = grid.snapToSpacing(changeInDimensions);
+                changeInDimensions = Vec3.multiply(changeInDimensions, dimensionsMultiple);
+                
+                var averageDimensionChange = (changeInDimensions.x + changeInDimensions.y + changeInDimensions.z) / 3;
+                var averageInitialDimension = (initialDimensions.x + initialDimensions.y + initialDimensions.z) / 3;
+                percentChange = averageDimensionChange / averageInitialDimension;
+                percentChange += 1.0;
+                
+                var newDimensions = Vec3.multiply(percentChange, initialDimensions);
+                newDimensions.x = Math.abs(newDimensions.x);
+                newDimensions.y = Math.abs(newDimensions.y);
+                newDimensions.z = Math.abs(newDimensions.z);
+
+                var minimumDimension = Entities.getPropertyInfo("dimensions").minimum; 
+                if (newDimensions.x < minimumDimension) {
+                    newDimensions.x = minimumDimension;
+                    changeInDimensions.x = minimumDimension - initialDimensions.x;
+                }
+                if (newDimensions.y < minimumDimension) {
+                    newDimensions.y = minimumDimension;
+                    changeInDimensions.y = minimumDimension - initialDimensions.y;
+                }
+                if (newDimensions.z < minimumDimension) {
+                    newDimensions.z = minimumDimension;
+                    changeInDimensions.z = minimumDimension - initialDimensions.z;
+                }
+                
+                Entities.editEntity(SelectionManager.selections[0], { dimensions: newDimensions });
+                    
+                var wantDebug = false;
+                if (wantDebug) {
+                    print(mode);
+                    Vec3.print("            changeInDimensions:", changeInDimensions);
+                    Vec3.print("                 newDimensions:", newDimensions);
+                }
+                
+                previousPickRay = pickRay;
+        
+                SelectionManager._update(false, this);
+            }
+        });
+    }
+
+    // FUNCTION: UPDATE ROTATION DEGREES OVERLAY
+    function updateRotationDegreesOverlay(angleFromZero, position) {
+        var toCameraDistance = getDistanceToCamera(position);
+        var overlayProps = {
+            position: position,
+            dimensions: {
+                x: toCameraDistance * ROTATE_DISPLAY_SIZE_X_MULTIPLIER,
+                y: toCameraDistance * ROTATE_DISPLAY_SIZE_Y_MULTIPLIER
+            },
+            lineHeight: toCameraDistance * ROTATE_DISPLAY_LINE_HEIGHT_MULTIPLIER,
+            text: normalizeDegrees(-angleFromZero) + "°"
+        };
+        Overlays.editOverlay(rotationDegreesDisplay, overlayProps);
+    }
+
+    // FUNCTION DEF: updateSelectionsRotation
+    //    Helper func used by rotation handle tools 
+    function updateSelectionsRotation(rotationChange, initialPosition) {
+        if (!rotationChange) {
+            print("ERROR: entitySelectionTool.updateSelectionsRotation - Invalid arg specified!!");
+
+            // EARLY EXIT
+            return;
+        }
+
+        // Entities should only reposition if we are rotating multiple selections around
+        // the selections center point.  Otherwise, the rotation will be around the entities
+        // registration point which does not need repositioning.
+        var reposition = (SelectionManager.selections.length > 1);
+
+        // editing a parent will cause all the children to automatically follow along, so don't
+        // edit any entity who has an ancestor in SelectionManager.selections
+        var toRotate = SelectionManager.selections.filter(function (selection) {
+            if (SelectionManager.selections.indexOf(SelectionManager.savedProperties[selection].parentID) >= 0) {
+                return false; // a parent is also being moved, so don't issue an edit for this entity
+            } else {
+                return true;
+            }
+        });
+
+        for (var i = 0; i < toRotate.length; i++) {
+            var entityID = toRotate[i];
+            var initialProperties = SelectionManager.savedProperties[entityID];
+
+            var newProperties = {
+                rotation: Quat.multiply(rotationChange, initialProperties.rotation)
+            };
+
+            if (reposition) {
+                var dPos = Vec3.subtract(initialProperties.position, initialPosition);
+                dPos = Vec3.multiplyQbyV(rotationChange, dPos);
+                newProperties.position = Vec3.sum(initialPosition, dPos);
+            }
+
+            Entities.editEntity(entityID, newProperties);
+        }
+    }
+
+    // TOOL DEFINITION: HANDLE ROTATION TOOL   
+    function addHandleRotateTool(overlay, mode, direction) {
+        var selectedHandle = null;
+        var worldRotation = null;
+        var initialRotation = null;
+        var rotationCenter = null;
+        var rotationNormal = null;
+        var rotationZero = null;
+        var rotationDegreesPosition = null;
+        addHandleTool(overlay, {
+            mode: mode,
+            onBegin: function(event, pickRay, pickResult) {
+                var wantDebug = false;
+                if (wantDebug) {
+                    print("================== " + getMode() + "(addHandleRotateTool onBegin) -> =======================");
+                }
+                
+                if (direction === ROTATE_DIRECTION.PITCH) {
+                    rotationNormal = { x: 1, y: 0, z: 0 };
+                    worldRotation = worldRotationY;
+                    selectedHandle = handleRotatePitchRing;
+                } else if (direction === ROTATE_DIRECTION.YAW) {
+                    rotationNormal = { x: 0, y: 1, z: 0 };
+                    worldRotation = worldRotationZ;
+                    selectedHandle = handleRotateYawRing;
+                } else if (direction === ROTATE_DIRECTION.ROLL) {
+                    rotationNormal = { x: 0, y: 0, z: 1 };
+                    worldRotation = worldRotationX;
+                    selectedHandle = handleRotateRollRing;
+                }
+                
+                initialRotation = spaceMode === SPACE_LOCAL ? SelectionManager.localRotation : SelectionManager.worldRotation;
+                rotationNormal = Vec3.multiplyQbyV(initialRotation, rotationNormal);
+                rotationCenter = SelectionManager.worldPosition;
+
+                SelectionManager.saveProperties();
+                that.resetPreviousHandleColor();
+    
+                that.setHandleTranslateVisible(false);
+                that.setHandleRotatePitchVisible(direction === ROTATE_DIRECTION.PITCH);
+                that.setHandleRotateYawVisible(direction === ROTATE_DIRECTION.YAW);
+                that.setHandleRotateRollVisible(direction === ROTATE_DIRECTION.ROLL);
+                that.setHandleStretchVisible(false);
+                that.setHandleScaleVisible(false);
+                that.setHandleDuplicatorVisible(false);
+
+                Overlays.editOverlay(selectedHandle, { 
+                    hasTickMarks: true,
+                    solid: false,
+                    innerRadius: ROTATE_RING_SELECTED_INNER_RADIUS
+                });
+
+                Overlays.editOverlay(rotationDegreesDisplay, { visible: true });
+                Overlays.editOverlay(handleRotateCurrentRing, {
+                    position: rotationCenter,
+                    rotation: worldRotation,
+                    startAt: 0,
+                    endAt: 0,
+                    visible: true,
+                    ignorePickIntersection: false
+                });
+
+                // editOverlays may not have committed rotation changes.
+                // Compute zero position based on where the overlay will be eventually.
+                var initialPick = rayPlaneIntersection(pickRay, rotationCenter, rotationNormal);
+                // In case of a parallel ray, this will be null, which will cause early-out
+                // in the onMove helper.
+                rotationZero = initialPick;
+
+                var rotationCenterToZero = Vec3.subtract(rotationZero, rotationCenter);
+                var rotationCenterToZeroLength = Vec3.length(rotationCenterToZero);
+                rotationDegreesPosition = Vec3.sum(rotationCenter, Vec3.multiply(Vec3.normalize(rotationCenterToZero), 
+                                                   rotationCenterToZeroLength * ROTATE_DISPLAY_DISTANCE_MULTIPLIER));
+                updateRotationDegreesOverlay(0, rotationDegreesPosition);
+                
+                if (debugPickPlaneEnabled) {
+                    that.showDebugPickPlane(rotationCenter, rotationNormal);
+                    that.showDebugPickPlaneHit(initialPick);
+                }
+
+                if (wantDebug) {
+                    print("================== " + getMode() + "(addHandleRotateTool onBegin) <- =======================");
+                }
+            },
+            onEnd: function(event, reason) {
+                var wantDebug = false;
+                if (wantDebug) {
+                    print("================== " + getMode() + "(addHandleRotateTool onEnd) -> =======================");
+                }
+                Overlays.editOverlay(rotationDegreesDisplay, { visible: false, ignorePickIntersection: true });
+                Overlays.editOverlay(selectedHandle, { 
+                    hasTickMarks: false,
+                    solid: true,
+                    innerRadius: ROTATE_RING_IDLE_INNER_RADIUS
+                });
+                Overlays.editOverlay(handleRotateCurrentRing, { visible: false, ignorePickIntersection: true });
+                pushCommandForSelections();
+                if (wantDebug) {
+                    print("================== " + getMode() + "(addHandleRotateTool onEnd) <- =======================");
+                }
+            },
+            onMove: function(event) {
+                if (!rotationZero) {
+                    print("ERROR: entitySelectionTool.addHandleRotateTool.onMove - " +
+                          "Invalid RotationZero Specified (missed rotation target plane?)");
+
+                    // EARLY EXIT
+                    return;
+                }
+                
+                var wantDebug = false;
+                if (wantDebug) {
+                    print("================== "+ getMode() + "(addHandleRotateTool onMove) -> =======================");
+                    Vec3.print("    rotationZero: ", rotationZero);
+                }
+
+                var pickRay = generalComputePickRay(event.x, event.y);
+                var result = rayPlaneIntersection(pickRay, rotationCenter, rotationNormal);
+                if (result) {
+                    var centerToZero = Vec3.subtract(rotationZero, rotationCenter);
+                    var centerToIntersect = Vec3.subtract(result, rotationCenter);
+
+                    if (wantDebug) {
+                        Vec3.print("    RotationNormal:    ", rotationNormal);
+                        Vec3.print("    rotationZero:           ", rotationZero);
+                        Vec3.print("    rotationCenter:         ", rotationCenter);
+                        Vec3.print("    intersect:         ", result);
+                        Vec3.print("    centerToZero:      ", centerToZero);
+                        Vec3.print("    centerToIntersect: ", centerToIntersect);
+                    }
+
+                    // Note: orientedAngle which wants normalized centerToZero and centerToIntersect
+                    //             handles that internally, so it's to pass unnormalized vectors here.
+                    var angleFromZero = Vec3.orientedAngle(centerToZero, centerToIntersect, rotationNormal);        
+                    var snapAngle = ctrlPressed ? ROTATE_CTRL_SNAP_ANGLE : ROTATE_DEFAULT_SNAP_ANGLE;
+                    angleFromZero = Math.floor(angleFromZero / snapAngle) * snapAngle;
+                    var rotationChange = Quat.angleAxis(angleFromZero, rotationNormal);
+                    updateSelectionsRotation(rotationChange, rotationCenter);
+                    updateRotationDegreesOverlay(-angleFromZero, rotationDegreesPosition);
+
+                    if (direction === ROTATE_DIRECTION.YAW) {
+                        angleFromZero *= -1;
+                    }
+
+                    var startAtCurrent = 0;
+                    var endAtCurrent = angleFromZero;
+                    var maxDegrees = 360;
+                    if (angleFromZero < 0) {
+                        startAtCurrent = maxDegrees + angleFromZero;
+                        endAtCurrent = maxDegrees;
+                    }
+                    Overlays.editOverlay(handleRotateCurrentRing, {
+                        startAt: startAtCurrent,
+                        endAt: endAtCurrent
+                    });
+                    
+                    if (debugPickPlaneEnabled) {
+                        that.showDebugPickPlaneHit(result);
+                    }
+                }
+
+                if (wantDebug) {
+                    print("================== "+ getMode() + "(addHandleRotateTool onMove) <- =======================");
+                }
+            }
+        });
+    }    
+
+    addHandleTranslateXZTool(selectionBox, "TRANSLATE_XZ", false);
+    addHandleTranslateXZTool(iconSelectionBox, "TRANSLATE_XZ", false);
+    addHandleTranslateXZTool(handleDuplicator, "DUPLICATE", true);
+
+    addHandleTranslateTool(handleTranslateXCone, "TRANSLATE_X", TRANSLATE_DIRECTION.X);
+    addHandleTranslateTool(handleTranslateXCylinder, "TRANSLATE_X", TRANSLATE_DIRECTION.X);
+    addHandleTranslateTool(handleTranslateYCone, "TRANSLATE_Y", TRANSLATE_DIRECTION.Y);
+    addHandleTranslateTool(handleTranslateYCylinder, "TRANSLATE_Y", TRANSLATE_DIRECTION.Y);
+    addHandleTranslateTool(handleTranslateZCone, "TRANSLATE_Z", TRANSLATE_DIRECTION.Z);
+    addHandleTranslateTool(handleTranslateZCylinder, "TRANSLATE_Z", TRANSLATE_DIRECTION.Z);
+
+    addHandleRotateTool(handleRotatePitchRing, "ROTATE_PITCH", ROTATE_DIRECTION.PITCH);
+    addHandleRotateTool(handleRotateYawRing, "ROTATE_YAW", ROTATE_DIRECTION.YAW);
+    addHandleRotateTool(handleRotateRollRing, "ROTATE_ROLL", ROTATE_DIRECTION.ROLL);
+
+    addHandleStretchTool(handleStretchXCube, "STRETCH_X", STRETCH_DIRECTION.X);
+    addHandleStretchTool(handleStretchYCube, "STRETCH_Y", STRETCH_DIRECTION.Y);
+    addHandleStretchTool(handleStretchZCube, "STRETCH_Z", STRETCH_DIRECTION.Z);
+
+    addHandleScaleTool(handleScaleCube, "SCALE");
+    
+    return that;
+}());
diff --git a/scripts/simplifiedUI/system/libraries/globals.js b/scripts/simplifiedUI/system/libraries/globals.js
new file mode 100644
index 0000000000..b51a905e0a
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/globals.js
@@ -0,0 +1,13 @@
+//
+//  globals.js
+//  examples/libraries
+//
+//  Copyright 2014 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
+//
+
+HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
+
+isInterstitialOverlaysVisible = false;
diff --git a/scripts/simplifiedUI/system/libraries/gridTool.js b/scripts/simplifiedUI/system/libraries/gridTool.js
new file mode 100644
index 0000000000..233ae3a3e4
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/gridTool.js
@@ -0,0 +1,311 @@
+/* global keyUpEventFromUIWindow */
+
+var GRID_CONTROLS_HTML_URL = Script.resolvePath('../html/gridControls.html');
+
+Grid = function() {
+    var that = {};
+    var gridColor = { red: 0, green: 0, blue: 0 };
+    var gridAlpha = 0.6;
+    var origin = { x: 0, y: +MyAvatar.getJointPosition('LeftToeBase').y.toFixed(1) + 0.1, z: 0 };
+    var scale = 500;
+    var minorGridEvery = 1.0;
+    var majorGridEvery = 5;
+    var halfSize = 40;
+
+    var worldSize = 16384;
+
+    var snapToGrid = false;
+
+    var gridOverlay = Overlays.addOverlay("grid", {
+        rotation: Quat.fromPitchYawRollDegrees(90, 0, 0),
+        dimensions: { x: scale, y: scale, z: scale },
+        position: origin,
+        visible: false,
+        drawInFront: false,
+        color: gridColor,
+        alpha: gridAlpha,
+        minorGridEvery: minorGridEvery,
+        majorGridEvery: majorGridEvery,
+        ignorePickIntersection: true
+    });
+
+    that.visible = false;
+    that.enabled = false;
+
+    that.getOrigin = function() {
+        return origin;
+    };
+
+    that.getMinorIncrement = function() {
+        return minorGridEvery;
+    };
+
+    that.setMinorIncrement = function(value) {
+        minorGridEvery = value;
+        updateGrid();
+    };
+
+    that.getMajorIncrement = function() {
+        return majorGridEvery;
+    };
+
+    that.setMajorIncrement = function(value) {
+        majorGridEvery = value;
+        updateGrid();
+    };
+
+    that.getColor = function() {
+        return gridColor;
+    };
+
+    that.setColor = function(value) {
+        gridColor = value;
+        updateGrid();
+    };
+
+    that.getSnapToGrid = function() {
+        return snapToGrid;
+    };
+    that.setSnapToGrid = function(value) {
+        snapToGrid = value;
+        that.emitUpdate();
+    };
+
+    that.setEnabled = function(enabled) {
+        that.enabled = enabled;
+        updateGrid();
+    };
+
+    that.getVisible = function() { 
+        return that.visible; 
+    };
+    that.setVisible = function(visible, noUpdate) {
+        that.visible = visible;
+        updateGrid();
+
+        if (!noUpdate) {
+            that.emitUpdate();
+        }
+    };
+
+    that.snapToSurface = function(position, dimensions, registration) {
+        if (!snapToGrid) {
+            return position;
+        }
+
+        if (dimensions === undefined) {
+            dimensions = { x: 0, y: 0, z: 0 };
+        }
+
+        if (registration === undefined) {
+            registration = { x: 0.5, y: 0.5, z: 0.5 };
+        }
+
+        return {
+            x: position.x,
+            y: origin.y + (registration.y * dimensions.y),
+            z: position.z
+        };
+    };
+
+    that.snapToGrid = function(position, majorOnly, dimensions, registration) {
+        if (!snapToGrid) {
+            return position;
+        }
+
+        if (dimensions === undefined) {
+            dimensions = { x: 0, y: 0, z: 0 };
+        }
+
+        if (registration === undefined) {
+            registration = { x: 0.5, y: 0.5, z: 0.5 };
+        }
+
+        var spacing = majorOnly ? majorGridEvery : minorGridEvery;
+
+        position = Vec3.subtract(position, origin);
+
+        position.x = Math.round(position.x / spacing) * spacing;
+        position.y = Math.round(position.y / spacing) * spacing;
+        position.z = Math.round(position.z / spacing) * spacing;
+
+        return Vec3.sum(Vec3.sum(position, Vec3.multiplyVbyV(registration, dimensions)), origin);
+    };
+
+    that.snapToSpacing = function(delta, majorOnly) {
+        if (!snapToGrid) {
+            return delta;
+        }
+
+        var spacing = majorOnly ? majorGridEvery : minorGridEvery;
+
+        var snappedDelta = {
+            x: Math.round(delta.x / spacing) * spacing,
+            y: Math.round(delta.y / spacing) * spacing,
+            z: Math.round(delta.z / spacing) * spacing
+        };
+
+        return snappedDelta;
+    };
+
+
+    that.setPosition = function(newPosition, noUpdate) {
+        origin = { x: 0, y: newPosition.y, z: 0 };
+        updateGrid();
+
+        if (!noUpdate) {
+            that.emitUpdate();
+        }
+    };
+    
+    that.moveToSelection = function() {
+        var newPosition = SelectionManager.worldPosition;
+        newPosition = Vec3.subtract(newPosition, { x: 0, y: SelectionManager.worldDimensions.y * 0.5, z: 0 });
+        that.setPosition(newPosition);
+    };
+
+    that.emitUpdate = function() {
+        if (that.onUpdate) {
+            that.onUpdate({
+                origin: origin,
+                minorGridEvery: minorGridEvery,
+                majorGridEvery: majorGridEvery,
+                gridSize: halfSize,
+                visible: that.visible,
+                snapToGrid: snapToGrid
+            });
+        }
+    };
+
+    that.update = function(data) {
+        if (data.snapToGrid !== undefined) {
+            snapToGrid = data.snapToGrid;
+        }
+
+        if (data.origin) {
+            var pos = data.origin;
+            pos.x = pos.x === undefined ? origin.x : parseFloat(pos.x);
+            pos.y = pos.y === undefined ? origin.y : parseFloat(pos.y);
+            pos.z = pos.z === undefined ? origin.z : parseFloat(pos.z);
+            that.setPosition(pos, true);
+        }
+
+        if (data.minorGridEvery) {
+            minorGridEvery = data.minorGridEvery;
+        }
+
+        if (data.majorGridEvery) {
+            majorGridEvery = data.majorGridEvery;
+        }
+
+        if (data.gridColor) {
+            gridColor = data.gridColor;
+        }
+
+        if (data.gridSize) {
+            halfSize = data.gridSize;
+        }
+
+        if (data.visible !== undefined) {
+            that.setVisible(data.visible, true);
+        }
+
+        updateGrid(true);
+    };
+
+    function updateGrid(noUpdate) {
+        Overlays.editOverlay(gridOverlay, {
+            position: { x: 0, y: origin.y, z: 0 },
+            visible: that.visible && that.enabled,
+            minorGridEvery: minorGridEvery,
+            majorGridEvery: majorGridEvery,
+            color: gridColor,
+            alpha: gridAlpha
+        });
+
+        if (!noUpdate) {
+            that.emitUpdate();
+        }
+    }
+
+    function cleanup() {
+        Overlays.deleteOverlay(gridOverlay);
+    }
+
+    that.addListener = function(callback) {
+        that.onUpdate = callback;
+    };
+
+    Script.scriptEnding.connect(cleanup);
+    updateGrid();
+
+    that.onUpdate = null;
+
+    return that;
+};
+
+GridTool = function(opts) {
+    var that = {};
+
+    var horizontalGrid = opts.horizontalGrid;
+    var verticalGrid = opts.verticalGrid;
+    var createToolsWindow = opts.createToolsWindow;
+    var shouldUseEditTabletApp = opts.shouldUseEditTabletApp;
+    var listeners = [];
+
+    var webView = null;
+    webView = Tablet.getTablet("com.highfidelity.interface.tablet.system");
+    webView.setVisible = function(value) { };
+
+    horizontalGrid.addListener(function(data) {
+        var dataString = JSON.stringify(data);
+        webView.emitScriptEvent(dataString);
+        createToolsWindow.emitScriptEvent(dataString);
+        if (selectionDisplay) {
+            selectionDisplay.updateHandles();
+        }
+    });
+
+    var webEventReceived = function(data) {
+        try {
+            data = JSON.parse(data);
+        } catch (e) {
+            return;
+        }
+
+        if (data.type === "init") {
+            horizontalGrid.emitUpdate();
+        } else if (data.type === "update") {
+            horizontalGrid.update(data);
+            for (var i = 0; i < listeners.length; i++) {
+                listeners[i] && listeners[i](data);
+            }
+        } else if (data.type === "action") {
+            var action = data.action;
+            if (action === "moveToAvatar") {
+                var position = MyAvatar.getJointPosition("LeftFoot");
+                if (position.x === 0 && position.y === 0 && position.z === 0) {
+                    position = MyAvatar.position;
+                }
+                horizontalGrid.setPosition(position);
+            } else if (action === "moveToSelection") {
+                horizontalGrid.moveToSelection();
+            }
+        } else if (data.type === 'keyUpEvent') {
+            keyUpEventFromUIWindow(data.keyUpEvent);
+        }
+    };
+
+    webView.webEventReceived.connect(webEventReceived);
+    createToolsWindow.webEventReceived.addListener(webEventReceived);
+
+    that.addListener = function(callback) {
+        listeners.push(callback);
+    };
+
+    that.setVisible = function(visible) {
+        webView.setVisible(shouldUseEditTabletApp() && visible);
+    };
+
+    return that;
+};
diff --git a/scripts/simplifiedUI/system/libraries/overlayUtils.js b/scripts/simplifiedUI/system/libraries/overlayUtils.js
new file mode 100644
index 0000000000..366cdbcc97
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/overlayUtils.js
@@ -0,0 +1,74 @@
+//
+//  overlayUtils.js
+//  examples/libraries
+//
+//  Copyright 2015 High Fidelity, Inc.
+//
+
+
+//
+//  DEPRECATION WARNING: Will be deprecated soon in favor of FloatingUIPanel.
+//
+//  OverlayGroup provides a way to create composite overlays and control their
+//  position relative to a settable rootPosition and rootRotation.
+//
+OverlayGroup = function(opts) {
+    var that = {};
+
+    var overlays = {};
+
+    var rootPosition = opts.position || { x: 0, y: 0, z: 0 };
+    var rootRotation = opts.rotation || Quat.IDENTITY;
+    var visible = opts.visible == true;
+
+    function updateOverlays() {
+        for (overlayID in overlays) {
+            var overlay = overlays[overlayID];
+            var newPosition = Vec3.multiplyQbyV(rootRotation, overlay.position);
+            newPosition = Vec3.sum(rootPosition, newPosition);
+            Overlays.editOverlay(overlayID, {
+                visible: visible,
+                position: newPosition,
+                rotation: Quat.multiply(rootRotation, overlay.rotation),
+            });
+        };
+    }
+
+    that.createOverlay = function(type, properties) {
+        properties.position = properties.position || { x: 0, y: 0, z: 0 };
+        properties.rotation = properties.rotation || Quat.IDENTITY;
+
+        var overlay = Overlays.addOverlay(type, properties);
+
+        overlays[overlay] = {
+            position: properties.position,
+            rotation: properties.rotation,
+        };
+
+        updateOverlays();
+
+        return overlay;
+    }
+
+    that.setProperties = function(properties) {
+        if (properties.position !== undefined) {
+            rootPosition = properties.position;
+        }
+        if (properties.rotation !== undefined) {
+            rootRotation = properties.rotation;
+        }
+        if (properties.visible !== undefined) {
+            visible = properties.visible;
+        }
+        updateOverlays();
+    };
+
+    that.destroy = function() {
+        for (var overlay in overlays) {
+            Overlays.deleteOverlay(overlay);
+        }
+        overlays = {};
+    }
+
+    return that;
+};
diff --git a/scripts/simplifiedUI/system/libraries/pointersUtils.js b/scripts/simplifiedUI/system/libraries/pointersUtils.js
new file mode 100644
index 0000000000..ec9a08b7eb
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/pointersUtils.js
@@ -0,0 +1,204 @@
+"use strict";
+
+//  pointerUtils.js
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+
+/* jslint bitwise: true */
+
+/* global Script, Pointers,
+   DEFAULT_SEARCH_SPHERE_DISTANCE, COLORS_GRAB_SEARCHING_HALF_SQUEEZE, COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
+   COLORS_GRAB_DISTANCE_HOLD, TRIGGER_ON_VALUE,
+   Pointer:true, PointerManager:true
+*/
+
+Script.include("/~/system/libraries/controllerDispatcherUtils.js");
+Pointer = function(hudLayer, pickType, pointerData) {
+    this.SEARCH_SPHERE_SIZE = 0.0132;
+    this.dim = {x: this.SEARCH_SPHERE_SIZE, y: this.SEARCH_SPHERE_SIZE, z: this.SEARCH_SPHERE_SIZE};
+    this.halfPath = {
+        type: "line3d",
+        color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
+        visible: true,
+        alpha: 1,
+        solid: true,
+        glow: 1.0,
+        ignoreRayIntersection: true, // always ignore this
+        drawInFront: !hudLayer, // Even when burried inside of something, show it.
+        drawHUDLayer: hudLayer,
+    };
+    this.halfEnd = {
+        type: "sphere",
+        dimensions: this.dim,
+        solid: true,
+        color: COLORS_GRAB_SEARCHING_HALF_SQUEEZE,
+        alpha: 0.9,
+        ignoreRayIntersection: true,
+        drawInFront: !hudLayer, // Even when burried inside of something, show it.
+        drawHUDLayer: hudLayer,
+        visible: true
+    };
+    this.fullPath = {
+        type: "line3d",
+        color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
+        visible: true,
+        alpha: 1,
+        solid: true,
+        glow: 1.0,
+        ignoreRayIntersection: true, // always ignore this
+        drawInFront: !hudLayer, // Even when burried inside of something, show it.
+        drawHUDLayer: hudLayer,
+    };
+    this.fullEnd = {
+        type: "sphere",
+        dimensions: this.dim,
+        solid: true,
+        color: COLORS_GRAB_SEARCHING_FULL_SQUEEZE,
+        alpha: 0.9,
+        ignoreRayIntersection: true,
+        drawInFront: !hudLayer, // Even when burried inside of something, show it.
+        drawHUDLayer: hudLayer,
+        visible: true
+    };
+    this.holdPath = {
+        type: "line3d",
+        color: COLORS_GRAB_DISTANCE_HOLD,
+        visible: true,
+        alpha: 1,
+        solid: true,
+        glow: 1.0,
+        ignoreRayIntersection: true, // always ignore this
+        drawInFront: !hudLayer, // Even when burried inside of something, show it.
+        drawHUDLayer: hudLayer,
+    };
+
+    this.renderStates = [
+        {name: "half", path: this.halfPath, end: this.halfEnd},
+        {name: "full", path: this.fullPath, end: this.fullEnd},
+        {name: "hold", path: this.holdPath}
+    ];
+
+    this.defaultRenderStates = [
+        {name: "half", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: this.halfPath},
+        {name: "full", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: this.fullPath},
+        {name: "hold", distance: DEFAULT_SEARCH_SPHERE_DISTANCE, path: this.holdPath}
+    ];
+
+
+    this.pointerID = null;
+    this.visible = false;
+    this.locked = false;
+    this.allwaysOn = false;
+    this.hand = pointerData.hand;
+    delete pointerData.hand;
+
+    function createPointer(pickType, pointerData) {
+        var pointerID = Pointers.createPointer(pickType, pointerData);
+        Pointers.setRenderState(pointerID, "");
+        Pointers.enablePointer(pointerID);
+        return pointerID;
+    }
+
+    this.enable = function() {
+        Pointers.enablePointer(this.pointerID);
+    };
+
+    this.disable = function() {
+        Pointers.disablePointer(this.pointerID);
+    };
+
+    this.removePointer = function() {
+        Pointers.removePointer(this.pointerID);
+    };
+
+    this.makeVisible = function() {
+        this.visible = true;
+    };
+
+    this.makeInvisible = function() {
+        this.visible = false;
+    };
+
+    this.lockEnd = function(lockData) {
+        if (lockData !== undefined) {
+            if (this.visible && !this.locked && lockData.targetID !== null) {
+                var targetID = lockData.targetID;
+                var targetIsOverlay = lockData.isOverlay;
+                if (lockData.offset === undefined) {
+                    Pointers.setLockEndUUID(this.pointerID, targetID, targetIsOverlay);
+                } else {
+                    Pointers.setLockEndUUID(this.pointerID, targetID, targetIsOverlay, lockData.offset);
+                }
+                this.locked = targetID;
+            }
+        } else if (this.locked) {
+            Pointers.setLockEndUUID(this.pointerID, null, false);
+            this.locked = false;
+        }
+    };
+
+    this.updateRenderState = function(triggerClicks, triggerValues) {
+        var mode = "";
+        if (this.visible) {
+            if (this.locked) {
+                mode = "hold";
+            } else if (triggerClicks[this.hand]) {
+                mode = "full";
+            } else if (triggerValues[this.hand] > TRIGGER_ON_VALUE || this.alwaysOn) {
+                mode = "half";
+            }
+        }
+
+        Pointers.setRenderState(this.pointerID, mode);
+    };
+    pointerData.renderStates = this.renderStates;
+    pointerData.defaultRenderStates = this.defaultRenderStates;
+    this.pointerID = createPointer(pickType, pointerData);
+};
+
+
+PointerManager = function() {
+    this.pointers = [];
+
+    this.createPointer = function(hudLayer, pickType, pointerData) {
+        var pointer = new Pointer(hudLayer, pickType, pointerData);
+        this.pointers.push(pointer);
+        return pointer.pointerID;
+    };
+
+    this.makePointerVisible = function(laserParams) {
+        var index = laserParams.hand;
+        if (index < this.pointers.length && index >= 0) {
+            this.pointers[index].makeVisible();
+            this.pointers[index].alwaysOn = laserParams.alwaysOn;
+        }
+    };
+
+    this.makePointerInvisible = function(laserParams) {
+        var index = laserParams.hand;
+        if (index < this.pointers.length && index >= 0) {
+            this.pointers[index].makeInvisible();
+        }
+    };
+
+    this.lockPointerEnd = function(laserParams, lockData) {
+        var index = laserParams.hand;
+        if (index < this.pointers.length && index >= 0) {
+            this.pointers[index].lockEnd(lockData);
+        }
+    };
+
+    this.updatePointersRenderState = function(triggerClicks, triggerValues) {
+        for (var index = 0; index < this.pointers.length; index++) {
+            this.pointers[index].updateRenderState(triggerClicks, triggerValues);
+        }
+    };
+
+    this.removePointers = function() {
+        for (var index = 0; index < this.pointers.length; index++) {
+            this.pointers[index].removePointer();
+        }
+        this.pointers = [];
+    };
+};
diff --git a/scripts/simplifiedUI/system/libraries/progressDialog.js b/scripts/simplifiedUI/system/libraries/progressDialog.js
new file mode 100644
index 0000000000..4cb2644004
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/progressDialog.js
@@ -0,0 +1,147 @@
+//
+//  progressDialog.js
+//  examples/libraries
+//
+//  Copyright 2014 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
+//
+
+var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/";
+var toolIconUrl = HIFI_PUBLIC_BUCKET + "images/tools/";
+
+progressDialog = (function () {
+    var that = {},
+        progressBackground,
+        progressMessage,
+        cancelButton,
+        displayed = false,
+        backgroundWidth = 300,
+        backgroundHeight = 100,
+        messageHeight = 32,
+        cancelWidth = 70,
+        cancelHeight = 32,
+        textColor = { red: 255, green: 255, blue: 255 },
+        textBackground = { red: 52, green: 52, blue: 52 },
+        backgroundUrl = toolIconUrl + "progress-background.svg",
+        windowDimensions;
+
+    progressBackground = Overlays.addOverlay("image", {
+        width: backgroundWidth,
+        height: backgroundHeight,
+        imageURL: backgroundUrl,
+        alpha: 0.9,
+        visible: false
+    });
+
+    progressMessage = Overlays.addOverlay("text", {
+        width: backgroundWidth - 40,
+        height: messageHeight,
+        text: "",
+        textColor: textColor,
+        backgroundColor: textBackground,
+        alpha: 0.9,
+        backgroundAlpha: 0.9,
+        visible: false
+    });
+
+    cancelButton = Overlays.addOverlay("text", {
+        width: cancelWidth,
+        height: cancelHeight,
+        text: "Cancel",
+        textColor: textColor,
+        backgroundColor: textBackground,
+        alpha: 0.9,
+        backgroundAlpha: 0.9,
+        visible: false
+    });
+
+    function move() {
+        var progressX,
+            progressY;
+
+        if (displayed) {
+
+            if (windowDimensions.x === Window.innerWidth && windowDimensions.y === Window.innerHeight) {
+                return;
+            }
+            windowDimensions.x = Window.innerWidth;
+            windowDimensions.y = Window.innerHeight;
+
+            progressX = (windowDimensions.x - backgroundWidth) / 2;  // Center.
+            progressY = windowDimensions.y / 2 - backgroundHeight;  // A little up from center.
+
+            Overlays.editOverlay(progressBackground, { x: progressX, y: progressY });
+            Overlays.editOverlay(progressMessage, { x: progressX + 20, y: progressY + 15 });
+            Overlays.editOverlay(cancelButton, {
+                x: progressX + backgroundWidth - cancelWidth - 20,
+                y: progressY + backgroundHeight - cancelHeight - 15
+            });
+        }
+    }
+    that.move = move;
+
+    that.onCancel = undefined;
+
+    function open(message) {
+        if (!displayed) {
+            windowDimensions = { x: 0, y : 0 };
+            displayed = true;
+            move();
+            Overlays.editOverlay(progressBackground, { visible: true });
+            Overlays.editOverlay(progressMessage, { visible: true, text: message });
+            Overlays.editOverlay(cancelButton, { visible: true });
+        } else {
+            throw new Error("open() called on progressDialog when already open");
+        }
+    }
+    that.open = open;
+
+    function isOpen() {
+        return displayed;
+    }
+    that.isOpen = isOpen;
+
+    function update(message) {
+        if (displayed) {
+            Overlays.editOverlay(progressMessage, { text: message });
+        } else {
+            throw new Error("update() called on progressDialog when not open");
+        }
+    }
+    that.update = update;
+
+    function close() {
+        if (displayed) {
+            Overlays.editOverlay(cancelButton, { visible: false });
+            Overlays.editOverlay(progressMessage, { visible: false });
+            Overlays.editOverlay(progressBackground, { visible: false });
+            displayed = false;
+        } else {
+            throw new Error("close() called on progressDialog when not open");
+        }
+    }
+    that.close = close;
+
+    function mousePressEvent(event) {
+        if (Overlays.getOverlayAtPoint({ x: event.x, y: event.y }) === cancelButton) {
+            if (typeof this.onCancel === "function") {
+                close();
+                this.onCancel();
+            }
+            return true;
+        }
+        return false;
+    }
+    that.mousePressEvent = mousePressEvent;
+
+    function cleanup() {
+        Overlays.deleteOverlay(cancelButton);
+        Overlays.deleteOverlay(progressMessage);
+        Overlays.deleteOverlay(progressBackground);
+    }
+    that.cleanup = cleanup;
+
+    return that;
+}());
diff --git a/scripts/simplifiedUI/system/libraries/soundArray.js b/scripts/simplifiedUI/system/libraries/soundArray.js
new file mode 100644
index 0000000000..7e5da11948
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/soundArray.js
@@ -0,0 +1,42 @@
+/**
+ * An array for sounds, allows you to randomly play a sound
+ * taken from the removed editVoxels.js
+ */
+SoundArray = function(audioOptions, autoUpdateAudioPosition) {
+    this.audioOptions = audioOptions !== undefined ? audioOptions : {};
+    this.autoUpdateAudioPosition = autoUpdateAudioPosition !== undefined ? autoUpdateAudioPosition : false;
+    if (this.audioOptions.position === undefined) {
+        this.audioOptions.position = Vec3.sum(MyAvatar.position, { x: 0, y: 1, z: 0});
+    }
+    if (this.audioOptions.volume === undefined) {
+        this.audioOptions.volume = 1.0;
+    }
+    this.sounds = new Array();
+    this.addSound = function (soundURL) {
+        this.sounds[this.sounds.length] = SoundCache.getSound(soundURL);
+    };
+    this.play = function (index) {
+        if (0 <= index && index < this.sounds.length) {
+            if (this.autoUpdateAudioPosition) {
+                this.updateAudioPosition();
+            }
+            if (this.sounds[index].downloaded) {
+                Audio.playSound(this.sounds[index], this.audioOptions);
+            }
+        } else {
+            print("[ERROR] libraries/soundArray.js:play() : Index " + index + " out of range.");
+        }
+    };
+    this.playRandom = function () {
+        if (this.sounds.length > 0) {
+            this.play(Math.floor(Math.random() * this.sounds.length));
+        } else {
+            print("[ERROR] libraries/soundArray.js:playRandom() : Array is empty.");
+        }
+    };
+    this.updateAudioPosition = function() {
+        var position = MyAvatar.position;
+        var forwardVector = Quat.getForward(MyAvatar.orientation);
+        this.audioOptions.position = Vec3.sum(position, forwardVector);
+    };
+};
diff --git a/scripts/simplifiedUI/system/libraries/stringHelpers.js b/scripts/simplifiedUI/system/libraries/stringHelpers.js
new file mode 100644
index 0000000000..81f990ef7f
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/stringHelpers.js
@@ -0,0 +1,476 @@
+if (typeof String.prototype.fileName !== "function") {
+    String.prototype.fileName = function() {
+        return this.replace(/^(.*[\/\\])*/, "");
+    };
+}
+
+if (typeof String.prototype.fileBase !== "function") {
+    String.prototype.fileBase = function() {
+        var filename = this.fileName();
+        return filename.slice(0, filename.indexOf("."));
+    };
+}
+
+if (typeof String.prototype.fileType !== "function") {
+    String.prototype.fileType = function() {
+        return this.slice(this.lastIndexOf(".") + 1);
+    };
+}
+
+if (typeof String.prototype.path !== "function") {
+    String.prototype.path = function() {
+        return this.replace(/[\\\/][^\\\/]*$/, "");
+    };
+}
+
+if (typeof String.prototype.regExpEscape !== "function") {
+    String.prototype.regExpEscape = function() {
+        return this.replace(/([$\^.+*?|\\\/{}()\[\]])/g, '\\$1');
+    };
+}
+
+if (typeof String.prototype.toArrayBuffer !== "function") {
+    String.prototype.toArrayBuffer = function() {
+        var length,
+            buffer,
+            view,
+            charCode,
+            charCodes,
+            i;
+
+        charCodes = [];
+
+        length = this.length;
+        for (i = 0; i < length; i += 1) {
+            charCode = this.charCodeAt(i);
+            if (charCode <= 255) {
+                charCodes.push(charCode);
+            } else {
+                charCodes.push(charCode / 256);
+                charCodes.push(charCode % 256);
+            }
+        }
+
+        length = charCodes.length;
+        buffer = new ArrayBuffer(length);
+        view = new Uint8Array(buffer);
+        for (i = 0; i < length; i += 1) {
+            view[i] = charCodes[i];
+        }
+
+        return buffer;
+    };
+}
+// Copyright Mathias Bynens <https://mathiasbynens.be/>
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+/*! https://mths.be/includes v1.0.0 by @mathias */
+if (!String.prototype.includes) {
+    (function() {
+        'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
+        var toString = {}.toString;
+        var defineProperty = (function() {
+            // IE 8 only supports `Object.defineProperty` on DOM elements
+            try {
+                var object = {};
+                var $defineProperty = Object.defineProperty;
+                var result = $defineProperty(object, object, object) && $defineProperty;
+            } catch (error) {}
+            return result;
+        }());
+        var indexOf = ''.indexOf;
+        var includes = function(search) {
+            if (this == null) {
+                throw TypeError();
+            }
+            var string = String(this);
+            if (search && toString.call(search) == '[object RegExp]') {
+                throw TypeError();
+            }
+            var stringLength = string.length;
+            var searchString = String(search);
+            var searchLength = searchString.length;
+            var position = arguments.length > 1 ? arguments[1] : undefined;
+            // `ToInteger`
+            var pos = position ? Number(position) : 0;
+            if (pos != pos) { // better `isNaN`
+                pos = 0;
+            }
+            var start = Math.min(Math.max(pos, 0), stringLength);
+            // Avoid the `indexOf` call if no match is possible
+            if (searchLength + start > stringLength) {
+                return false;
+            }
+            return indexOf.call(string, searchString, pos) != -1;
+        };
+        if (defineProperty) {
+            defineProperty(String.prototype, 'includes', {
+                'value': includes,
+                'configurable': true,
+                'writable': true
+            });
+        } else {
+            String.prototype.includes = includes;
+        }
+    }());
+}
+
+/*! https://mths.be/startswith v0.2.0 by @mathias */
+if (!String.prototype.startsWith) {
+    (function() {
+        'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
+        var defineProperty = (function() {
+            // IE 8 only supports `Object.defineProperty` on DOM elements
+            try {
+                var object = {};
+                var $defineProperty = Object.defineProperty;
+                var result = $defineProperty(object, object, object) && $defineProperty;
+            } catch (error) {}
+            return result;
+        }());
+        var toString = {}.toString;
+        var startsWith = function(search) {
+            if (this == null) {
+                throw TypeError();
+            }
+            var string = String(this);
+            if (search && toString.call(search) == '[object RegExp]') {
+                throw TypeError();
+            }
+            var stringLength = string.length;
+            var searchString = String(search);
+            var searchLength = searchString.length;
+            var position = arguments.length > 1 ? arguments[1] : undefined;
+            // `ToInteger`
+            var pos = position ? Number(position) : 0;
+            if (pos != pos) { // better `isNaN`
+                pos = 0;
+            }
+            var start = Math.min(Math.max(pos, 0), stringLength);
+            // Avoid the `indexOf` call if no match is possible
+            if (searchLength + start > stringLength) {
+                return false;
+            }
+            var index = -1;
+            while (++index < searchLength) {
+                if (string.charCodeAt(start + index) != searchString.charCodeAt(index)) {
+                    return false;
+                }
+            }
+            return true;
+        };
+        if (defineProperty) {
+            defineProperty(String.prototype, 'startsWith', {
+                'value': startsWith,
+                'configurable': true,
+                'writable': true
+            });
+        } else {
+            String.prototype.startsWith = startsWith;
+        }
+    }());
+}
+if (!String.prototype.endsWith) {
+    (function() {
+        'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
+        var defineProperty = (function() {
+            // IE 8 only supports `Object.defineProperty` on DOM elements
+            try {
+                var object = {};
+                var $defineProperty = Object.defineProperty;
+                var result = $defineProperty(object, object, object) && $defineProperty;
+            } catch (error) {}
+            return result;
+        }());
+        var toString = {}.toString;
+        var endsWith = function(search) {
+            if (this == null) {
+                throw TypeError();
+            }
+            var string = String(this);
+            if (search && toString.call(search) == '[object RegExp]') {
+                throw TypeError();
+            }
+            var stringLength = string.length;
+            var searchString = String(search);
+            var searchLength = searchString.length;
+            var pos = stringLength;
+            if (arguments.length > 1) {
+                var position = arguments[1];
+                if (position !== undefined) {
+                    // `ToInteger`
+                    pos = position ? Number(position) : 0;
+                    if (pos != pos) { // better `isNaN`
+                        pos = 0;
+                    }
+                }
+            }
+            var end = Math.min(Math.max(pos, 0), stringLength);
+            var start = end - searchLength;
+            if (start < 0) {
+                return false;
+            }
+            var index = -1;
+            while (++index < searchLength) {
+                if (string.charCodeAt(start + index) != searchString.charCodeAt(index)) {
+                    return false;
+                }
+            }
+            return true;
+        };
+        if (defineProperty) {
+            defineProperty(String.prototype, 'endsWith', {
+                'value': endsWith,
+                'configurable': true,
+                'writable': true
+            });
+        } else {
+            String.prototype.endsWith = endsWith;
+        }
+    }());
+}
+
+/*! https://mths.be/repeat v0.2.0 by @mathias */
+if (!String.prototype.repeat) {
+    (function() {
+        'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
+        var defineProperty = (function() {
+            // IE 8 only supports `Object.defineProperty` on DOM elements
+            try {
+                var object = {};
+                var $defineProperty = Object.defineProperty;
+                var result = $defineProperty(object, object, object) && $defineProperty;
+            } catch (error) {}
+            return result;
+        }());
+        var repeat = function(count) {
+            if (this == null) {
+                throw TypeError();
+            }
+            var string = String(this);
+            // `ToInteger`
+            var n = count ? Number(count) : 0;
+            if (n != n) { // better `isNaN`
+                n = 0;
+            }
+            // Account for out-of-bounds indices
+            if (n < 0 || n == Infinity) {
+                throw RangeError();
+            }
+            var result = '';
+            while (n) {
+                if (n % 2 == 1) {
+                    result += string;
+                }
+                if (n > 1) {
+                    string += string;
+                }
+                n >>= 1;
+            }
+            return result;
+        };
+        if (defineProperty) {
+            defineProperty(String.prototype, 'repeat', {
+                'value': repeat,
+                'configurable': true,
+                'writable': true
+            });
+        } else {
+            String.prototype.repeat = repeat;
+        }
+    }());
+}
+
+if (!String.prototype.at) {
+    (function() {
+        'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
+        var defineProperty = (function() {
+            // IE 8 only supports `Object.defineProperty` on DOM elements.
+            try {
+                var object = {};
+                var $defineProperty = Object.defineProperty;
+                var result = $defineProperty(object, object, object) && $defineProperty;
+            } catch (exception) {}
+            return result;
+        }());
+        var at = function(position) {
+            if (this == null) {
+                throw TypeError();
+            }
+            var string = String(this);
+            var size = string.length;
+            // `ToInteger`
+            var index = position ? Number(position) : 0;
+            if (index != index) { // better `isNaN`
+                index = 0;
+            }
+            // Account for out-of-bounds indices
+            // The odd lower bound is because the ToInteger operation is
+            // going to round `n` to `0` for `-1 < n <= 0`.
+            if (index <= -1 || index >= size) {
+                return '';
+            }
+            // Second half of `ToInteger`
+            index = index | 0;
+            // Get the first code unit and code unit value
+            var cuFirst = string.charCodeAt(index);
+            var cuSecond;
+            var nextIndex = index + 1;
+            var len = 1;
+            if ( // Check if it’s the start of a surrogate pair.
+                cuFirst >= 0xD800 && cuFirst <= 0xDBFF && // high surrogate
+                size > nextIndex // there is a next code unit
+            ) {
+                cuSecond = string.charCodeAt(nextIndex);
+                if (cuSecond >= 0xDC00 && cuSecond <= 0xDFFF) { // low surrogate
+                    len = 2;
+                }
+            }
+            return string.slice(index, index + len);
+        };
+        if (defineProperty) {
+            defineProperty(String.prototype, 'at', {
+                'value': at,
+                'configurable': true,
+                'writable': true
+            });
+        } else {
+            String.prototype.at = at;
+        }
+    }());
+}
+
+/*! https://mths.be/codepointat v0.2.0 by @mathias */
+if (!String.prototype.codePointAt) {
+    (function() {
+        'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
+        var defineProperty = (function() {
+            // IE 8 only supports `Object.defineProperty` on DOM elements
+            try {
+                var object = {};
+                var $defineProperty = Object.defineProperty;
+                var result = $defineProperty(object, object, object) && $defineProperty;
+            } catch (error) {}
+            return result;
+        }());
+        var codePointAt = function(position) {
+            if (this == null) {
+                throw TypeError();
+            }
+            var string = String(this);
+            var size = string.length;
+            // `ToInteger`
+            var index = position ? Number(position) : 0;
+            if (index != index) { // better `isNaN`
+                index = 0;
+            }
+            // Account for out-of-bounds indices:
+            if (index < 0 || index >= size) {
+                return undefined;
+            }
+            // Get the first code unit
+            var first = string.charCodeAt(index);
+            var second;
+            if ( // check if it’s the start of a surrogate pair
+                first >= 0xD800 && first <= 0xDBFF && // high surrogate
+                size > index + 1 // there is a next code unit
+            ) {
+                second = string.charCodeAt(index + 1);
+                if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate
+                    // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
+                    return (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
+                }
+            }
+            return first;
+        };
+        if (defineProperty) {
+            defineProperty(String.prototype, 'codePointAt', {
+                'value': codePointAt,
+                'configurable': true,
+                'writable': true
+            });
+        } else {
+            String.prototype.codePointAt = codePointAt;
+        }
+    }());
+}
+
+/*! https://mths.be/fromcodepoint v0.2.1 by @mathias */
+if (!String.fromCodePoint) {
+    (function() {
+        var defineProperty = (function() {
+            // IE 8 only supports `Object.defineProperty` on DOM elements
+            try {
+                var object = {};
+                var $defineProperty = Object.defineProperty;
+                var result = $defineProperty(object, object, object) && $defineProperty;
+            } catch (error) {}
+            return result;
+        }());
+        var stringFromCharCode = String.fromCharCode;
+        var floor = Math.floor;
+        var fromCodePoint = function(_) {
+            var MAX_SIZE = 0x4000;
+            var codeUnits = [];
+            var highSurrogate;
+            var lowSurrogate;
+            var index = -1;
+            var length = arguments.length;
+            if (!length) {
+                return '';
+            }
+            var result = '';
+            while (++index < length) {
+                var codePoint = Number(arguments[index]);
+                if (!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
+                    codePoint < 0 || // not a valid Unicode code point
+                    codePoint > 0x10FFFF || // not a valid Unicode code point
+                    floor(codePoint) != codePoint // not an integer
+                ) {
+                    throw RangeError('Invalid code point: ' + codePoint);
+                }
+                if (codePoint <= 0xFFFF) { // BMP code point
+                    codeUnits.push(codePoint);
+                } else { // Astral code point; split in surrogate halves
+                    // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
+                    codePoint -= 0x10000;
+                    highSurrogate = (codePoint >> 10) + 0xD800;
+                    lowSurrogate = (codePoint % 0x400) + 0xDC00;
+                    codeUnits.push(highSurrogate, lowSurrogate);
+                }
+                if (index + 1 == length || codeUnits.length > MAX_SIZE) {
+                    result += stringFromCharCode.apply(null, codeUnits);
+                    codeUnits.length = 0;
+                }
+            }
+            return result;
+        };
+        if (defineProperty) {
+            defineProperty(String, 'fromCodePoint', {
+                'value': fromCodePoint,
+                'configurable': true,
+                'writable': true
+            });
+        } else {
+            String.fromCodePoint = fromCodePoint;
+        }
+    }());
+}
\ No newline at end of file
diff --git a/scripts/simplifiedUI/system/libraries/toolBars.js b/scripts/simplifiedUI/system/libraries/toolBars.js
new file mode 100644
index 0000000000..058910940b
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/toolBars.js
@@ -0,0 +1,493 @@
+//
+//  toolBars.js
+//  examples
+//
+//  Created by Clément Brisset on 5/7/14.
+//  Persistable drag position by HRS 6/11/15.
+//  Copyright 2014 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
+//
+
+Overlay2D = function(properties, overlay) { // overlay is an optional variable
+    if (!(typeof(properties) === 'undefined')) {
+        if(typeof(overlay) === 'undefined') {
+            overlay = Overlays.addOverlay("image", properties);
+        } else {
+            Overlays.editOverlay(overlay, properties);
+        }
+    }
+    
+    this.overlay = function() {
+        return overlay;
+    }
+    this.x = function() {
+        return properties.x;
+    }
+    this.y = function() {
+        return properties.y;
+    }
+    this.width = function() {
+        return properties.width;
+    }
+    this.height = function() {
+        return properties.height;
+    }
+    this.alpha = function() {
+        return properties.alpha;
+    }
+    this.visible = function() {
+        return properties.visible;
+    }
+    
+    
+    this.move = function(x, y) {
+        properties.x = x;
+        properties.y = y;
+        Overlays.editOverlay(overlay, { x: x, y: y });
+    }
+    this.resize = function(width, height) {
+        properties.width = width;
+        properties.height = height;
+        Overlays.editOverlay(overlay, { width: width, height: height });
+    }
+    this.setAlpha = function(alpha) {
+        properties.alpha = alpha;
+        Overlays.editOverlay(overlay, { alpha: alpha });
+    }
+    this.setImageURL = function(imageURL) {
+        properties.imageURL = imageURL;
+        Overlays.editOverlay(overlay, { imageURL: imageURL });
+    }
+    this.show = function(doShow) {
+        properties.visible = doShow;
+        Overlays.editOverlay(overlay, { visible: doShow });
+    }
+    
+    this.clicked = function(clickedOverlay) {
+        return overlay === clickedOverlay;
+    }
+    
+    this.cleanup = function() {
+        Overlays.deleteOverlay(overlay);
+    }
+}
+
+
+Tool = function(properties, selectable, selected) { // selectable and selected are optional variables.
+    Overlay2D.call(this, properties);
+    
+    if(typeof(selectable)==='undefined') {
+        selectable = false;
+        if(typeof(selected)==='undefined') {
+            selected = false;
+            
+        }
+    }
+    
+    this.selectable = function() {
+        return selectable;
+    }
+    
+    this.selected = function() {
+        return selected;
+    }
+    this.select = function(doSelect) {
+        if (!selectable) {
+            return;
+        }
+
+        selected = doSelect;
+        properties.subImage.y = (selected ? 0 : 1) * properties.subImage.height;
+        Overlays.editOverlay(this.overlay(), { subImage: properties.subImage });
+    }
+    this.toggle = function() {
+        if (!selectable) {
+            return;
+        }
+        selected = !selected;
+        properties.subImage.y = (selected ? 0 : 1) * properties.subImage.height;
+        Overlays.editOverlay(this.overlay(), { subImage: properties.subImage });
+        
+        return selected;
+    }
+    
+    this.select(selected);
+
+    this.isButtonDown = false;
+    this.buttonDown = function (down) {
+        if (down !== this.isButtonDown) {
+            properties.subImage.y = (down ? 0 : 1) * properties.subImage.height;
+            Overlays.editOverlay(this.overlay(), { subImage: properties.subImage });
+            this.isButtonDown = down;
+        }
+    }
+
+    this.baseClicked = this.clicked;
+    this.clicked = function(clickedOverlay, update) {
+        if (this.baseClicked(clickedOverlay)) {
+            if (update) {
+                if (selectable) {
+                    this.toggle();
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+}
+Tool.prototype = new Overlay2D;
+Tool.IMAGE_HEIGHT = 50;
+Tool.IMAGE_WIDTH = 50;
+
+ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPositionFunction, optionalOffset) {
+    this.tools = new Array();
+    this.x = x;
+    this.y = y;
+    this.offset = optionalOffset ? optionalOffset : { x: 0, y: 0 };
+    this.width = 0;
+    this.height = 0;
+    this.backAlpha = 1.0;
+    this.back = Overlays.addOverlay("rectangle", {
+                    color: { red: 255, green: 255, blue: 255 },
+                    x: this.x,
+                    y: this.y,
+                    radius: 4,
+                    width: this.width,
+                    height: this.height,
+                    alpha: this.backAlpha,
+                    visible: false
+                });
+    this.spacing = [];
+    this.onMove = null;
+    
+    this.addTool = function(properties, selectable, selected) {
+        if (direction == ToolBar.HORIZONTAL) {
+            properties.x = this.x + this.width;
+            properties.y = this.y;
+            this.width += properties.width + ToolBar.SPACING;
+            this.height = Math.max(properties.height, this.height);
+        } else {
+            properties.x = this.x;
+            properties.y = this.y + this.height;
+            this.width = Math.max(properties.width, this.width);
+            this.height += properties.height + ToolBar.SPACING;
+        }
+        
+        if (this.back != null) {
+            Overlays.editOverlay(this.back, {
+                                 width: this.width +
+                                 ((direction == ToolBar.HORIZONTAL) ? 1 : 2) * ToolBar.SPACING,
+                                 height: this.height +
+                                 ((direction == ToolBar.VERTICAL) ? 1 : 2) * ToolBar.SPACING,
+            });
+        }
+        
+        this.tools.push(new Tool(properties, selectable, selected));
+        return ((this.tools.length) - 1);
+    }
+    
+    this.addSpacing = function(size) {
+        if (direction == ToolBar.HORIZONTAL) {
+            this.width += size;
+        } else {
+            this.height += size;
+        }
+        this.spacing[this.tools.length] = size;
+        
+        return (this.tools.length);
+    }
+    
+    this.changeSpacing = function(size, id) {
+        if (this.spacing[id] === null) {
+            this.spacing[id] = 0;
+        }
+        var diff = size - this.spacing[id];
+        this.spacing[id] = size;
+        
+        var dx = (direction == ToolBar.HORIZONTAL) ? diff : 0;
+        var dy = (direction == ToolBar.VERTICAL) ? diff : 0;
+        this.width += dx;
+        this.height += dy;
+        
+        for(i = id; i < this.tools.length; i++) {
+            this.tools[i].move(this.tools[i].x() + dx,
+                               this.tools[i].y() + dy);
+        }
+        if (this.back != null) {
+            Overlays.editOverlay(this.back, {
+                                 width: this.width +
+                                 ((direction == ToolBar.HORIZONTAL) ? 1 : 2) * ToolBar.SPACING,
+                                 height: this.height +
+                                 ((direction == ToolBar.VERTICAL) ? 1 : 2) * ToolBar.SPACING,
+                                 });
+        }
+    }
+
+    this.removeLastTool = function() {
+        this.tools.pop().cleanup();
+
+        if (direction == ToolBar.HORIZONTAL) {
+            this.width -= Tool.IMAGE_WIDTH + ToolBar.SPACING;
+        } else {
+            this.height -= Tool.IMAGE_HEIGHT + ToolBar.SPACING;
+        }
+        if (this.back != null) {
+            Overlays.editOverlay(this.back, {
+                width: this.width + 2 * ToolBar.SPACING,
+                height: this.height + 2 * ToolBar.SPACING
+            });
+        }
+    }
+    
+    this.move = function (x, y) {
+        var dx = x - this.x;
+        var dy = y - this.y;
+        this.x = x;
+        this.y = y;
+        for(var tool in this.tools) {
+            this.tools[tool].move(this.tools[tool].x() + dx, this.tools[tool].y() + dy);
+        }
+        if (this.back != null) {
+            Overlays.editOverlay(this.back, {
+                x: x - ToolBar.SPACING,
+                y: y - ToolBar.SPACING
+            });
+        }
+        if (this.onMove !== null) {
+            this.onMove(x, y, dx, dy);
+        };
+    }
+
+    this.setAlpha = function(alpha, tool) {
+        if(typeof(tool) === 'undefined') {
+            for(var tool in this.tools) {
+                this.tools[tool].setAlpha(alpha);
+            }
+            if (this.back != null) {
+                this.backAlpha = alpha;
+                Overlays.editOverlay(this.back, { alpha: alpha });
+            }
+        } else {
+            this.tools[tool].setAlpha(alpha);
+        }
+    }
+
+    this.setImageURL = function(imageURL, tool) {
+        this.tools[tool].setImageURL(imageURL);
+    }
+
+    this.setBack = function(color, alpha) {
+        if (color == null) {
+            Overlays.editOverlay(this.back, { visible: false });
+        } else {
+            Overlays.editOverlay(this.back, {
+                                 width: this.width +
+                                 ((direction == ToolBar.HORIZONTAL) ? 1 : 2) * ToolBar.SPACING,
+                                 height: this.height +
+                                 ((direction == ToolBar.VERTICAL) ? 1 : 2) * ToolBar.SPACING,
+                                 visible: true,
+                                 color: color,
+                                 alpha: alpha
+                                 });
+        }
+    }
+
+    this.showTool = function(tool, doShow) {
+        this.tools[tool].show(doShow);
+    }
+    
+    this.show = function(doShow) {
+        for(var tool in this.tools) {
+            this.tools[tool].show(doShow);
+        }
+        if (this.back != null) {
+            Overlays.editOverlay(this.back, { visible: doShow});
+        }
+    }
+    
+    this.clicked = function(clickedOverlay, update) {
+        if(typeof(update) === 'undefined') {
+            update = true;
+        }
+        
+        for(var tool in this.tools) {
+            if (this.tools[tool].visible() && this.tools[tool].clicked(clickedOverlay, update)) {
+                return parseInt(tool);
+            }
+        }
+        return -1;
+    }
+    
+    this.numberOfTools = function() {
+        return this.tools.length;
+    }
+    
+    this.selectTool = function (tool, select) {
+        this.tools[tool].select(select);
+    }
+
+    this.toolSelected = function (tool) {
+        return this.tools[tool].selected();
+    }
+
+    this.cleanup = function() {
+        for(var tool in this.tools) {
+            this.tools[tool].cleanup();
+        }
+
+        if (this.back != null) {
+            Overlays.deleteOverlay(this.back);
+            this.back = null;
+        }
+
+        this.tools = [];
+        this.x = x;
+        this.y = y;
+        this.width = 0;
+        this.height = 0;
+    }
+
+    var that = this;
+    this.contains = function (xOrPoint, optionalY) {  // All four margins are draggable.
+        var x = (optionalY === undefined) ? xOrPoint.x : xOrPoint,
+            y = (optionalY === undefined) ? xOrPoint.y : optionalY;
+        return ((that.x - ToolBar.SPACING) <= x) && (x <= (that.x + that.width + ToolBar.SPACING)) &&
+            ((that.y - ToolBar.SPACING) <= y) && (y <= (that.y + that.height));
+    }
+    that.hover = function (enable) {  // Can be overridden or extended by clients.
+        that.isHovering = enable;
+        if (that.back) {
+            Overlays.editOverlay(this.back, {
+                visible: enable,
+                alpha: enable ? 0.5 : that.backAlpha
+            });
+        }
+    };
+
+    function clamp(value, min, max) {
+        return Math.min(Math.max(value, min), max);
+    }
+
+    var recommendedRect = Controller.getRecommendedHUDRect();
+    var recommendedDimmensions = { x: recommendedRect.width, y: recommendedRect.height };
+    that.windowDimensions = recommendedDimmensions; // Controller.getViewportDimensions();
+    that.origin = { x: recommendedRect.x, y: recommendedRect.y };
+    // Maybe fixme: Keeping the same percent of the window size isn't always the right thing.
+    // For example, maybe we want "keep the same percentage to whatever two edges are closest to the edge of screen".
+    // If we change that, the places to do so are onResizeViewport, save (maybe), and the initial move based on Settings, below.
+    that.onResizeViewport = function (newSize) { // Can be overridden or extended by clients.
+        var recommendedRect = Controller.getRecommendedHUDRect();
+        var recommendedDimmensions = { x: recommendedRect.width, y: recommendedRect.height };
+        var originRelativeX = (that.x - that.origin.x - that.offset.x);
+        var originRelativeY = (that.y - that.origin.y - that.offset.y);
+        var fractionX = clamp(originRelativeX / that.windowDimensions.x, 0, 1);
+        var fractionY = clamp(originRelativeY / that.windowDimensions.y, 0, 1);
+        that.windowDimensions = newSize || recommendedDimmensions;
+        that.origin = { x: recommendedRect.x, y: recommendedRect.y };
+        var newX = (fractionX * that.windowDimensions.x) + recommendedRect.x + that.offset.x;
+        var newY = (fractionY * that.windowDimensions.y) + recommendedRect.y + that.offset.y;
+        that.move(newX, newY);
+    };
+    if (optionalPersistenceKey) {
+        this.fractionKey = optionalPersistenceKey + '.fraction';
+        // FIXME: New default position in RC8 is bottom center of screen instead of right. Can remove this key and associated
+        // code once the new toolbar position is well established with users.
+        this.isNewPositionKey = optionalPersistenceKey + '.isNewPosition';
+        this.save = function () {
+            var recommendedRect = Controller.getRecommendedHUDRect();
+            var screenSize = { x: recommendedRect.width, y: recommendedRect.height };
+            if (screenSize.x > 0 && screenSize.y > 0) {
+                // Guard against invalid screen size that can occur at shut-down.
+                var fraction = {x: (that.x - that.offset.x) / screenSize.x, y: (that.y - that.offset.y) / screenSize.y};
+                Settings.setValue(this.fractionKey, JSON.stringify(fraction));
+            }
+        }
+    } else {
+        this.save = function () { }; // Called on move. Can be overriden or extended by clients.
+    }
+    // These are currently only doing that which is necessary for toolbar hover and toolbar drag.
+    // They have not yet been extended to tool hover/click/release, etc.
+    this.mousePressEvent = function (event) {
+        if (Overlays.getOverlayAtPoint({ x: event.x, y: event.y }) == that.back) {
+            that.mightBeDragging = true;
+            that.dragOffsetX = that.x - event.x;
+            that.dragOffsetY = that.y - event.y;
+        } else {
+            that.mightBeDragging = false;
+        }
+    };
+    this.mouseReleaseEvent = function (event) {
+        for (var tool in that.tools) {
+            that.tools[tool].buttonDown(false);
+        }
+        if (that.mightBeDragging) {
+            that.save();
+        }
+    }
+    this.mouseMove  = function (event) {
+        if (!that.mightBeDragging || !event.isLeftButton) {
+            that.mightBeDragging = false;
+            if (!that.contains(event)) {
+                if (that.isHovering) {
+                    that.hover(false);
+                }
+                return;
+            }
+            if (!that.isHovering) {
+                that.hover(true);
+            }
+            return;
+        }
+        that.move(that.dragOffsetX + event.x, that.dragOffsetY + event.y);
+    };
+    that.checkResize = function () { // Can be overriden or extended, but usually not. See onResizeViewport.
+        var recommendedRect = Controller.getRecommendedHUDRect();
+        var currentWindowSize = { x: recommendedRect.width, y: recommendedRect.height };
+
+        if ((currentWindowSize.x !== that.windowDimensions.x) || (currentWindowSize.y !== that.windowDimensions.y)) {
+            that.onResizeViewport(currentWindowSize);            
+        }
+    };
+    Controller.mousePressEvent.connect(this.mousePressEvent);
+    Controller.mouseReleaseEvent.connect(this.mouseReleaseEvent);
+    Controller.mouseMoveEvent.connect(this.mouseMove);
+    Script.update.connect(that.checkResize);
+    // This compatability hack breaks the model, but makes converting existing scripts easier:
+    this.addOverlay = function (ignored, oldSchoolProperties) {
+        var properties = JSON.parse(JSON.stringify(oldSchoolProperties)); // a copy
+        if ((that.numberOfTools() === 0) && (properties.x != undefined)  && (properties.y != undefined)) {
+            that.move(properties.x, properties.y);
+        }
+        delete properties.x;
+        delete properties.y;
+        var index = that.addTool(properties);
+        var id = that.tools[index].overlay();
+        return id;
+    }
+    if (this.fractionKey || optionalInitialPositionFunction) {
+        var isNewPosition = Settings.getValue(this.isNewPositionKey);
+        var savedFraction = isNewPosition ? JSON.parse(Settings.getValue(this.fractionKey) || "0") : 0;
+        Settings.setValue(this.isNewPositionKey, true);
+
+        var recommendedRect = Controller.getRecommendedHUDRect();
+        var screenSize = { x: recommendedRect.width, y: recommendedRect.height };
+        if (savedFraction) {
+            // If we have saved data, keep the toolbar at the same proportion of the screen width/height.
+            that.move(savedFraction.x * screenSize.x + that.offset.x, savedFraction.y * screenSize.y + that.offset.y);
+        } else if (!optionalInitialPositionFunction) {
+            print("No initPosition(screenSize, intializedToolbar) specified for ToolBar");
+        } else {
+            // Call the optionalInitialPositionFunctinon() AFTER the client has had a chance to set up.
+            var that = this;
+            Script.setTimeout(function () {
+                var position = optionalInitialPositionFunction(screenSize, that);
+                that.move(position.x + that.offset.x, position.y + that.offset.y);
+            }, 0);
+        }
+    }
+}
+ToolBar.SPACING = 6;
+ToolBar.VERTICAL = 0;
+ToolBar.HORIZONTAL = 1;
diff --git a/scripts/simplifiedUI/system/libraries/touchEventUtils.js b/scripts/simplifiedUI/system/libraries/touchEventUtils.js
new file mode 100644
index 0000000000..f0f7ec46fe
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/touchEventUtils.js
@@ -0,0 +1,250 @@
+"use strict";
+
+//  touchEventUtils.js
+//
+//  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,
+   enableDispatcherModule, disableDispatcherModule, makeRunningValues,
+   Messages, Quat, Vec3, getControllerWorldLocation, makeDispatcherModuleParameters, Overlays, controllerDispatcher.ZERO_VEC,
+   HMD, INCHES_TO_METERS, DEFAULT_REGISTRATION_POINT, Settings, getGrabPointSphereOffset
+*/
+
+var controllerDispatcher = Script.require("/~/system/libraries/controllerDispatcherUtils.js");
+function touchTargetHasKeyboardFocus(touchTarget) {
+    if (touchTarget.entityID && touchTarget.entityID !== Uuid.NULL) {
+        return Entities.keyboardFocusEntity === touchTarget.entityID;
+    } else if (touchTarget.overlayID && touchTarget.overlayID !== Uuid.NULL) {
+        return Overlays.keyboardFocusOverlay === touchTarget.overlayID;
+    }
+}
+
+function setKeyboardFocusOnTouchTarget(touchTarget) {
+    if (touchTarget.entityID && touchTarget.entityID !== Uuid.NULL &&
+        Entities.wantsHandControllerPointerEvents(touchTarget.entityID)) {
+        Overlays.keyboardFocusOverlay = Uuid.NULL;
+        Entities.keyboardFocusEntity = touchTarget.entityID;
+    } else if (touchTarget.overlayID && touchTarget.overlayID !== Uuid.NULL) {
+        Overlays.keyboardFocusOverlay = touchTarget.overlayID;
+        Entities.keyboardFocusEntity = Uuid.NULL;
+    }
+}
+
+function sendHoverEnterEventToTouchTarget(hand, touchTarget) {
+    var pointerEvent = {
+        type: "Move",
+        id: hand + 1, // 0 is reserved for hardware mouse
+        pos2D: touchTarget.position2D,
+        pos3D: touchTarget.position,
+        normal: touchTarget.normal,
+        direction: Vec3.subtract(controllerDispatcher.ZERO_VEC, touchTarget.normal),
+        button: "None"
+    };
+
+    if (touchTarget.entityID && touchTarget.entityID !== Uuid.NULL) {
+        Entities.sendHoverEnterEntity(touchTarget.entityID, pointerEvent);
+    } else if (touchTarget.overlayID && touchTarget.overlayID !== Uuid.NULL) {
+        Overlays.sendHoverEnterOverlay(touchTarget.overlayID, pointerEvent);
+    }
+}
+
+function sendHoverOverEventToTouchTarget(hand, touchTarget) {
+    var pointerEvent = {
+        type: "Move",
+        id: hand + 1, // 0 is reserved for hardware mouse
+        pos2D: touchTarget.position2D,
+        pos3D: touchTarget.position,
+        normal: touchTarget.normal,
+        direction: Vec3.subtract(controllerDispatcher.ZERO_VEC, touchTarget.normal),
+        button: "None"
+    };
+
+    if (touchTarget.entityID && touchTarget.entityID !== Uuid.NULL) {
+        Entities.sendMouseMoveOnEntity(touchTarget.entityID, pointerEvent);
+        Entities.sendHoverOverEntity(touchTarget.entityID, pointerEvent);
+    } else if (touchTarget.overlayID && touchTarget.overlayID !== Uuid.NULL) {
+        Overlays.sendMouseMoveOnOverlay(touchTarget.overlayID, pointerEvent);
+        Overlays.sendHoverOverOverlay(touchTarget.overlayID, pointerEvent);
+    }
+}
+
+function sendTouchStartEventToTouchTarget(hand, touchTarget) {
+    var pointerEvent = {
+        type: "Press",
+        id: hand + 1, // 0 is reserved for hardware mouse
+        pos2D: touchTarget.position2D,
+        pos3D: touchTarget.position,
+        normal: touchTarget.normal,
+        direction: Vec3.subtract(controllerDispatcher.ZERO_VEC, touchTarget.normal),
+        button: "Primary",
+        isPrimaryHeld: true
+    };
+
+    if (touchTarget.entityID && touchTarget.entityID !== Uuid.NULL) {
+        Entities.sendMousePressOnEntity(touchTarget.entityID, pointerEvent);
+        Entities.sendClickDownOnEntity(touchTarget.entityID, pointerEvent);
+    } else if (touchTarget.overlayID && touchTarget.overlayID !== Uuid.NULL) {
+        Overlays.sendMousePressOnOverlay(touchTarget.overlayID, pointerEvent);
+    }
+}
+
+function sendTouchEndEventToTouchTarget(hand, touchTarget) {
+    var pointerEvent = {
+        type: "Release",
+        id: hand + 1, // 0 is reserved for hardware mouse
+        pos2D: touchTarget.position2D,
+        pos3D: touchTarget.position,
+        normal: touchTarget.normal,
+        direction: Vec3.subtract(controllerDispatcher.ZERO_VEC, touchTarget.normal),
+        button: "Primary"
+    };
+
+    if (touchTarget.entityID && touchTarget.entityID !== Uuid.NULL) {
+        Entities.sendMouseReleaseOnEntity(touchTarget.entityID, pointerEvent);
+        Entities.sendClickReleaseOnEntity(touchTarget.entityID, pointerEvent);
+        Entities.sendHoverLeaveEntity(touchTarget.entityID, pointerEvent);
+    } else if (touchTarget.overlayID && touchTarget.overlayID !== Uuid.NULL) {
+        Overlays.sendMouseReleaseOnOverlay(touchTarget.overlayID, pointerEvent);
+    }
+}
+
+function sendTouchMoveEventToTouchTarget(hand, touchTarget) {
+    var pointerEvent = {
+        type: "Move",
+        id: hand + 1, // 0 is reserved for hardware mouse
+        pos2D: touchTarget.position2D,
+        pos3D: touchTarget.position,
+        normal: touchTarget.normal,
+        direction: Vec3.subtract(controllerDispatcher.ZERO_VEC, touchTarget.normal),
+        button: "Primary",
+        isPrimaryHeld: true
+    };
+
+    if (touchTarget.entityID && touchTarget.entityID !== Uuid.NULL) {
+        Entities.sendMouseMoveOnEntity(touchTarget.entityID, pointerEvent);
+        Entities.sendHoldingClickOnEntity(touchTarget.entityID, pointerEvent);
+    } else if (touchTarget.overlayID && touchTarget.overlayID !== Uuid.NULL) {
+        Overlays.sendMouseMoveOnOverlay(touchTarget.overlayID, pointerEvent);
+    }
+}
+
+function composeTouchTargetFromIntersection(intersection) {
+    var isEntity = (intersection.type === Picks.INTERSECTED_ENTITY);
+    var objectID = intersection.objectID;
+    var worldPos = intersection.intersection;
+    var props = null;
+    if (isEntity) {
+        props = Entities.getProperties(intersection.objectID);
+    }
+
+    var position2D =(isEntity ? controllerDispatcher.projectOntoEntityXYPlane(objectID, worldPos, props) :
+        controllerDispatcher.projectOntoOverlayXYPlane(objectID, worldPos));
+    return {
+        entityID: isEntity ? objectID : null,
+        overlayID: isEntity ? null : objectID,
+        distance: intersection.distance,
+        position: worldPos,
+        position2D: position2D,
+        normal: intersection.surfaceNormal
+    };
+}
+
+// will return undefined if overlayID does not exist.
+function calculateTouchTargetFromOverlay(touchTip, overlayID) {
+    var overlayPosition = Overlays.getProperty(overlayID, "position");
+    if (overlayPosition === undefined) {
+        return;
+    }
+
+    // project touchTip onto overlay plane.
+    var overlayRotation = Overlays.getProperty(overlayID, "rotation");
+    if (overlayRotation === undefined) {
+        return;
+    }
+    var normal = Vec3.multiplyQbyV(overlayRotation, {x: 0, y: 0, z: 1});
+    var distance = Vec3.dot(Vec3.subtract(touchTip.position, overlayPosition), normal);
+    var position = Vec3.subtract(touchTip.position, Vec3.multiply(normal, distance));
+
+    // calclulate normalized position
+    var invRot = Quat.inverse(overlayRotation);
+    var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, overlayPosition));
+
+    var dimensions = Overlays.getProperty(overlayID, "dimensions");
+    if (dimensions === undefined) {
+        return;
+    }
+    dimensions.z = 0.01; // we are projecting onto the XY plane of the overlay, so ignore the z dimension
+    var invDimensions = { x: 1 / dimensions.x, y: 1 / dimensions.y, z: 1 / dimensions.z };
+    var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), DEFAULT_REGISTRATION_POINT);
+
+    // 2D position on overlay plane in meters, relative to the bounding box upper-left hand corner.
+    var position2D = {
+        x: normalizedPosition.x * dimensions.x,
+        y: (1 - normalizedPosition.y) * dimensions.y // flip y-axis
+    };
+
+    return {
+        entityID: null,
+        overlayID: overlayID,
+        distance: distance,
+        position: position,
+        position2D: position2D,
+        normal: normal,
+        normalizedPosition: normalizedPosition,
+        dimensions: dimensions,
+        valid: true
+    };
+}
+
+// will return undefined if entity does not exist.
+function calculateTouchTargetFromEntity(touchTip, props) {
+    if (props.rotation === undefined) {
+        // if rotation is missing from props object, then this entity has probably been deleted.
+        return;
+    }
+
+    // project touch tip onto entity plane.
+    var normal = Vec3.multiplyQbyV(props.rotation, {x: 0, y: 0, z: 1});
+    Vec3.multiplyQbyV(props.rotation, {x: 0, y: 1, z: 0});
+    var distance = Vec3.dot(Vec3.subtract(touchTip.position, props.position), normal);
+    var position = Vec3.subtract(touchTip.position, Vec3.multiply(normal, distance));
+
+    // generate normalized coordinates
+    var invRot = Quat.inverse(props.rotation);
+    var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, props.position));
+    var invDimensions = { x: 1 / props.dimensions.x, y: 1 / props.dimensions.y, z: 1 / props.dimensions.z };
+    var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), props.registrationPoint);
+
+    // 2D position on entity plane in meters, relative to the bounding box upper-left hand corner.
+    var position2D = {
+        x: normalizedPosition.x * props.dimensions.x,
+        y: (1 - normalizedPosition.y) * props.dimensions.y // flip y-axis
+    };
+
+    return {
+        entityID: props.id,
+        entityProps: props,
+        overlayID: null,
+        distance: distance,
+        position: position,
+        position2D: position2D,
+        normal: normal,
+        normalizedPosition: normalizedPosition,
+        dimensions: props.dimensions,
+        valid: true
+    };
+}
+
+module.exports = {
+    calculateTouchTargetFromEntity: calculateTouchTargetFromEntity,
+    calculateTouchTargetFromOverlay: calculateTouchTargetFromOverlay,
+    touchTargetHasKeyboardFocus: touchTargetHasKeyboardFocus,
+    setKeyboardFocusOnTouchTarget: setKeyboardFocusOnTouchTarget,
+    sendHoverEnterEventToTouchTarget: sendHoverEnterEventToTouchTarget,
+    sendHoverOverEventToTouchTarget: sendHoverOverEventToTouchTarget,
+    sendTouchStartEventToTouchTarget: sendTouchStartEventToTouchTarget,
+    sendTouchEndEventToTouchTarget: sendTouchEndEventToTouchTarget,
+    sendTouchMoveEventToTouchTarget: sendTouchMoveEventToTouchTarget,
+    composeTouchTargetFromIntersection: composeTouchTargetFromIntersection
+};
diff --git a/scripts/simplifiedUI/system/libraries/utils.js b/scripts/simplifiedUI/system/libraries/utils.js
new file mode 100644
index 0000000000..508e8d46e3
--- /dev/null
+++ b/scripts/simplifiedUI/system/libraries/utils.js
@@ -0,0 +1,504 @@
+//
+//  Created by Bradley Austin Davis on 2015/08/29
+//  Copyright 2015 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
+//
+
+// note: this constant is currently duplicated in edit.js and ambientSound.js
+EDIT_SETTING = "io.highfidelity.isEditing";
+isInEditMode = function isInEditMode() {
+    return Settings.getValue(EDIT_SETTING);
+};
+
+if (!Function.prototype.bind) {
+    Function.prototype.bind = function(oThis) {
+        if (typeof this !== 'function') {
+            // closest thing possible to the ECMAScript 5
+            // internal IsCallable function
+            throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
+        }
+
+        var aArgs   = Array.prototype.slice.call(arguments, 1),
+            fToBind = this,
+            fNOP    = function() {},
+            fBound  = function() {
+                return fToBind.apply(this instanceof fNOP
+                        ? this
+                        : oThis,
+                        aArgs.concat(Array.prototype.slice.call(arguments)));
+            };
+
+        if (this.prototype) {
+            // Function.prototype doesn't have a prototype property
+            fNOP.prototype = this.prototype;
+        }
+        fBound.prototype = new fNOP();
+
+        return fBound;
+    };
+}
+
+vec3toStr = function(v, digits) {
+    if (!digits) { digits = 3; }
+    return "{ " + v.x.toFixed(digits) + ", " + v.y.toFixed(digits) + ", " + v.z.toFixed(digits)+ " }";
+}
+
+quatToStr = function(q, digits) {
+    if (!digits) { digits = 3; }
+    return "{ " + q.w.toFixed(digits) + ", " + q.x.toFixed(digits) + ", " +
+        q.y.toFixed(digits) + ", " + q.z.toFixed(digits)+ " }";
+}
+
+vec3equal = function(v0, v1) {
+    return (v0.x == v1.x) && (v0.y == v1.y) && (v0.z == v1.z);
+}
+
+colorMix = function(colorA, colorB, mix) {
+    var result = {};
+    for (var key in colorA) {
+        result[key] = (colorA[key] * (1 - mix)) + (colorB[key] * mix);
+    }
+    return result;
+}
+scaleLine = function (start, end, scale) {
+    var v = Vec3.subtract(end, start);
+    var length = Vec3.length(v);
+    v = Vec3.multiply(scale, v);
+    return Vec3.sum(start, v);
+}
+
+findAction = function(name) {
+    return Controller.findAction(name);
+}
+
+addLine = function(origin, vector, color) {
+    if (!color) {
+        color = COLORS.WHITE
+    }
+    return Entities.addEntity(mergeObjects(LINE_PROTOTYPE, {
+        position: origin,
+        linePoints: [
+            ZERO_VECTOR,
+            vector,
+        ],
+        color: color
+    }));
+}
+
+// FIXME fetch from a subkey of user data to support non-destructive modifications
+setEntityUserData = function(id, data) {
+    var json = JSON.stringify(data)
+    Entities.editEntity(id, { userData: json });
+}
+
+// FIXME do non-destructive modification of the existing user data
+getEntityUserData = function(id) {
+    var results = null;
+    var properties = Entities.getEntityProperties(id, "userData");
+    if (properties.userData) {
+        try {
+            results = JSON.parse(properties.userData);
+        } catch(err) {
+            logDebug(err);
+        }
+    }
+    return results ? results : {};
+}
+
+
+// Non-destructively modify the user data of an entity.
+setEntityCustomData = function(customKey, id, data) {
+    var userData = getEntityUserData(id);
+    if (data == null) {
+        delete userData[customKey];
+    } else {
+        userData[customKey] = data;
+    }
+    setEntityUserData(id, userData);
+}
+
+getEntityCustomData = function(customKey, id, defaultValue) {
+    var userData = getEntityUserData(id);
+    if (undefined != userData[customKey]) {
+        return userData[customKey];
+    } else {
+        return defaultValue;
+    }
+}
+
+mergeObjects = function(proto, custom) {
+    var result = {};
+    for (var attrname in proto) {
+        result[attrname] = proto[attrname];
+    }
+    for (var attrname in custom) {
+        result[attrname] = custom[attrname];
+    }
+    return result;
+}
+
+LOG_WARN = 1;
+
+logWarn = function(str) {
+    if (LOG_WARN) {
+        print(str);
+    }
+}
+
+LOG_ERROR = 1;
+
+logError = function(str) {
+    if (LOG_ERROR) {
+        print(str);
+    }
+}
+
+LOG_INFO = 1;
+
+logInfo = function(str) {
+    if (LOG_INFO) {
+        print(str);
+    }
+}
+
+LOG_DEBUG = 0;
+
+logDebug = function(str) {
+    if (LOG_DEBUG) {
+        print(str);
+    }
+}
+
+LOG_TRACE = 0;
+
+logTrace = function(str) {
+    if (LOG_TRACE) {
+        print(str);
+    }
+}
+
+// Computes the penetration between a point and a sphere (centered at the origin)
+// if point is inside sphere: returns true and stores the result in 'penetration'
+// (the vector that would move the point outside the sphere)
+// otherwise returns false
+findSphereHit = function(point, sphereRadius) {
+    var EPSILON = 0.000001;     //smallish positive number - used as margin of error for some computations
+    var vectorLength = Vec3.length(point);
+    if (vectorLength < EPSILON) {
+        return true;
+    }
+    var distance = vectorLength - sphereRadius;
+    if (distance < 0.0) {
+        return true;
+    }
+    return false;
+}
+
+findSpherePointHit = function(sphereCenter, sphereRadius, point) {
+    return findSphereHit(Vec3.subtract(point,sphereCenter), sphereRadius);
+}
+
+findSphereSphereHit = function(firstCenter, firstRadius, secondCenter, secondRadius) {
+    return findSpherePointHit(firstCenter, firstRadius + secondRadius, secondCenter);
+}
+
+// Given a vec3 v, return a vec3 that is the same vector relative to the avatars
+// DEFAULT eye position, rotated into the avatars reference frame.
+getEyeRelativePosition = function(v) {
+    return Vec3.sum(MyAvatar.getDefaultEyePosition(), Vec3.multiplyQbyV(MyAvatar.orientation, v));
+}
+
+getAvatarRelativeRotation = function(q) {
+    return Quat.multiply(MyAvatar.orientation, q);
+}
+
+pointInExtents = function(point, minPoint, maxPoint) {
+    return (point.x >= minPoint.x && point.x <= maxPoint.x) &&
+           (point.y >= minPoint.y && point.y <= maxPoint.y) &&
+           (point.z >= minPoint.z && point.z <= maxPoint.z);
+}
+
+/**
+ * Converts an HSL color value to RGB. Conversion formula
+ * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
+ * Assumes h, s, and l are contained in the set [0, 1] and
+ * returns r, g, and b in the set [0, 255].
+ *
+ * @param   Number  h       The hue
+ * @param   Number  s       The saturation
+ * @param   Number  l       The lightness
+ * @return  Array           The RGB representation
+ */
+hslToRgb = function(hsl) {
+    var r, g, b;
+    if (hsl.s == 0) {
+        r = g = b = hsl.l; // achromatic
+    } else {
+        var hue2rgb = function hue2rgb(p, q, t) {
+            if (t < 0) t += 1;
+            if (t > 1) t -= 1;
+            if (t < 1 / 6) return p + (q - p) * 6 * t;
+            if (t < 1 / 2) return q;
+            if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+            return p;
+        }
+
+        var q = hsl.l < 0.5 ? hsl.l * (1 + hsl.s) : hsl.l + hsl.s - hsl.l * hsl.s;
+        var p = 2 * hsl.l - q;
+        r = hue2rgb(p, q, hsl.h + 1 / 3);
+        g = hue2rgb(p, q, hsl.h);
+        b = hue2rgb(p, q, hsl.h - 1 / 3);
+    }
+
+    return {
+        red: Math.round(r * 255),
+        green: Math.round(g * 255),
+        blue: Math.round(b * 255)
+    };
+}
+
+map = function(value, min1, max1, min2, max2) {
+    return min2 + (max2 - min2) * ((value - min1) / (max1 - min1));
+}
+
+orientationOf = function(vector) {
+    var Y_AXIS = {
+        x: 0,
+        y: 1,
+        z: 0
+    };
+    var X_AXIS = {
+        x: 1,
+        y: 0,
+        z: 0
+    };
+
+    var theta = 0.0;
+
+    var RAD_TO_DEG = 180.0 / Math.PI;
+    var direction, yaw, pitch;
+    direction = Vec3.normalize(vector);
+    yaw = Quat.angleAxis(Math.atan2(direction.x, direction.z) * RAD_TO_DEG, Y_AXIS);
+    pitch = Quat.angleAxis(Math.asin(-direction.y) * RAD_TO_DEG, X_AXIS);
+    return Quat.multiply(yaw, pitch);
+}
+
+randFloat = function(low, high) {
+    return low + Math.random() * (high - low);
+}
+
+
+randInt = function(low, high) {
+    return Math.floor(randFloat(low, high));
+}
+
+
+randomColor = function() {
+    return {
+                red: randInt(0, 255),
+                green: randInt(0, 255),
+                blue: randInt(0, 255)
+            }
+}
+
+
+hexToRgb = function(hex) {
+    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+    return result ? {
+        red: parseInt(result[1], 16),
+        green: parseInt(result[2], 16),
+        blue: parseInt(result[3], 16)
+    } : null;
+}
+
+calculateHandSizeRatio = function() {
+    // Get the ratio of the current avatar's hand to Owen's hand
+
+    var standardCenterHandPoint = 0.11288;
+    var jointNames = MyAvatar.getJointNames();
+    //get distance from handJoint up to leftHandIndex3 as a proxy for center of hand
+    var wristToFingertipDistance = 0;;
+    for (var i = 0; i < jointNames.length; i++) {
+        var jointName = jointNames[i];
+        print(jointName)
+        if (jointName.indexOf("LeftHandIndex") !== -1) {
+            // translations are relative to parent joint, so simply add them together
+            // joints face down the y-axis
+            var translation = MyAvatar.getDefaultJointTranslation(i).y;
+            wristToFingertipDistance += translation;
+        }
+    }
+    // Right now units are in cm, so convert to meters
+    wristToFingertipDistance /= 100;
+
+    var centerHandPoint = wristToFingertipDistance/2;
+
+    // Compare against standard hand (Owen)
+    var handSizeRatio = centerHandPoint/standardCenterHandPoint;
+    return handSizeRatio;
+}
+
+clamp = function(val, min, max){
+     return Math.max(min, Math.min(max, val))
+}
+
+// flattens an array of arrays into a single array
+// example: flatten([[1], [3, 4], []) => [1, 3, 4]
+// NOTE: only does one level of flattening, it is not recursive.
+flatten = function(array) {
+    return [].concat.apply([], array);
+}
+
+getTabletWidthFromSettings = function () {
+    var DEFAULT_TABLET_WIDTH = 0.4375;
+    var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
+    var toolbarMode = tablet.toolbarMode;
+    var DEFAULT_DESKTOP_TABLET_SCALE = 75;
+    var DEFAULT_HMD_TABLET_SCALE = 60;
+    var tabletScalePercentage = DEFAULT_HMD_TABLET_SCALE;
+    if (!toolbarMode) {
+        if (HMD.active) {
+            tabletScalePercentage = Settings.getValue("hmdTabletScale") || DEFAULT_HMD_TABLET_SCALE;
+        } else {
+            tabletScalePercentage = Settings.getValue("desktopTabletScale") || DEFAULT_DESKTOP_TABLET_SCALE;
+        }
+    }
+    return DEFAULT_TABLET_WIDTH * (tabletScalePercentage / 100);
+};
+
+resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride) {
+
+    if (!HMD.tabletID || !HMD.tabletScreenID || !HMD.homeButtonID || !HMD.homeButtonHighlightID) {
+        return;
+    }
+    var sensorScaleFactor = sensorToWorldScaleOverride || MyAvatar.sensorToWorldScale;
+    var sensorScaleOffsetOverride = 1;
+    var SENSOR_TO_ROOM_MATRIX = 65534;
+    var parentJointIndex = newParentJointIndex || Overlays.getProperty(HMD.tabletID, "parentJointIndex");
+    if (parentJointIndex === SENSOR_TO_ROOM_MATRIX) {
+        sensorScaleOffsetOverride = 1 / sensorScaleFactor;
+    }
+
+
+    // will need to be recaclulated if dimensions of fbx model change.
+    var TABLET_NATURAL_DIMENSIONS = {x: 32.083, y: 48.553, z: 2.269};
+    var DEFAULT_DPI = 31;
+    var DEFAULT_WIDTH = 0.4375;
+
+    // scale factor of natural tablet dimensions.
+    var tabletWidth = (width || DEFAULT_WIDTH) * sensorScaleFactor;
+    var tabletScaleFactor = tabletWidth / TABLET_NATURAL_DIMENSIONS.x;
+    var tabletHeight = TABLET_NATURAL_DIMENSIONS.y * tabletScaleFactor;
+    var tabletDepth = TABLET_NATURAL_DIMENSIONS.z * tabletScaleFactor;
+    var tabletDpi = DEFAULT_DPI * (DEFAULT_WIDTH / tabletWidth);
+
+    // update tablet model dimensions
+    Entities.editEntity(HMD.tabletID, {
+        dimensions: { x: tabletWidth, y: tabletHeight, z: tabletDepth }
+    });
+
+    // update webOverlay
+    var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.5) * sensorScaleOffsetOverride;
+    var WEB_ENTITY_Y_OFFSET = 1.25 * tabletScaleFactor * sensorScaleOffsetOverride;
+    var screenWidth = 0.9367 * tabletWidth;
+    var screenHeight = 0.9000 * tabletHeight;
+    var landscape = Tablet.getTablet("com.highfidelity.interface.tablet.system").landscape;
+    Entities.editEntity(HMD.tabletScreenID, {
+        localPosition: { x: 0, y: WEB_ENTITY_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET},
+        dimensions: {x: landscape ? screenHeight : screenWidth, y: landscape ? screenWidth : screenHeight, z: 1.0},
+        dpi: tabletDpi
+    });
+
+    // update homeButton
+    var homeButtonDim = 4.0 * tabletScaleFactor / 1.5;
+    var HOME_BUTTON_X_OFFSET = 0.00079 * sensorScaleOffsetOverride * sensorScaleFactor;
+    var HOME_BUTTON_Y_OFFSET = -1 * ((tabletHeight / 2) - (4.0 * tabletScaleFactor / 2)) * sensorScaleOffsetOverride;
+    var HOME_BUTTON_Z_OFFSET = (tabletDepth / 1.9) * sensorScaleOffsetOverride;
+    Entities.editEntity(HMD.homeButtonID, {
+        localPosition: { x: HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
+        dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
+    });
+
+    Entities.editEntity(HMD.homeButtonHighlightID, {
+        localPosition: { x: -HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
+        dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
+    });
+};
+
+
+reparentAndScaleTablet = function(width, reparentProps) {
+
+    if (!HMD.tabletID || !HMD.tabletScreenID || !HMD.homeButtonID || !HMD.homeButtonHighlightID) {
+        return;
+    }
+    var sensorScaleFactor = MyAvatar.sensorToWorldScale;
+    var sensorScaleOffsetOverride = 1;
+    var SENSOR_TO_ROOM_MATRIX = 65534;
+    var parentJointIndex = reparentProps.parentJointIndex;
+    if (parentJointIndex === SENSOR_TO_ROOM_MATRIX) {
+        sensorScaleOffsetOverride = 1 / sensorScaleFactor;
+    }
+
+
+    // will need to be recaclulated if dimensions of fbx model change.
+    var TABLET_NATURAL_DIMENSIONS = {x: 32.083, y: 48.553, z: 2.269};
+    var DEFAULT_DPI = 31;
+    var DEFAULT_WIDTH = 0.4375;
+
+    // scale factor of natural tablet dimensions.
+    var tabletWidth = (width || DEFAULT_WIDTH) * sensorScaleFactor;
+    var tabletScaleFactor = tabletWidth / TABLET_NATURAL_DIMENSIONS.x;
+    var tabletHeight = TABLET_NATURAL_DIMENSIONS.y * tabletScaleFactor;
+    var tabletDepth = TABLET_NATURAL_DIMENSIONS.z * tabletScaleFactor;
+    var tabletDpi = DEFAULT_DPI * (DEFAULT_WIDTH / tabletWidth);
+
+    // update tablet model dimensions
+
+    Entities.editEntity(HMD.tabletID, {
+        parentID: reparentProps.parentID,
+        parentJointIndex: reparentProps.parentJointIndex,
+        dimensions: { x: tabletWidth, y: tabletHeight, z: tabletDepth}
+    });
+    // update webOverlay
+    var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.5) * sensorScaleOffsetOverride;
+    var WEB_ENTITY_Y_OFFSET = 1.25 * tabletScaleFactor * sensorScaleOffsetOverride;
+    var screenWidth = 0.9367 * tabletWidth;
+    var screenHeight = 0.9000 * tabletHeight;
+    var landscape = Tablet.getTablet("com.highfidelity.interface.tablet.system").landscape;
+    Entities.editEntity(HMD.tabletScreenID, {
+        localPosition: { x: 0, y: WEB_ENTITY_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET},
+        dimensions: {x: landscape ? screenHeight : screenWidth, y: landscape ? screenWidth : screenHeight, z: 1.0},
+        dpi: tabletDpi
+    });
+
+    // update homeButton
+    var homeButtonDim = 4.0 * tabletScaleFactor / 1.5;
+    var HOME_BUTTON_X_OFFSET = 0.00079 * sensorScaleOffsetOverride * sensorScaleFactor;
+    var HOME_BUTTON_Y_OFFSET = -1 * ((tabletHeight / 2) - (4.0 * tabletScaleFactor / 2)) * sensorScaleOffsetOverride;
+    var HOME_BUTTON_Z_OFFSET = (tabletDepth / 1.9) * sensorScaleOffsetOverride;
+    Entities.editEntity(HMD.homeButtonID, {
+        localPosition: { x: HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
+        dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
+    });
+
+    Entities.editEntity(HMD.homeButtonHighlightID, {
+        localPosition: { x: -HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
+        dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
+    });
+}
+
+getMainTabletIDs = function () {
+    var tabletIDs = [];
+    if (HMD.tabletID) {
+        tabletIDs.push(HMD.tabletID);
+    }
+    if (HMD.tabletScreenID) {
+        tabletIDs.push(HMD.tabletScreenID);
+    }
+    if (HMD.homeButtonID) {
+        tabletIDs.push(HMD.homeButtonID);
+    }
+    return tabletIDs;
+};