diff --git a/examples/editVoxels.js b/examples/editVoxels.js
index cd9566717a..ed63c12eb9 100644
--- a/examples/editVoxels.js
+++ b/examples/editVoxels.js
@@ -68,7 +68,8 @@ colors[4] = { red: 236, green: 174,  blue: 0 };
 colors[5] = { red: 234, green: 133,  blue: 0 };
 colors[6] = { red: 211, green: 115,  blue: 0 };
 colors[7] = { red: 48,  green: 116,  blue: 119 };
-var numColors = 8;
+colors[8] = { red: 31,  green: 64,  blue: 64 };
+var numColors = 9;
 var whichColor = -1;            //  Starting color is 'Copy' mode
 
 //  Create sounds for adding, deleting, recoloring voxels 
@@ -138,34 +139,46 @@ var linePreviewRight = Overlays.addOverlay("line3d", {
 
 
 // these will be used below
-var sliderWidth = 158;
-var sliderHeight = 35;
+var sliderWidth = 154;
+var sliderHeight = 37;
 
 // These will be our "overlay IDs"
 var swatches = new Array();
-var swatchHeight = 54;
-var swatchWidth = 31;
-var swatchesWidth = swatchWidth * numColors;
+var swatchExtraPadding = 5;
+var swatchHeight = 37;
+var swatchWidth = 27;
+var swatchesWidth = swatchWidth * numColors + numColors + swatchExtraPadding * 2;
 var swatchesX = (windowDimensions.x - (swatchesWidth + sliderWidth)) / 2;
-var swatchesY = windowDimensions.y - swatchHeight;
+var swatchesY = windowDimensions.y - swatchHeight + 1;
+
+var toolIconUrl = "http://highfidelity-public.s3-us-west-1.amazonaws.com/images/tools/";
 
 // create the overlays, position them in a row, set their colors, and for the selected one, use a different source image
 // location so that it displays the "selected" marker
 for (s = 0; s < numColors; s++) {
-    var imageFromX = 12 + (s * 27);
-    var imageFromY = 0;
-    if (s == whichColor) {
-        imageFromY = 55;
+
+    var extraWidth = 0;
+
+    if (s == 0) {
+        extraWidth = swatchExtraPadding;
+    }
+
+    var imageFromX = swatchExtraPadding - extraWidth + s * swatchWidth;
+    var imageFromY = swatchHeight + 1;
+
+    var swatchX = swatchExtraPadding - extraWidth + swatchesX + ((swatchWidth - 1) * s);
+
+    if (s == (numColors - 1)) {
+        extraWidth = swatchExtraPadding;
     }
-    var swatchX = swatchesX + (30 * s);
 
     swatches[s] = Overlays.addOverlay("image", {
                     x: swatchX,
                     y: swatchesY,
-                    width: swatchWidth,
+                    width: swatchWidth + extraWidth,
                     height: swatchHeight,
-                    subImage: { x: imageFromX, y: imageFromY, width: (swatchWidth - 1), height: swatchHeight },
-                    imageURL: "http://highfidelity-public.s3-us-west-1.amazonaws.com/images/swatches.svg",
+                    subImage: { x: imageFromX, y: imageFromY, width: swatchWidth + extraWidth, height: swatchHeight },
+                    imageURL: toolIconUrl + "swatches.svg",
                     color: colors[s],
                     alpha: 1,
                     visible: editToolsOn
@@ -174,66 +187,41 @@ for (s = 0; s < numColors; s++) {
 
 
 // These will be our tool palette overlays
-var numberOfTools = 5;
-var toolHeight = 40;
-var toolWidth = 62;
-var toolsHeight = toolHeight * numberOfTools;
-var toolsX = 0;
+var numberOfTools = 3;
+var toolHeight = 50;
+var toolWidth = 50;
+var toolVerticalSpacing = 4;
+var toolsHeight = toolHeight * numberOfTools + toolVerticalSpacing * (numberOfTools - 1);
+var toolsX = 8;
 var toolsY = (windowDimensions.y - toolsHeight) / 2;
 
-var addToolAt = 0;
-var deleteToolAt = 1;
-var recolorToolAt = 2;
-var eyedropperToolAt = 3;
-var selectToolAt = 4;
-var toolSelectedColor = { red: 255, green: 255, blue: 255 };
-var notSelectedColor = { red: 128, green: 128, blue: 128 };
+var voxelToolAt = 0;
+var recolorToolAt = 1;
+var eyedropperToolAt = 2;
 
-var addTool = Overlays.addOverlay("image", {
+var voxelTool = Overlays.addOverlay("image", {
                     x: 0, y: 0, width: toolWidth, height: toolHeight,
-                    subImage: { x: 0, y: toolHeight * addToolAt, width: toolWidth, height: toolHeight },
-                    imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/hifi-interface-tools.svg",
-                    color: toolSelectedColor,
-                    visible: false,
-                    alpha: 0.9
-                });
-
-var deleteTool = Overlays.addOverlay("image", {
-                    x: 0, y: 0, width: toolWidth, height: toolHeight,
-                    subImage: { x: 0, y: toolHeight * deleteToolAt, width: toolWidth, height: toolHeight },
-                    imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/hifi-interface-tools.svg",
-                    color: toolSelectedColor,
+                    subImage: { x: 0, y: toolHeight, width: toolWidth, height: toolHeight },
+                    imageURL: toolIconUrl + "voxel-tool.svg",
                     visible: false,
                     alpha: 0.9
                 });
 
 var recolorTool = Overlays.addOverlay("image", {
                     x: 0, y: 0, width: toolWidth, height: toolHeight,
-                    subImage: { x: 0, y: toolHeight * recolorToolAt, width: toolWidth, height: toolHeight },
-                    imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/hifi-interface-tools.svg",
-                    color: toolSelectedColor,
+                    subImage: { x: 0, y: toolHeight, width: toolWidth, height: toolHeight },
+                    imageURL: toolIconUrl + "paint-tool.svg",
                     visible: false,
                     alpha: 0.9
                 });
 
 var eyedropperTool = Overlays.addOverlay("image", {
                     x: 0, y: 0, width: toolWidth, height: toolHeight,
-                    subImage: { x: 0, y: toolHeight * eyedropperToolAt, width: toolWidth, height: toolHeight },
-                    imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/hifi-interface-tools.svg",
-                    color: toolSelectedColor,
+                    subImage: { x: 0, y: toolHeight, width: toolWidth, height: toolHeight },
+                    imageURL: toolIconUrl + "eyedropper-tool.svg",
                     visible: false,
                     alpha: 0.9
                 });
-
-var selectTool = Overlays.addOverlay("image", {
-                    x: 0, y: 0, width: toolWidth, height: toolHeight,
-                    subImage: { x: 0, y: toolHeight * selectToolAt, width: toolWidth, height: toolHeight },
-                    imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/hifi-interface-tools.svg",
-                    color: toolSelectedColor,
-                    visible: false,
-                    alpha: 0.9
-                });
-                
                 
 // This will create a couple of image overlays that make a "slider", we will demonstrate how to trap mouse messages to
 // move the slider
@@ -242,35 +230,34 @@ var selectTool = Overlays.addOverlay("image", {
 //var sliderWidth = 158;
 //var sliderHeight = 35;
 
-var sliderX = swatchesX + swatchesWidth;
-var sliderY = windowDimensions.y - sliderHeight;
+var sliderOffsetX = 17;
+var sliderX = swatchesX - swatchWidth - sliderOffsetX;
+var sliderY = windowDimensions.y - sliderHeight + 1;
 var slider = Overlays.addOverlay("image", {
                     // alternate form of expressing bounds
                     bounds: { x: sliderX, y: sliderY, width: sliderWidth, height: sliderHeight},
-                    imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/slider.png",
-                    color: { red: 255, green: 255, blue: 255},
+                    imageURL: toolIconUrl + "voxel-size-slider-bg.svg",
                     alpha: 1,
                     visible: false
                 });
 
-
 // The slider is handled in the mouse event callbacks.
 var isMovingSlider = false;
 var thumbClickOffsetX = 0;
 
 // This is the thumb of our slider
-var minThumbX = 30; // relative to the x of the slider
-var maxThumbX = minThumbX + 65;
+var minThumbX = 20; // relative to the x of the slider
+var maxThumbX = minThumbX + 90;
 var thumbExtents = maxThumbX - minThumbX;
 var thumbX = (minThumbX + maxThumbX) / 2;
-var thumbY = sliderY + 9;
+var thumbOffsetY = 11;
+var thumbY = sliderY + thumbOffsetY;
 var thumb = Overlays.addOverlay("image", {
                     x: sliderX + thumbX,
                     y: thumbY,
-                    width: 18,
+                    width: 17,
                     height: 17,
-                    imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/thumb.png",
-                    color: { red: 255, green: 255, blue: 255},
+                    imageURL: toolIconUrl + "voxel-size-slider-handle.svg",
                     alpha: 1,
                     visible: false
                 });
@@ -347,11 +334,9 @@ var trackAsRecolor = false;
 var trackAsEyedropper = false;
 var trackAsOrbitOrPan = false;
 
-var addToolSelected = true;
-var deleteToolSelected = false;
+var voxelToolSelected = true;
 var recolorToolSelected = false;
 var eyedropperToolSelected = false;
-var selectToolSelected = false;
 
 function playRandomAddSound(audioOptions) {
     if (Math.random() < 0.33) {
@@ -524,51 +509,12 @@ function showPreviewVoxel() {
     }
 
     var guidePosition;
-    
-    if (trackAsDelete || deleteToolSelected) {
-        guidePosition = calculateVoxelFromIntersection(intersection,"delete");
-        Overlays.editOverlay(voxelPreview, { 
-                position: guidePosition,
-                size: guidePosition.s + zFightingSizeAdjust,
-                visible: true,
-                color: { red: 255, green: 0, blue: 0 },
-                solid: false,
-                alpha: 1
-            });
-    } else if (selectToolSelected) {
-        guidePosition = calculateVoxelFromIntersection(intersection,"select");
-        Overlays.editOverlay(voxelPreview, { 
-                position: guidePosition,
-                size: guidePosition.s + zFightingSizeAdjust,
-                visible: true,
-                color: { red: 255, green: 255, blue: 0 },
-                solid: false,
-                alpha: 1
-            });
-    } else if (trackAsRecolor || recolorToolSelected || trackAsEyedropper|| eyedropperToolSelected) {
-        guidePosition = calculateVoxelFromIntersection(intersection,"recolor");
-
-        Overlays.editOverlay(voxelPreview, { 
-                position: guidePosition,
-                size: guidePosition.s + zFightingSizeAdjust,
-                visible: true,
-                color: voxelColor,
-                solid: true,
-                alpha: 0.8
-            });
+    if (trackAsRecolor || recolorToolSelected || trackAsEyedropper || eyedropperToolSelected) {
+        Overlays.editOverlay(voxelPreview, { visible: true });
     } else if (trackAsOrbitOrPan) {
         Overlays.editOverlay(voxelPreview, { visible: false });
-    } else if (addToolSelected && !isExtruding) {
-        guidePosition = calculateVoxelFromIntersection(intersection,"add");
-
-        Overlays.editOverlay(voxelPreview, { 
-                position: guidePosition,
-                size: (guidePosition.s - zFightingSizeAdjust),
-                visible: true,
-                color: voxelColor,
-                solid: true,
-                alpha: 0.7
-            });
+    } else if (voxelToolSelected && !isExtruding) {
+        Overlays.editOverlay(voxelPreview, { visible: true });
     } else if (isExtruding) {
         Overlays.editOverlay(voxelPreview, { visible: false });
     }
@@ -587,23 +533,11 @@ function showPreviewLines() {
         }
 
         resultVoxel = calculateVoxelFromIntersection(intersection,"");
-        if (selectToolSelected) {
-            Overlays.editOverlay(voxelPreview, { 
-                    position: resultVoxel,
-                    size: resultVoxel.s + zFightingSizeAdjust,
-                    visible: true,
-                    color: { red: 255, green: 255, blue: 0 },
-                    lineWidth: previewLineWidth,
-                    solid: false,
-                    alpha: 1
-                });
-        } else {
-            Overlays.editOverlay(voxelPreview, { visible: false });
-            Overlays.editOverlay(linePreviewTop, { position: resultVoxel.topLeft, end: resultVoxel.topRight, visible: true });
-            Overlays.editOverlay(linePreviewBottom, { position: resultVoxel.bottomLeft, end: resultVoxel.bottomRight, visible: true });
-            Overlays.editOverlay(linePreviewLeft, { position: resultVoxel.topLeft, end: resultVoxel.bottomLeft, visible: true });
-            Overlays.editOverlay(linePreviewRight, { position: resultVoxel.topRight, end: resultVoxel.bottomRight, visible: true });
-        }
+        Overlays.editOverlay(voxelPreview, { visible: false });
+        Overlays.editOverlay(linePreviewTop, { position: resultVoxel.topLeft, end: resultVoxel.topRight, visible: true });
+        Overlays.editOverlay(linePreviewBottom, { position: resultVoxel.bottomLeft, end: resultVoxel.bottomRight, visible: true });
+        Overlays.editOverlay(linePreviewLeft, { position: resultVoxel.topLeft, end: resultVoxel.bottomLeft, visible: true });
+        Overlays.editOverlay(linePreviewRight, { position: resultVoxel.topRight, end: resultVoxel.bottomRight, visible: true });
     } else {
         Overlays.editOverlay(voxelPreview, { visible: false });
         Overlays.editOverlay(linePreviewTop, { visible: false });
@@ -829,44 +763,39 @@ function mousePressEvent(event) {
             isMovingSlider = true;
             thumbClickOffsetX = event.x - (sliderX + thumbX); // this should be the position of the mouse relative to the thumb
             clickedOnSomething = true;
-        } else if (clickedOverlay == addTool) {
-            addToolSelected = true;
-            deleteToolSelected = false;
+
+            Overlays.editOverlay(thumb, { imageURL: toolIconUrl + "voxel-size-slider-handle.svg", });
+
+        } else if (clickedOverlay == voxelTool) {
+            voxelToolSelected = true;
             recolorToolSelected = false;
             eyedropperToolSelected = false;
-            selectToolSelected = false;
-            moveTools();
-            clickedOnSomething = true;
-        } else if (clickedOverlay == deleteTool) {
-            addToolSelected = false;
-            deleteToolSelected = true;
-            recolorToolSelected = false;
-            eyedropperToolSelected = false;
-            selectToolSelected = false;
             moveTools();
             clickedOnSomething = true;
         } else if (clickedOverlay == recolorTool) {
-            addToolSelected = false;
-            deleteToolSelected = false;
+            voxelToolSelected = false;
             recolorToolSelected = true;
             eyedropperToolSelected = false;
-            selectToolSelected = false;
             moveTools();
             clickedOnSomething = true;
         } else if (clickedOverlay == eyedropperTool) {
-            addToolSelected = false;
-            deleteToolSelected = false;
+            voxelToolSelected = false;
             recolorToolSelected = false;
             eyedropperToolSelected = true;
-            selectToolSelected = false;
             moveTools();
             clickedOnSomething = true;
-        } else if (clickedOverlay == selectTool) {
-            addToolSelected = false;
-            deleteToolSelected = false;
-            recolorToolSelected = false;
-            eyedropperToolSelected = false;
-            selectToolSelected = true;
+        } else if (clickedOverlay == slider) {
+
+            if (event.x < sliderX + minThumbX) {
+                thumbX -= thumbDeltaPerStep;
+                calcScaleFromThumb(thumbX);
+            }
+
+            if (event.x > sliderX + maxThumbX) {
+                thumbX += thumbDeltaPerStep;
+                calcScaleFromThumb(thumbX);
+            }
+
             moveTools();
             clickedOnSomething = true;
         } else {
@@ -908,7 +837,7 @@ function mousePressEvent(event) {
                 startPanMode(event);
                 isPanning = true;
             }
-        } else if (deleteToolSelected || trackAsDelete || (event.isRightButton && !trackAsEyedropper)) {
+        } else if (trackAsDelete || event.isRightButton && !trackAsEyedropper) {
             //  Delete voxel
             voxelDetails = calculateVoxelFromIntersection(intersection,"delete");
             Voxels.eraseVoxel(voxelDetails.x, voxelDetails.y, voxelDetails.z, voxelDetails.s);
@@ -932,7 +861,7 @@ function mousePressEvent(event) {
                             colors[whichColor].red, colors[whichColor].green, colors[whichColor].blue);
             Audio.playSound(changeColorSound, audioOptions);
             Overlays.editOverlay(voxelPreview, { visible: false });
-        } else if (addToolSelected) {
+        } else if (voxelToolSelected) {
             //  Add voxel on face
             if (whichColor == -1) {
                 //  Copy mode - use clicked voxel color
@@ -968,7 +897,7 @@ function keyPressEvent(event) {
     // if our tools are off, then don't do anything
     if (editToolsOn) {
         var nVal = parseInt(event.text);
-        if (event.text == "0") {
+        if (event.text == "`") {
             print("Color = Copy");
             whichColor = -1;
             Audio.playSound(clickSound, audioOptions);
@@ -978,7 +907,7 @@ function keyPressEvent(event) {
             print("Color = " + (whichColor + 1));
             Audio.playSound(clickSound, audioOptions);
             moveTools();
-        } else if (event.text == "9") {
+        } else if (event.text == "0") {
             // Create a brand new 1 meter voxel in front of your avatar 
             var color = whichColor; 
             if (color == -1) color = 0;
@@ -1010,18 +939,6 @@ function keyPressEvent(event) {
 
 function keyReleaseEvent(event) {
     trackKeyReleaseEvent(event); // used by preview support
-
-    // handle clipboard items
-    if (selectToolSelected) {
-        // menu tied to BACKSPACE, so we handle DELETE key here...
-        if (event.text == "DELETE") {
-            var pickRay = Camera.computePickRay(trackLastMouseX, trackLastMouseY);
-            var intersection = Voxels.findRayIntersection(pickRay);
-            selectedVoxel = calculateVoxelFromIntersection(intersection,"select");
-            print("the DELETE key was pressed... delete");
-            Clipboard.deleteVoxel(selectedVoxel.x, selectedVoxel.y, selectedVoxel.z, selectedVoxel.s);
-        }
-    }
 }
 
 function setupMenus() {
@@ -1186,22 +1103,33 @@ function mouseReleaseEvent(event) {
 function moveTools() {
     // move the swatches
     swatchesX = (windowDimensions.x - (swatchesWidth + sliderWidth)) / 2;
-    swatchesY = windowDimensions.y - swatchHeight;
+    swatchesY = windowDimensions.y - swatchHeight + 1;
 
     // create the overlays, position them in a row, set their colors, and for the selected one, use a different source image
     // location so that it displays the "selected" marker
     for (s = 0; s < numColors; s++) {
-        var imageFromX = 12 + (s * 27);
-        var imageFromY = 0;
-        if (s == whichColor) {
-            imageFromY = 55;
-        }
-        var swatchX = swatchesX + ((swatchWidth - 1) * s);
+	    var extraWidth = 0;
 
+	    if (s == 0) {
+	        extraWidth = swatchExtraPadding;
+	    }
+
+	    var imageFromX = swatchExtraPadding - extraWidth + s * swatchWidth;
+	    var imageFromY = swatchHeight + 1;
+	    if (s == whichColor) {
+	        imageFromY = 0;
+	    }
+
+	    var swatchX = swatchExtraPadding - extraWidth + swatchesX + ((swatchWidth - 1) * s);
+
+	    if (s == (numColors - 1)) {
+	        extraWidth = swatchExtraPadding;
+	    }
+		
         Overlays.editOverlay(swatches[s], {
                         x: swatchX,
                         y: swatchesY,
-                        subImage: { x: imageFromX, y: imageFromY, width: (swatchWidth - 1), height: swatchHeight },
+                        subImage: { x: imageFromX, y: imageFromY, width: swatchWidth + extraWidth, height: swatchHeight },
                         color: colors[s],
                         alpha: 1,
                         visible: editToolsOn
@@ -1210,62 +1138,45 @@ function moveTools() {
 
     // move the tools
     toolsY = (windowDimensions.y - toolsHeight) / 2;
-    addToolColor = notSelectedColor;
-    deleteToolColor = notSelectedColor;
-    recolorToolColor = notSelectedColor;
-    eyedropperToolColor = notSelectedColor;
-    selectToolColor = notSelectedColor;
 
-    if (trackAsDelete || deleteToolSelected) {
-        deleteToolColor = toolSelectedColor;
-    } else if (trackAsRecolor || recolorToolSelected) {
-        recolorToolColor = toolSelectedColor;
+    var voxelToolOffset = 1,
+        recolorToolOffset = 1,
+        eyedropperToolOffset = 1;
+
+    if (trackAsRecolor || recolorToolSelected) {
+        recolorToolOffset = 2;
     } else if (trackAsEyedropper || eyedropperToolSelected) {
-        eyedropperToolColor = toolSelectedColor;
-    } else if (selectToolSelected) {
-        selectToolColor = toolSelectedColor;
+        eyedropperToolOffset = 2;
     } else if (trackAsOrbitOrPan) {
         // nothing gets selected in this case...
     } else {
-        addToolColor = toolSelectedColor;
+        voxelToolOffset = 2;
     }
 
-    Overlays.editOverlay(addTool, {
-                    x: 0, y: toolsY + (toolHeight * addToolAt), width: toolWidth, height: toolHeight,
-                    color: addToolColor,
-                    visible: editToolsOn
-                });
-
-    Overlays.editOverlay(deleteTool, {
-                    x: 0, y: toolsY + (toolHeight * deleteToolAt), width: toolWidth, height: toolHeight,
-                    color: deleteToolColor,
+    Overlays.editOverlay(voxelTool, {
+                    subImage: { x: 0, y: toolHeight * voxelToolOffset, width: toolWidth, height: toolHeight },
+                    x: toolsX, y: toolsY + ((toolHeight + toolVerticalSpacing) * voxelToolAt), width: toolWidth, height: toolHeight,
                     visible: editToolsOn
                 });
 
     Overlays.editOverlay(recolorTool, {
-                    x: 0, y: toolsY + (toolHeight * recolorToolAt), width: toolWidth, height: toolHeight,
-                    color: recolorToolColor,
+                    subImage: { x: 0, y: toolHeight * recolorToolOffset, width: toolWidth, height: toolHeight },
+                    x: toolsX, y: toolsY + ((toolHeight + toolVerticalSpacing) * recolorToolAt), width: toolWidth, height: toolHeight,
                     visible: editToolsOn
                 });
 
     Overlays.editOverlay(eyedropperTool, {
-                    x: 0, y: toolsY + (toolHeight * eyedropperToolAt), width: toolWidth, height: toolHeight,
-                    color: eyedropperToolColor,
+                    subImage: { x: 0, y: toolHeight * eyedropperToolOffset, width: toolWidth, height: toolHeight },
+                    x: toolsX, y: toolsY + ((toolHeight + toolVerticalSpacing) * eyedropperToolAt), width: toolWidth, height: toolHeight,
                     visible: editToolsOn
                 });
 
-    Overlays.editOverlay(selectTool, {
-                    x: 0, y: toolsY + (toolHeight * selectToolAt), width: toolWidth, height: toolHeight,
-                    color: selectToolColor,
-                    visible: editToolsOn
-                });
-
-    sliderX = swatchesX + swatchesWidth;
-    sliderY = windowDimensions.y - sliderHeight;
+    sliderX = swatchesX + swatchesWidth - sliderOffsetX;
+    sliderY = windowDimensions.y - sliderHeight + 1;
+    thumbY = sliderY + thumbOffsetY;
     Overlays.editOverlay(slider, { x: sliderX, y: sliderY, visible: editToolsOn });
 
     // This is the thumb of our slider
-    thumbY = sliderY + 9;
     Overlays.editOverlay(thumb, { x: sliderX + thumbX, y: thumbY, visible: editToolsOn });
 
 }
@@ -1421,11 +1332,9 @@ function scriptEnding() {
     for (s = 0; s < numColors; s++) {
         Overlays.deleteOverlay(swatches[s]);
     }
-    Overlays.deleteOverlay(addTool);
-    Overlays.deleteOverlay(deleteTool);
+    Overlays.deleteOverlay(voxelTool);
     Overlays.deleteOverlay(recolorTool);
     Overlays.deleteOverlay(eyedropperTool);
-    Overlays.deleteOverlay(selectTool);
     Overlays.deleteOverlay(slider);
     Overlays.deleteOverlay(thumb);
     Controller.releaseKeyEvents({ text: "+" });
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 762ada8eea..2efba6f98f 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -58,6 +58,7 @@
 #include <PacketHeaders.h>
 #include <ParticlesScriptingInterface.h>
 #include <PerfStat.h>
+#include <ResourceCache.h>
 #include <UUID.h>
 #include <VoxelSceneStats.h>
 
@@ -275,6 +276,9 @@ Application::Application(int& argc, char** argv, timeval &startup_time) :
     cache->setCacheDirectory(!cachePath.isEmpty() ? cachePath : "interfaceCache");
     _networkAccessManager->setCache(cache);
 
+    ResourceCache::setNetworkAccessManager(_networkAccessManager);
+    ResourceCache::setRequestLimit(3);
+
     _window->setCentralWidget(_glWidget);
 
     restoreSizeAndPosition();
diff --git a/interface/src/MetavoxelSystem.cpp b/interface/src/MetavoxelSystem.cpp
index 59a714ece5..eeb92be5ec 100644
--- a/interface/src/MetavoxelSystem.cpp
+++ b/interface/src/MetavoxelSystem.cpp
@@ -35,9 +35,6 @@ void MetavoxelSystem::init() {
         _program.link();
        
         _pointScaleLocation = _program.uniformLocation("pointScale");
-        
-        // let the script cache know to use our common access manager
-        ScriptCache::getInstance()->setNetworkAccessManager(Application::getInstance()->getNetworkAccessManager());
     }
     _buffer.setUsagePattern(QOpenGLBuffer::DynamicDraw);
     _buffer.create();
diff --git a/interface/src/renderer/GeometryCache.cpp b/interface/src/renderer/GeometryCache.cpp
index 13563be200..e9719e7a84 100644
--- a/interface/src/renderer/GeometryCache.cpp
+++ b/interface/src/renderer/GeometryCache.cpp
@@ -8,7 +8,6 @@
 #include <cmath>
 
 #include <QNetworkReply>
-#include <QTimer>
 
 #include "Application.h"
 #include "GeometryCache.h"
@@ -287,53 +286,25 @@ void GeometryCache::renderGrid(int xDivisions, int yDivisions) {
 }
 
 QSharedPointer<NetworkGeometry> GeometryCache::getGeometry(const QUrl& url, const QUrl& fallback, bool delayLoad) {
-    if (!url.isValid() && fallback.isValid()) {
-        return getGeometry(fallback, QUrl(), delayLoad);
-    }
-    QSharedPointer<NetworkGeometry> geometry = _networkGeometry.value(url);
-    if (geometry.isNull()) {
-        geometry = QSharedPointer<NetworkGeometry>(new NetworkGeometry(url, fallback.isValid() ?
-            getGeometry(fallback, QUrl(), true) : QSharedPointer<NetworkGeometry>(), delayLoad));
-        geometry->setLODParent(geometry);
-        _networkGeometry.insert(url, geometry);
-    }
-    return geometry;
+    return getResource(url, fallback, delayLoad).staticCast<NetworkGeometry>();
+}
+
+QSharedPointer<Resource> GeometryCache::createResource(const QUrl& url,
+        const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra) {
+    
+    QSharedPointer<NetworkGeometry> geometry(new NetworkGeometry(url, fallback.staticCast<NetworkGeometry>(), delayLoad));
+    geometry->setLODParent(geometry);
+    return geometry.staticCast<Resource>();
 }
 
 const float NetworkGeometry::NO_HYSTERESIS = -1.0f;
 
 NetworkGeometry::NetworkGeometry(const QUrl& url, const QSharedPointer<NetworkGeometry>& fallback, bool delayLoad,
         const QVariantHash& mapping, const QUrl& textureBase) :
-    _request(url),
-    _reply(NULL),
+    Resource(url, delayLoad),
     _mapping(mapping),
     _textureBase(textureBase.isValid() ? textureBase : url),
-    _fallback(fallback),
-    _startedLoading(false),
-    _failedToLoad(false),
-    _attempts(0) {
-    
-    if (!url.isValid()) {
-        return;
-    }
-    _request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
-    
-    // start loading immediately unless instructed otherwise
-    if (!delayLoad) {    
-        makeRequest();
-    }
-}
-
-NetworkGeometry::~NetworkGeometry() {
-    if (_reply != NULL) {
-        delete _reply;
-    }
-}
-
-void NetworkGeometry::ensureLoading() {
-    if (!_startedLoading) {
-        makeRequest();
-    }
+    _fallback(fallback) {
 }
 
 QSharedPointer<NetworkGeometry> NetworkGeometry::getLODOrFallback(float distance, float& hysteresis, bool delayLoad) const {
@@ -406,24 +377,60 @@ glm::vec4 NetworkGeometry::computeAverageColor() const {
     return (totalTriangles == 0) ? glm::vec4(1.0f, 1.0f, 1.0f, 1.0f) : totalColor / totalTriangles;
 }
 
-void NetworkGeometry::makeRequest() {
-    _startedLoading = true;
-    _reply = Application::getInstance()->getNetworkAccessManager()->get(_request);
+void NetworkGeometry::setLoadPriority(const QPointer<QObject>& owner, float priority) {
+    Resource::setLoadPriority(owner, priority);
     
-    connect(_reply, SIGNAL(downloadProgress(qint64,qint64)), SLOT(handleDownloadProgress(qint64,qint64)));
-    connect(_reply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(handleReplyError()));
+    for (int i = 0; i < _meshes.size(); i++) {
+        NetworkMesh& mesh = _meshes[i];
+        for (int j = 0; j < mesh.parts.size(); j++) {
+            NetworkMeshPart& part = mesh.parts[j];
+            if (part.diffuseTexture) {
+                part.diffuseTexture->setLoadPriority(owner, priority);
+            }
+            if (part.normalTexture) {
+                part.normalTexture->setLoadPriority(owner, priority);
+            }
+        }
+    }
 }
 
-void NetworkGeometry::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
-    if (!_reply->isFinished()) {
-        return;
-    }
+void NetworkGeometry::setLoadPriorities(const QHash<QPointer<QObject>, float>& priorities) {
+    Resource::setLoadPriorities(priorities);
     
-    QUrl url = _reply->url();
-    QByteArray data = _reply->readAll();
-    _reply->disconnect(this);
-    _reply->deleteLater();
-    _reply = NULL;
+    for (int i = 0; i < _meshes.size(); i++) {
+        NetworkMesh& mesh = _meshes[i];
+        for (int j = 0; j < mesh.parts.size(); j++) {
+            NetworkMeshPart& part = mesh.parts[j];
+            if (part.diffuseTexture) {
+                part.diffuseTexture->setLoadPriorities(priorities);
+            }
+            if (part.normalTexture) {
+                part.normalTexture->setLoadPriorities(priorities);
+            }
+        }
+    }
+}
+
+void NetworkGeometry::clearLoadPriority(const QPointer<QObject>& owner) {
+    Resource::clearLoadPriority(owner);
+    
+    for (int i = 0; i < _meshes.size(); i++) {
+        NetworkMesh& mesh = _meshes[i];
+        for (int j = 0; j < mesh.parts.size(); j++) {
+            NetworkMeshPart& part = mesh.parts[j];
+            if (part.diffuseTexture) {
+                part.diffuseTexture->clearLoadPriority(owner);
+            }
+            if (part.normalTexture) {
+                part.normalTexture->clearLoadPriority(owner);
+            }
+        }
+    }
+}
+
+void NetworkGeometry::downloadFinished(QNetworkReply* reply) {
+    QUrl url = reply->url();
+    QByteArray data = reply->readAll();
     
     if (url.path().toLower().endsWith(".fst")) {
         // it's a mapping file; parse it and get the mesh filename
@@ -453,7 +460,7 @@ void NetworkGeometry::handleDownloadProgress(qint64 bytesReceived, qint64 bytesT
             // make the request immediately only if we have no LODs to switch between
             _startedLoading = false;
             if (_lods.isEmpty()) {
-                makeRequest();
+                attemptRequest();
             }
         }
         return;
@@ -477,10 +484,12 @@ void NetworkGeometry::handleDownloadProgress(qint64 bytesReceived, qint64 bytesT
             if (!part.diffuseFilename.isEmpty()) {
                 networkPart.diffuseTexture = Application::getInstance()->getTextureCache()->getTexture(
                     _textureBase.resolved(QUrl(part.diffuseFilename)), false, mesh.isEye);
+                networkPart.diffuseTexture->setLoadPriorities(_loadPriorities);
             }
             if (!part.normalFilename.isEmpty()) {
                 networkPart.normalTexture = Application::getInstance()->getTextureCache()->getTexture(
                     _textureBase.resolved(QUrl(part.normalFilename)), true);
+                networkPart.normalTexture->setLoadPriorities(_loadPriorities);
             }
             networkMesh.parts.append(networkPart);
                         
@@ -560,42 +569,6 @@ void NetworkGeometry::handleDownloadProgress(qint64 bytesReceived, qint64 bytesT
     }
 }
 
-void NetworkGeometry::handleReplyError() {
-    QDebug debug = qDebug() << _reply->errorString();
-    
-    QNetworkReply::NetworkError error = _reply->error();
-    _reply->disconnect(this);
-    _reply->deleteLater();
-    _reply = NULL;
-    
-    // retry for certain types of failures
-    switch (error) {
-        case QNetworkReply::RemoteHostClosedError:
-        case QNetworkReply::TimeoutError:
-        case QNetworkReply::TemporaryNetworkFailureError:
-        case QNetworkReply::ProxyConnectionClosedError:
-        case QNetworkReply::ProxyTimeoutError:
-        case QNetworkReply::UnknownNetworkError:
-        case QNetworkReply::UnknownProxyError:
-        case QNetworkReply::UnknownContentError:
-        case QNetworkReply::ProtocolFailure: {        
-            // retry with increasing delays
-            const int MAX_ATTEMPTS = 8;
-            const int BASE_DELAY_MS = 1000;
-            if (++_attempts < MAX_ATTEMPTS) {
-                QTimer::singleShot(BASE_DELAY_MS * (int)pow(2.0, _attempts), this, SLOT(makeRequest()));
-                debug << " -- retrying...";
-                return;
-            }
-            // fall through to final failure
-        }    
-        default:
-            _failedToLoad = true;
-            break;
-    }
-    
-}
-
 bool NetworkMeshPart::isTranslucent() const {
     return diffuseTexture && diffuseTexture->isTranslucent();
 }
diff --git a/interface/src/renderer/GeometryCache.h b/interface/src/renderer/GeometryCache.h
index a1d159be31..e5139088d3 100644
--- a/interface/src/renderer/GeometryCache.h
+++ b/interface/src/renderer/GeometryCache.h
@@ -12,24 +12,19 @@
 // include this before QOpenGLBuffer, which includes an earlier version of OpenGL
 #include "InterfaceConfig.h"
 
-#include <QHash>
 #include <QMap>
-#include <QNetworkRequest>
-#include <QObject>
 #include <QOpenGLBuffer>
-#include <QSharedPointer>
-#include <QWeakPointer>
+
+#include <ResourceCache.h>
 
 #include "FBXReader.h"
 
-class QNetworkReply;
-
 class NetworkGeometry;
 class NetworkMesh;
 class NetworkTexture;
 
 /// Stores cached geometry.
-class GeometryCache {
+class GeometryCache : public ResourceCache {
 public:
     
     ~GeometryCache();
@@ -44,6 +39,11 @@ public:
     /// \param delayLoad if true, don't load the geometry immediately; wait until load is first requested
     QSharedPointer<NetworkGeometry> getGeometry(const QUrl& url, const QUrl& fallback = QUrl(), bool delayLoad = false);
     
+protected:
+
+    virtual QSharedPointer<Resource> createResource(const QUrl& url,
+        const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra);
+        
 private:
     
     typedef QPair<int, int> IntPair;
@@ -58,7 +58,7 @@ private:
 };
 
 /// Geometry loaded from the network.
-class NetworkGeometry : public QObject {
+class NetworkGeometry : public Resource {
     Q_OBJECT
 
 public:
@@ -68,14 +68,10 @@ public:
     
     NetworkGeometry(const QUrl& url, const QSharedPointer<NetworkGeometry>& fallback, bool delayLoad,
         const QVariantHash& mapping = QVariantHash(), const QUrl& textureBase = QUrl());
-    ~NetworkGeometry();
 
     /// Checks whether the geometry is fulled loaded.
     bool isLoaded() const { return !_geometry.joints.isEmpty(); }
 
-    /// Makes sure that the geometry has started loading.
-    void ensureLoading();
-
     /// Returns a pointer to the geometry appropriate for the specified distance.
     /// \param hysteresis a hysteresis parameter that prevents rapid model switching
     QSharedPointer<NetworkGeometry> getLODOrFallback(float distance, float& hysteresis, bool delayLoad = false) const;
@@ -86,11 +82,13 @@ public:
     /// Returns the average color of all meshes in the geometry.
     glm::vec4 computeAverageColor() const;
 
-private slots:
+    virtual void setLoadPriority(const QPointer<QObject>& owner, float priority);
+    virtual void setLoadPriorities(const QHash<QPointer<QObject>, float>& priorities);
+    virtual void clearLoadPriority(const QPointer<QObject>& owner);
     
-    void makeRequest();
-    void handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
-    void handleReplyError();
+protected:
+
+    virtual void downloadFinished(QNetworkReply* reply);
     
 private:
     
@@ -98,15 +96,10 @@ private:
     
     void setLODParent(const QWeakPointer<NetworkGeometry>& lodParent) { _lodParent = lodParent; }
     
-    QNetworkRequest _request;
-    QNetworkReply* _reply;
     QVariantHash _mapping;
     QUrl _textureBase;
     QSharedPointer<NetworkGeometry> _fallback;
-    bool _startedLoading;
-    bool _failedToLoad;
     
-    int _attempts;
     QMap<float, QSharedPointer<NetworkGeometry> > _lods;
     FBXGeometry _geometry;
     QVector<NetworkMesh> _meshes;
diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp
index 55594fa3dc..599abf29aa 100644
--- a/interface/src/renderer/Model.cpp
+++ b/interface/src/renderer/Model.cpp
@@ -139,6 +139,7 @@ void Model::simulate(float deltaTime, bool delayLoad) {
             _geometry = geometry;
         }
         if (!delayLoad) {
+            _geometry->setLoadPriority(this, -_lodDistance);
             _geometry->ensureLoading();
         }
     }
@@ -833,6 +834,10 @@ void Model::deleteGeometry() {
     _blendedVertexBufferIDs.clear();
     _jointStates.clear();
     _meshStates.clear();
+    
+    if (_geometry) {
+        _geometry->clearLoadPriority(this);
+    }
 }
 
 void Model::renderMeshes(float alpha, bool translucent) {
diff --git a/interface/src/renderer/TextureCache.cpp b/interface/src/renderer/TextureCache.cpp
index 8bfef5a742..0a6a3fe6f8 100644
--- a/interface/src/renderer/TextureCache.cpp
+++ b/interface/src/renderer/TextureCache.cpp
@@ -11,7 +11,6 @@
 #include <QGLWidget>
 #include <QNetworkReply>
 #include <QOpenGLFramebufferObject>
-#include <QTimer>
 
 #include <glm/gtc/random.hpp>
 
@@ -124,19 +123,13 @@ GLuint TextureCache::getFileTextureID(const QString& filename) {
 }
 
 QSharedPointer<NetworkTexture> TextureCache::getTexture(const QUrl& url, bool normalMap, bool dilatable) {
-    QSharedPointer<NetworkTexture> texture;
-    if (dilatable) {
-        texture = _dilatableNetworkTextures.value(url);
-        if (texture.isNull()) {
-            texture = QSharedPointer<NetworkTexture>(new DilatableNetworkTexture(url, normalMap));
-            _dilatableNetworkTextures.insert(url, texture);
-        }
-    } else {
-        texture = _networkTextures.value(url);
-        if (texture.isNull()) {
-            texture = QSharedPointer<NetworkTexture>(new NetworkTexture(url, normalMap));
-            _networkTextures.insert(url, texture);
-        }
+    if (!dilatable) {
+        return ResourceCache::getResource(url, QUrl(), false, &normalMap).staticCast<NetworkTexture>();
+    }
+    QSharedPointer<NetworkTexture> texture = _dilatableNetworkTextures.value(url);
+    if (texture.isNull()) {
+        texture = QSharedPointer<NetworkTexture>(new DilatableNetworkTexture(url));
+        _dilatableNetworkTextures.insert(url, texture);
     }
     return texture;
 }
@@ -233,6 +226,11 @@ bool TextureCache::eventFilter(QObject* watched, QEvent* event) {
     return false;
 }
 
+QSharedPointer<Resource> TextureCache::createResource(const QUrl& url,
+        const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra) {
+    return QSharedPointer<Resource>(new NetworkTexture(url, *(const bool*)extra));
+}
+
 QOpenGLFramebufferObject* TextureCache::createFramebufferObject() {
     QOpenGLFramebufferObject* fbo = new QOpenGLFramebufferObject(Application::getInstance()->getGLWidget()->size());
     Application::getInstance()->getGLWidget()->installEventFilter(this);
@@ -254,9 +252,7 @@ Texture::~Texture() {
 }
 
 NetworkTexture::NetworkTexture(const QUrl& url, bool normalMap) :
-    _request(url),
-    _reply(NULL),
-    _attempts(0),
+    Resource(url),
     _averageColor(1.0f, 1.0f, 1.0f, 1.0f),
     _translucent(false),
     _loaded(false) {
@@ -265,8 +261,6 @@ NetworkTexture::NetworkTexture(const QUrl& url, bool normalMap) :
         _loaded = true;
         return;
     }
-    _request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
-    makeRequest();
     
     // default to white/blue
     glBindTexture(GL_TEXTURE_2D, getID());
@@ -274,35 +268,13 @@ NetworkTexture::NetworkTexture(const QUrl& url, bool normalMap) :
     glBindTexture(GL_TEXTURE_2D, 0);
 }
 
-NetworkTexture::~NetworkTexture() {
-    if (_reply != NULL) {
-        delete _reply;
-    }
-}
-
-void NetworkTexture::imageLoaded(const QImage& image) {
-    // nothing by default
-}
-
-void NetworkTexture::makeRequest() {
-    _reply = Application::getInstance()->getNetworkAccessManager()->get(_request);
-    
-    connect(_reply, SIGNAL(downloadProgress(qint64,qint64)), SLOT(handleDownloadProgress(qint64,qint64)));
-    connect(_reply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(handleReplyError()));
-}
-
-void NetworkTexture::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
-    if (bytesReceived < bytesTotal && !_reply->isFinished()) {
-        return;
-    }
-
-    QByteArray entirety = _reply->readAll();
-    _reply->disconnect(this);
-    _reply->deleteLater();
-    _reply = NULL;
+void NetworkTexture::downloadFinished(QNetworkReply* reply) {
     _loaded = true;
     
-    QImage image = QImage::fromData(entirety).convertToFormat(QImage::Format_ARGB32);
+    QImage image = QImage::fromData(reply->readAll());
+    if (image.format() != QImage::Format_ARGB32) {
+        image = image.convertToFormat(QImage::Format_ARGB32);
+    }
     
     // sum up the colors for the average and check for translucency
     glm::vec4 accumulated;
@@ -334,27 +306,12 @@ void NetworkTexture::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTo
     glBindTexture(GL_TEXTURE_2D, 0);
 }
 
-void NetworkTexture::handleReplyError() {
-    QDebug debug = qDebug() << _reply->errorString();
-    
-    _reply->disconnect(this);
-    _reply->deleteLater();
-    _reply = NULL;
-    
-    // retry with increasing delays
-    const int MAX_ATTEMPTS = 8;
-    const int BASE_DELAY_MS = 1000;
-    if (++_attempts < MAX_ATTEMPTS) {
-        QTimer::singleShot(BASE_DELAY_MS * (int)pow(2.0, _attempts), this, SLOT(makeRequest()));
-        debug << " -- retrying...";
-        
-    } else {
-        _loaded = true;
-    }
+void NetworkTexture::imageLoaded(const QImage& image) {
+    // nothing by default
 }
 
-DilatableNetworkTexture::DilatableNetworkTexture(const QUrl& url, bool normalMap) :
-    NetworkTexture(url, normalMap),
+DilatableNetworkTexture::DilatableNetworkTexture(const QUrl& url) :
+    NetworkTexture(url, false),
     _innerRadius(0),
     _outerRadius(0)
 {
diff --git a/interface/src/renderer/TextureCache.h b/interface/src/renderer/TextureCache.h
index ca7bf67a32..2c8f4d7cf9 100644
--- a/interface/src/renderer/TextureCache.h
+++ b/interface/src/renderer/TextureCache.h
@@ -9,23 +9,19 @@
 #ifndef __interface__TextureCache__
 #define __interface__TextureCache__
 
-#include <QHash>
 #include <QImage>
 #include <QMap>
-#include <QNetworkRequest>
-#include <QObject>
-#include <QSharedPointer>
-#include <QWeakPointer>
+
+#include <ResourceCache.h>
 
 #include "InterfaceConfig.h"
 
-class QNetworkReply;
 class QOpenGLFramebufferObject;
 
 class NetworkTexture;
 
 /// Stores cached textures, including render-to-texture targets.
-class TextureCache : public QObject {
+class TextureCache : public ResourceCache {
     Q_OBJECT
     
 public:
@@ -73,6 +69,11 @@ public:
     
     virtual bool eventFilter(QObject* watched, QEvent* event);
 
+protected:
+
+    virtual QSharedPointer<Resource> createResource(const QUrl& url,
+        const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra);
+        
 private:
     
     QOpenGLFramebufferObject* createFramebufferObject();
@@ -83,7 +84,6 @@ private:
     
     QHash<QString, GLuint> _fileTextureIDs;
 
-    QHash<QUrl, QWeakPointer<NetworkTexture> > _networkTextures;
     QHash<QUrl, QWeakPointer<NetworkTexture> > _dilatableNetworkTextures;
     
     GLuint _primaryDepthTextureID;
@@ -110,13 +110,12 @@ private:
 };
 
 /// A texture loaded from the network.
-class NetworkTexture : public QObject, public Texture {
+class NetworkTexture : public Resource, public Texture {
     Q_OBJECT
 
 public:
     
     NetworkTexture(const QUrl& url, bool normalMap);
-    ~NetworkTexture();
 
     bool isLoaded() const { return _loaded; }
 
@@ -129,19 +128,11 @@ public:
 
 protected:
 
-    virtual void imageLoaded(const QImage& image);    
-    
-private slots:
-    
-    void makeRequest();
-    void handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
-    void handleReplyError();    
+    virtual void downloadFinished(QNetworkReply* reply);
+    virtual void imageLoaded(const QImage& image);      
 
 private:
 
-    QNetworkRequest _request;
-    QNetworkReply* _reply;
-    int _attempts;
     glm::vec4 _averageColor;
     bool _translucent;
     bool _loaded;
@@ -153,7 +144,7 @@ class DilatableNetworkTexture : public NetworkTexture {
     
 public:
     
-    DilatableNetworkTexture(const QUrl& url, bool normalMap);
+    DilatableNetworkTexture(const QUrl& url);
     
     /// Returns a pointer to a texture with the requested amount of dilation.
     QSharedPointer<Texture> getDilatedTexture(float dilation);
diff --git a/libraries/metavoxels/src/ScriptCache.cpp b/libraries/metavoxels/src/ScriptCache.cpp
index e7610038f5..da29186541 100644
--- a/libraries/metavoxels/src/ScriptCache.cpp
+++ b/libraries/metavoxels/src/ScriptCache.cpp
@@ -11,8 +11,6 @@
 #include <QNetworkReply>
 #include <QScriptEngine>
 #include <QTextStream>
-#include <QTimer>
-#include <QtDebug>
 
 #include "AttributeRegistry.h"
 #include "ScriptCache.h"
@@ -23,7 +21,6 @@ ScriptCache* ScriptCache::getInstance() {
 }
 
 ScriptCache::ScriptCache() :
-    _networkAccessManager(NULL),
     _engine(NULL) {
     
     setEngine(new QScriptEngine(this));
@@ -41,15 +38,6 @@ void ScriptCache::setEngine(QScriptEngine* engine) {
     _generatorString = engine->toStringHandle("generator");
 }
 
-QSharedPointer<NetworkProgram> ScriptCache::getProgram(const QUrl& url) {
-    QSharedPointer<NetworkProgram> program = _networkPrograms.value(url);
-    if (program.isNull()) {
-        program = QSharedPointer<NetworkProgram>(new NetworkProgram(this, url));
-        _networkPrograms.insert(url, program);
-    }
-    return program;
-}
-
 QSharedPointer<NetworkValue> ScriptCache::getValue(const ParameterizedURL& url) {
     QSharedPointer<NetworkValue> value = _networkValues.value(url);
     if (value.isNull()) {
@@ -61,65 +49,21 @@ QSharedPointer<NetworkValue> ScriptCache::getValue(const ParameterizedURL& url)
     return value;
 }
 
+QSharedPointer<Resource> ScriptCache::createResource(const QUrl& url,
+        const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra) {
+    return QSharedPointer<Resource>(new NetworkProgram(this, url));
+}
+
 NetworkProgram::NetworkProgram(ScriptCache* cache, const QUrl& url) :
-    _cache(cache),
-    _request(url),
-    _reply(NULL),
-    _attempts(0) {
-    
-    if (!url.isValid()) {
-        return;
-    }
-    _request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
-    makeRequest();
+    Resource(url),
+    _cache(cache) {
 }
 
-NetworkProgram::~NetworkProgram() {
-    if (_reply != NULL) {
-        delete _reply;
-    }
-}
-
-void NetworkProgram::makeRequest() {
-    QNetworkAccessManager* manager = _cache->getNetworkAccessManager();
-    if (manager == NULL) {
-        return;
-    }
-    _reply = manager->get(_request);
-    
-    connect(_reply, SIGNAL(downloadProgress(qint64,qint64)), SLOT(handleDownloadProgress(qint64,qint64)));
-    connect(_reply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(handleReplyError()));
-}
-
-void NetworkProgram::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
-    if (bytesReceived < bytesTotal && !_reply->isFinished()) {
-        return;
-    }
-    _program = QScriptProgram(QTextStream(_reply).readAll(), _reply->url().toString());
-    
-    _reply->disconnect(this);
-    _reply->deleteLater();
-    _reply = NULL;
-    
+void NetworkProgram::downloadFinished(QNetworkReply* reply) {
+    _program = QScriptProgram(QTextStream(reply).readAll(), reply->url().toString());
     emit loaded();
 }
 
-void NetworkProgram::handleReplyError() {
-    QDebug debug = qDebug() << _reply->errorString();
-    
-    _reply->disconnect(this);
-    _reply->deleteLater();
-    _reply = NULL;
-    
-    // retry with increasing delays
-    const int MAX_ATTEMPTS = 8;
-    const int BASE_DELAY_MS = 1000;
-    if (++_attempts < MAX_ATTEMPTS) {
-        QTimer::singleShot(BASE_DELAY_MS * (int)pow(2.0, _attempts), this, SLOT(makeRequest()));
-        debug << " -- retrying...";
-    }
-}
-
 NetworkValue::~NetworkValue() {
 }
 
diff --git a/libraries/metavoxels/src/ScriptCache.h b/libraries/metavoxels/src/ScriptCache.h
index 3ce525d979..072020d882 100644
--- a/libraries/metavoxels/src/ScriptCache.h
+++ b/libraries/metavoxels/src/ScriptCache.h
@@ -9,26 +9,20 @@
 #ifndef __interface__ScriptCache__
 #define __interface__ScriptCache__
 
-#include <QHash>
-#include <QList>
-#include <QNetworkRequest>
-#include <QObject>
 #include <QScriptProgram>
 #include <QScriptValue>
-#include <QSharedPointer>
-#include <QWeakPointer>
+
+#include <ResourceCache.h>
 
 #include "MetavoxelUtil.h"
 
-class QNetworkAccessManager;
-class QNetworkReply;
 class QScriptEngine;
 
 class NetworkProgram;
 class NetworkValue;
 
 /// Maintains a cache of loaded scripts.
-class ScriptCache : public QObject {
+class ScriptCache : public ResourceCache {
     Q_OBJECT
 
 public:
@@ -37,14 +31,11 @@ public:
 
     ScriptCache();
     
-    void setNetworkAccessManager(QNetworkAccessManager* manager) { _networkAccessManager = manager; }
-    QNetworkAccessManager* getNetworkAccessManager() const { return _networkAccessManager; }
-    
     void setEngine(QScriptEngine* engine);
     QScriptEngine* getEngine() const { return _engine; }
     
     /// Loads a script program from the specified URL.
-    QSharedPointer<NetworkProgram> getProgram(const QUrl& url);
+    QSharedPointer<NetworkProgram> getProgram(const QUrl& url) { return getResource(url).staticCast<NetworkProgram>(); }
 
     /// Loads a script value from the specified URL.
     QSharedPointer<NetworkValue> getValue(const ParameterizedURL& url);
@@ -55,11 +46,14 @@ public:
     const QScriptString& getTypeString() const { return _typeString; }
     const QScriptString& getGeneratorString() const { return _generatorString; }
 
+protected:
+
+    virtual QSharedPointer<Resource> createResource(const QUrl& url,
+        const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra);
+
 private:
     
-    QNetworkAccessManager* _networkAccessManager;
     QScriptEngine* _engine;
-    QHash<QUrl, QWeakPointer<NetworkProgram> > _networkPrograms;
     QHash<ParameterizedURL, QWeakPointer<NetworkValue> > _networkValues;
     QScriptString _parametersString;
     QScriptString _lengthString;
@@ -69,13 +63,12 @@ private:
 };
 
 /// A program loaded from the network.
-class NetworkProgram : public QObject {
+class NetworkProgram : public Resource {
     Q_OBJECT
 
 public:
 
     NetworkProgram(ScriptCache* cache, const QUrl& url);
-    ~NetworkProgram();
     
     ScriptCache* getCache() const { return _cache; }
     
@@ -87,18 +80,13 @@ signals:
 
     void loaded();
 
-private slots:
-    
-    void makeRequest();
-    void handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
-    void handleReplyError(); 
-        
+protected:
+
+    virtual void downloadFinished(QNetworkReply* reply);
+
 private:
     
     ScriptCache* _cache;
-    QNetworkRequest _request;
-    QNetworkReply* _reply;
-    int _attempts;
     QScriptProgram _program;
 };
 
diff --git a/libraries/shared/src/ResourceCache.cpp b/libraries/shared/src/ResourceCache.cpp
new file mode 100644
index 0000000000..24350794bc
--- /dev/null
+++ b/libraries/shared/src/ResourceCache.cpp
@@ -0,0 +1,199 @@
+//
+//  ResourceCache.cpp
+//  shared
+//
+//  Created by Andrzej Kapolka on 2/27/14.
+//  Copyright (c) 2014 High Fidelity, Inc. All rights reserved.
+//
+
+#include <cfloat>
+#include <cmath>
+
+#include <QNetworkReply>
+#include <QTimer>
+#include <QtDebug>
+
+#include "ResourceCache.h"
+
+ResourceCache::ResourceCache(QObject* parent) :
+    QObject(parent) {
+}
+
+QSharedPointer<Resource> ResourceCache::getResource(const QUrl& url, const QUrl& fallback, bool delayLoad, void* extra) {
+    if (!url.isValid() && fallback.isValid()) {
+        return getResource(fallback, QUrl(), delayLoad);
+    }
+    QSharedPointer<Resource> resource = _resources.value(url);
+    if (resource.isNull()) {
+        resource = createResource(url, fallback.isValid() ?
+            getResource(fallback, QUrl(), true) : QSharedPointer<Resource>(), delayLoad, extra);
+        _resources.insert(url, resource);
+    }
+    return resource;
+}
+
+void ResourceCache::attemptRequest(Resource* resource) {
+    if (_requestLimit <= 0) {
+        // wait until a slot becomes available
+        _pendingRequests.append(resource);
+        return;
+    }
+    _requestLimit--;
+    resource->makeRequest();
+}
+
+void ResourceCache::requestCompleted() {
+    _requestLimit++;
+    
+    // look for the highest priority pending request
+    int highestIndex = -1;
+    float highestPriority = -FLT_MAX;
+    for (int i = 0; i < _pendingRequests.size(); ) {
+        Resource* resource = _pendingRequests.at(i).data();
+        if (!resource) {
+            _pendingRequests.removeAt(i);
+            continue;
+        }
+        float priority = resource->getLoadPriority();
+        if (priority >= highestPriority) {
+            highestPriority = priority;
+            highestIndex = i;
+        }
+        i++;
+    }
+    if (highestIndex >= 0) {
+        attemptRequest(_pendingRequests.takeAt(highestIndex));
+    }
+}
+
+QNetworkAccessManager* ResourceCache::_networkAccessManager = NULL;
+
+const int DEFAULT_REQUEST_LIMIT = 10;
+int ResourceCache::_requestLimit = DEFAULT_REQUEST_LIMIT;
+
+QList<QPointer<Resource> > ResourceCache::_pendingRequests;
+
+Resource::Resource(const QUrl& url, bool delayLoad) :
+    _request(url),
+    _startedLoading(false),
+    _failedToLoad(false),
+    _attempts(0),
+    _reply(NULL) {
+    
+    if (!url.isValid()) {
+        _startedLoading = _failedToLoad = true;
+        return;
+    }
+    _request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
+    
+    // start loading immediately unless instructed otherwise
+    if (!delayLoad) {    
+        attemptRequest();
+    }
+}
+
+Resource::~Resource() {
+    if (_reply) {
+        ResourceCache::requestCompleted();
+        delete _reply;
+    }
+}
+
+void Resource::ensureLoading() {
+    if (!_startedLoading) {
+        attemptRequest();
+    }
+}
+
+void Resource::setLoadPriority(const QPointer<QObject>& owner, float priority) {
+    _loadPriorities.insert(owner, priority);
+}
+
+void Resource::setLoadPriorities(const QHash<QPointer<QObject>, float>& priorities) {
+    for (QHash<QPointer<QObject>, float>::const_iterator it = priorities.constBegin();
+            it != priorities.constEnd(); it++) {
+        _loadPriorities.insert(it.key(), it.value());
+    }
+}
+
+void Resource::clearLoadPriority(const QPointer<QObject>& owner) {
+    _loadPriorities.remove(owner);
+}
+
+float Resource::getLoadPriority() {
+    float highestPriority = -FLT_MAX;
+    for (QHash<QPointer<QObject>, float>::iterator it = _loadPriorities.begin(); it != _loadPriorities.end(); ) {
+        if (it.key().isNull()) {
+            it = _loadPriorities.erase(it);
+            continue;
+        }
+        highestPriority = qMax(highestPriority, it.value());
+        it++;
+    }
+    return highestPriority;
+}
+
+void Resource::attemptRequest() {
+    _startedLoading = true;
+    ResourceCache::attemptRequest(this);
+}
+
+void Resource::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
+    if (!_reply->isFinished()) {
+        return;
+    }
+    _reply->disconnect(this);
+    _reply->deleteLater();
+    QNetworkReply* reply = _reply;
+    _reply = NULL;
+    ResourceCache::requestCompleted();
+    
+    downloadFinished(reply);
+}
+
+void Resource::handleReplyError() {
+    QDebug debug = qDebug() << _reply->errorString();
+    
+    QNetworkReply::NetworkError error = _reply->error();
+    _reply->disconnect(this);
+    _reply->deleteLater();
+    _reply = NULL;
+    ResourceCache::requestCompleted();
+    
+    // retry for certain types of failures
+    switch (error) {
+        case QNetworkReply::RemoteHostClosedError:
+        case QNetworkReply::TimeoutError:
+        case QNetworkReply::TemporaryNetworkFailureError:
+        case QNetworkReply::ProxyConnectionClosedError:
+        case QNetworkReply::ProxyTimeoutError:
+        case QNetworkReply::UnknownNetworkError:
+        case QNetworkReply::UnknownProxyError:
+        case QNetworkReply::UnknownContentError:
+        case QNetworkReply::ProtocolFailure: {        
+            // retry with increasing delays
+            const int MAX_ATTEMPTS = 8;
+            const int BASE_DELAY_MS = 1000;
+            if (++_attempts < MAX_ATTEMPTS) {
+                QTimer::singleShot(BASE_DELAY_MS * (int)pow(2.0, _attempts), this, SLOT(attemptRequest()));
+                debug << " -- retrying...";
+                return;
+            }
+            // fall through to final failure
+        }    
+        default:
+            _failedToLoad = true;
+            break;
+    }
+}
+
+void Resource::makeRequest() {
+    _reply = ResourceCache::getNetworkAccessManager()->get(_request);
+    
+    connect(_reply, SIGNAL(downloadProgress(qint64,qint64)), SLOT(handleDownloadProgress(qint64,qint64)));
+    connect(_reply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(handleReplyError()));
+}
+
+uint qHash(const QPointer<QObject>& value, uint seed) {
+    return qHash(value.data(), seed);
+}
diff --git a/libraries/shared/src/ResourceCache.h b/libraries/shared/src/ResourceCache.h
new file mode 100644
index 0000000000..ac8ef68a05
--- /dev/null
+++ b/libraries/shared/src/ResourceCache.h
@@ -0,0 +1,121 @@
+//
+//  ResourceCache.h
+//  shared
+//
+//  Created by Andrzej Kapolka on 2/27/14.
+//  Copyright (c) 2014 High Fidelity, Inc. All rights reserved.
+//
+
+#ifndef __shared__ResourceCache__
+#define __shared__ResourceCache__
+
+#include <QHash>
+#include <QList>
+#include <QNetworkRequest>
+#include <QObject>
+#include <QPointer>
+#include <QSharedPointer>
+#include <QUrl>
+#include <QWeakPointer>
+
+class QNetworkAccessManager;
+class QNetworkReply;
+
+class Resource;
+
+/// Base class for resource caches.
+class ResourceCache : public QObject {
+    Q_OBJECT
+    
+public:
+
+    static void setNetworkAccessManager(QNetworkAccessManager* manager) { _networkAccessManager = manager; }
+    static QNetworkAccessManager* getNetworkAccessManager() { return _networkAccessManager; }
+
+    static void setRequestLimit(int limit) { _requestLimit = limit; }
+    static int getRequestLimit() { return _requestLimit; }
+
+    ResourceCache(QObject* parent = NULL);
+
+protected:
+
+    /// Loads a resource from the specified URL.
+    /// \param fallback a fallback URL to load if the desired one is unavailable
+    /// \param delayLoad if true, don't load the resource immediately; wait until load is first requested
+    /// \param extra extra data to pass to the creator, if appropriate
+    QSharedPointer<Resource> getResource(const QUrl& url, const QUrl& fallback = QUrl(),
+        bool delayLoad = false, void* extra = NULL);
+
+    /// Creates a new resource.
+    virtual QSharedPointer<Resource> createResource(const QUrl& url,
+        const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra) = 0;
+
+    static void attemptRequest(Resource* resource);
+    static void requestCompleted();
+
+private:
+    
+    friend class Resource;
+    
+    QHash<QUrl, QWeakPointer<Resource> > _resources;
+    
+    static QNetworkAccessManager* _networkAccessManager;
+    static int _requestLimit;
+    static QList<QPointer<Resource> > _pendingRequests;
+};
+
+/// Base class for resources.
+class Resource : public QObject {
+    Q_OBJECT
+
+public:
+    
+    Resource(const QUrl& url, bool delayLoad = false);
+    ~Resource();
+    
+    /// Makes sure that the resource has started loading.
+    void ensureLoading();
+
+    /// Sets the load priority for one owner.
+    virtual void setLoadPriority(const QPointer<QObject>& owner, float priority);
+    
+    /// Sets a set of priorities at once.
+    virtual void setLoadPriorities(const QHash<QPointer<QObject>, float>& priorities);
+    
+    /// Clears the load priority for one owner.
+    virtual void clearLoadPriority(const QPointer<QObject>& owner);
+    
+    /// Returns the highest load priority across all owners.
+    float getLoadPriority();
+
+protected slots:
+
+    void attemptRequest();
+
+protected:
+
+    virtual void downloadFinished(QNetworkReply* reply) = 0;
+
+    QNetworkRequest _request;
+    bool _startedLoading;
+    bool _failedToLoad;
+    QHash<QPointer<QObject>, float> _loadPriorities;
+    
+private slots:
+    
+    void handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+    void handleReplyError();
+
+private:
+    
+    void makeRequest();
+    
+    friend class ResourceCache;
+    
+    QNetworkReply* _reply;
+    int _attempts;
+};
+
+uint qHash(const QPointer<QObject>& value, uint seed = 0);
+
+#endif /* defined(__shared__ResourceCache__) */