//
//  toolsMenu.js
//
//  Created by David Rowe on 22 Jul 2017.
//  Copyright 2017 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 App, ToolsMenu */

ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) {
    // Tool menu displayed on top of forearm.

    "use strict";

    var attachmentJointName,

        menuOriginLocalPosition,
        menuOriginLocalRotation,

        menuOriginOverlay,
        menuHeaderOverlay,
        menuHeaderHeadingOverlay,
        menuHeaderBarOverlay,
        menuHeaderBackOverlay,
        menuHeaderTitleOverlay,
        menuHeaderIconOverlay,
        menuPanelOverlay,

        menuOverlays = [],
        menuHoverOverlays = [],
        menuIconOverlays = [],
        menuLabelOverlays = [],
        menuEnabled = [],

        optionsOverlays = [],
        optionsOverlaysIDs = [],  // Text ids (names) of options overlays.
        optionsOverlaysLabels = [],  // Overlay IDs of labels for optionsOverlays.
        optionsOverlaysSublabels = [],  // Overlay IDs of sublabels for optionsOverlays.
        optionsSliderData = [],  // Uses same index values as optionsOverlays.
        optionsColorData = [],  // Uses same index values as optionsOverlays.
        optionsExtraOverlays = [],
        optionsEnabled = [],
        optionsSettings = {
            //  <option item id>: { 
            //      key: <Settings key to store value in>
            //      value: <current value>
            //  }
            //  For reliable access, use the values from optionsSettings rather than doing Settings.getValue() - Settings values
            //  are not necessarily updated instantaneously.
        },
        optionsToggles = {},  // Cater for toggle buttons without a setting.

        swatchHighlightOverlay = null,

        staticOverlays = [],

        LEFT_HAND = 0,
        PANEL_ORIGIN_POSITION_LEFT_HAND = {
            x: -UIT.dimensions.canvasSeparation - UIT.dimensions.canvas.x / 2,
            y: UIT.dimensions.handOffset,
            z: 0
        },
        PANEL_ORIGIN_POSITION_RIGHT_HAND = {
            x: UIT.dimensions.canvasSeparation + UIT.dimensions.canvas.x / 2,
            y: UIT.dimensions.handOffset,
            z: 0
        },
        PANEL_ORIGIN_ROTATION_LEFT_HAND = Quat.fromVec3Degrees({ x: 0, y: -90, z: 0 }),
        PANEL_ORIGIN_ROTATION_RIGHT_HAND = Quat.fromVec3Degrees({ x: 0, y: 90, z: 0 }),
        panelLateralOffset,

        MENU_ORIGIN_PROPERTIES = {
            dimensions: { x: 0.005, y: 0.005, z: 0.005 },
            color: { red: 255, blue: 0, green: 0 },
            alpha: 1.0,
            parentID: Uuid.SELF,
            ignoreRayIntersection: true,
            visible: false,
            displayInFront: true
        },

        MENU_HEADER_HOVER_OFFSET = { x: 0, y: 0, z: 0.0040 },

        MENU_HEADER_PROPERTIES = {
            // Invisible box to catch laser intersections while menu heading and bar move inside.
            dimensions: Vec3.sum(UIT.dimensions.header, MENU_HEADER_HOVER_OFFSET),  // Keep the laser on top when hover.
            localPosition: {
                x: 0,
                y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.header.y / 2,
                z: UIT.dimensions.header.z / 2 + MENU_HEADER_HOVER_OFFSET.z / 2
            },
            localRotation: Quat.ZERO,
            alpha: 0.0,  // Invisible
            solid: true,
            ignoreRayIntersection: false,
            visible: true  // Catch laser intersections.
        },

        MENU_HEADER_HEADING_PROPERTIES = {
            dimensions:  UIT.dimensions.headerHeading,
            localPosition: { x: 0, y: UIT.dimensions.headerBar.y / 2, z: -MENU_HEADER_HOVER_OFFSET.z / 2 },
            localRotation: Quat.ZERO,
            color: UIT.colors.baseGray,
            alpha: 1.0,
            solid: true,
            ignoreRayIntersection: true,
            visible: true
        },

        MENU_HEADER_BAR_PROPERTIES = {
            dimensions: UIT.dimensions.headerBar,
            localPosition: { x: 0, y: -UIT.dimensions.headerHeading.y / 2 - UIT.dimensions.headerBar.y / 2, z: 0 },
            localRotation: Quat.ZERO,
            color: UIT.colors.greenHighlight,
            alpha: 1.0,
            solid: true,
            ignoreRayIntersection: true,
            visible: true
        },

        MENU_HEADER_BACK_PROPERTIES = {
            url: "../assets/tools/back-icon.svg",
            dimensions: { x: 0.0069, y: 0.0107 },
            localPosition: {
                x: -MENU_HEADER_HEADING_PROPERTIES.dimensions.x / 2 + 0.0118 + 0.0069 / 2,
                y: 0,
                z: MENU_HEADER_HEADING_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset
            },
            localRotation: Quat.ZERO,
            color: UIT.colors.lightGrayText,
            alpha: 1.0,
            emissive: true,
            ignoreRayIntersection: true,
            isFacingAvatar: false,
            visible: true
        },

        MENU_HEADER_TITLE_PROPERTIES = {
            url: "../assets/tools/tools-heading.svg",
            scale: 0.0327,
            localPosition: {
                x: 0,
                y: 0,
                z: MENU_HEADER_HEADING_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset
            },
            localRotation: Quat.ZERO,
            color: UIT.colors.white,
            alpha: 1.0,
            emissive: true,
            ignoreRayIntersection: true,
            isFacingAvatar: false,
            visible: true
        },

        MENU_HEADER_TITLE_BACK_URL = "../assets/tools/back-heading.svg",
        MENU_HEADER_TITLE_BACK_SCALE = 0.0256,

        MENU_HEADER_ICON_OFFSET = {
            // Default right center position for header tool icons.
            x: MENU_HEADER_HEADING_PROPERTIES.dimensions.x / 2 - 0.0118,
            y: 0,
            z: MENU_HEADER_HEADING_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset
        },

        MENU_HEADER_ICON_PROPERTIES = {
            url: "../assets/tools/color-icon.svg",  // Initial value so that the overlay is initialized OK.
            dimensions: { x: 0.01, y: 0.01 },       // ""
            localPosition: Vec3.ZERO,               // ""
            localRotation: Quat.ZERO,
            color: UIT.colors.lightGrayText,
            alpha: 1.0,
            emissive: true,
            ignoreRayIntersection: true,
            isFacingAvatar: false,
            visible: false
        },

        MENU_PANEL_PROPERTIES = {
            dimensions: UIT.dimensions.panel,
            localPosition: { x: 0, y: UIT.dimensions.panel.y / 2 - UIT.dimensions.canvas.y / 2, z: UIT.dimensions.panel.z / 2 },
            localRotation: Quat.ZERO,
            color: UIT.colors.baseGray,
            alpha: 1.0,
            solid: true,
            ignoreRayIntersection: false,
            visible: true
        },

        EMPTY_SWATCH_COLOR = UIT.colors.baseGrayShadow,
        SWATCH_HIGHLIGHT_DELTA = 0.0020,

        UI_ELEMENTS = {
            "menuButton": {
                overlay: "cube",  // Invisible cube for hit area.
                properties: {
                    dimensions: UIT.dimensions.itemCollisionZone,
                    localRotation: Quat.ZERO,
                    alpha: 0.0,  // Invisible.
                    solid: true,
                    ignoreRayIntersection: false,
                    visible: true  // So that laser intersects.
                },
                hoverButton: {
                    overlay: "shape",
                    properties: {
                        shape: "Cylinder",
                        dimensions: {
                            x: UIT.dimensions.menuButtonDimensions.x,
                            y: UIT.dimensions.menuButtonDimensions.z,
                            z: UIT.dimensions.menuButtonDimensions.y
                        },
                        localPosition: UIT.dimensions.menuButtonIconOffset,
                        localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: -90 }),
                        color: UIT.colors.greenHighlight,
                        alpha: 1.0,
                        emissive: true,  // TODO: This has no effect.
                        solid: true,
                        ignoreRayIntersection: true,
                        visible: false
                    }
                },
                icon: {
                    // Relative to hoverButton.
                    type: "image",
                    properties: {
                        localPosition: {
                            x: 0,
                            y: UIT.dimensions.menuButtonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset,
                            z: 0
                        },
                        localRotation: Quat.fromVec3Degrees({ x: -90, y: 90, z: 0 }),
                        color: UIT.colors.lightGrayText
                    },
                    highlightColor: UIT.colors.darkGray
                },
                label: {
                    // Relative to menuButton.
                    type: "image",
                    properties: {
                        localPosition: {
                            x: 0,
                            y: UIT.dimensions.menuButtonLabelYOffset,
                            z: -UIT.dimensions.itemCollisionZone.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        color: UIT.colors.white
                    }
                },
                sublabel: {
                    // Relative to menuButton.
                    type: "image",
                    properties: {
                        url: "../assets/tools/tool-label.svg",
                        scale: 0.0152,
                        localPosition: {
                            x: 0,
                            y: UIT.dimensions.menuButtonSublabelYOffset,
                            z: -UIT.dimensions.itemCollisionZone.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        color: UIT.colors.lightGrayText
                    }
                }
            },
            "button": {
                overlay: "cube",
                properties: {
                    dimensions: UIT.dimensions.buttonDimensions,
                    localRotation: Quat.ZERO,
                    color: UIT.colors.baseGrayShadow,
                    alpha: 1.0,
                    solid: true,
                    ignoreRayIntersection: false,
                    visible: true
                },
                label: {
                    // Relative to button.
                    localPosition: {
                        x: 0,
                        y: 0,
                        z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                    },
                    color: UIT.colors.white
                }
            },
            "toggleButton": {
                overlay: "cube",
                properties: {
                    dimensions: UIT.dimensions.buttonDimensions,
                    localRotation: Quat.ZERO,
                    color: UIT.colors.baseGrayShadow,
                    alpha: 1.0,
                    solid: true,
                    ignoreRayIntersection: false,
                    visible: true
                },
                onColor: UIT.colors.greenHighlight,
                offColor: UIT.colors.baseGrayShadow,
                onHoverColor: UIT.colors.greenShadow,
                offHoverColor: UIT.colors.darkGray,
                label: {
                    // Relative to toggleButton.
                    localPosition: {
                        x: 0,
                        y: 0,
                        z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                    },
                    color: UIT.colors.white
                },
                sublabel: {
                    // Relative to toggleButton.
                    localPosition: {
                        x: 0,
                        y: 0,
                        z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                    },
                    color: UIT.colors.lightGray
                }
                /*
                onSublabel: { }  // Optional properties to update sublabel with.
                offSublabel: { }  // Optional properties to update sublabel with.
                */
            },
            "swatch": {
                overlay: "shape",
                properties: {
                    shape: "Cylinder",
                    dimensions: { x: 0.0294, y: UIT.dimensions.buttonDimensions.z, z: 0.0294 },
                    localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: -90 }),
                    color: EMPTY_SWATCH_COLOR,
                    emissive: true,  // TODO: This has no effect.
                    alpha: 1.0,
                    solid: true,
                    ignoreRayIntersection: false,
                    visible: true
                }
                // Must have a setting property in order to function property.
                // Setting property may optionally include a defaultValue.
                // A setting value of "" means that the swatch is unpopulated.
            },
            "swatchHighlight": {
                overlay: "shape",
                properties: {
                    shape: "Cylinder",
                    dimensions: {
                        x: 0.0294 + SWATCH_HIGHLIGHT_DELTA,
                        y: UIT.dimensions.buttonDimensions.z,
                        z: 0.0294 + SWATCH_HIGHLIGHT_DELTA
                    },
                    localRotation: Quat.ZERO,  // Relative to swatch.
                    localPosition: { x: 0, y: -0.0001, z: 0 },
                    color: UIT.colors.faintGray,
                    emissive: true,  // TODO: This has no effect.
                    alpha: 1.0,
                    solid: true,
                    ignoreRayIntersection: true,
                    visible: false
                }

            },
            "square": {
                overlay: "cube",  // Emulate a 2D square with a cube.
                properties: {
                    localRotation: Quat.ZERO,
                    color: UIT.colors.baseGrayShadow,
                    alpha: 1.0,
                    solid: true,
                    ignoreRayIntersection: true,
                    visible: true
                }
            },
            "image": {
                overlay: "image3d",
                properties: {
                    localPosition: { x: 0, y: 0, z: 0 },
                    localRotation: Quat.ZERO,
                    alpha: 1.0,
                    emissive: true,
                    ignoreRayIntersection: true,
                    isFacingAvatar: false,
                    visible: true
                }
            },
            "horizontalRule": {
                overlay: "image3d",
                properties: {
                    url: "../assets/horizontal-rule.svg",
                    dimensions: { x: UIT.dimensions.panel.x - 2 * UIT.dimensions.leftMargin, y: 0.001 },
                    localRotation: Quat.ZERO,
                    color: UIT.colors.baseGrayShadow,
                    alpha: 1.0,
                    solid: true,
                    ignoreRayIntersection: true,
                    isFacingAvatar: false,
                    visible: true
                }
            },
            "sphere": {
                overlay: "sphere",
                properties: {
                    dimensions: { x: 0.01, y: 0.01, z: 0.01 },
                    localRotation: Quat.ZERO,
                    color: { red: 192, green: 192, blue: 192 },
                    alpha: 1.0,
                    solid: true,
                    ignoreRayIntersection: true,
                    visible: true
                }
            },
            "barSlider": {
                // Invisible cube to catch laser intersections; value and remainder entities move inside.
                // Values range between 0.0 and 1.0.
                overlay: "cube",
                properties: {
                    dimensions: { x: 0.02, y: 0.1, z: 0.01 },
                    localRotation: Quat.ZERO,
                    alpha: 0.0,  // Invisible.
                    solid: true,
                    ignoreRayIntersection: false,
                    visible: true  // Catch laser intersections.
                },
                label: {
                    // Relative to barSlider.
                    color: UIT.colors.white
                },
                zeroIndicator: {
                    overlay: "image3d",
                    properties: {
                        url: "../assets/horizontal-rule.svg",
                        dimensions: { x: 0.02, y: 0.001 },
                        localRotation: Quat.ZERO,
                        color: UIT.colors.lightGrayText,
                        alpha: 1.0,
                        solid: true,
                        ignoreRayIntersection: true,
                        isFacingAvatar: false,
                        visible: true
                    }
                },
                sliderValue: {
                    overlay: "cube",
                    properties: {
                        dimensions: { x: 0.02, y: 0.03, z: 0.01 },
                        localPosition: { x: 0, y: 0.035, z: 0 },
                        localRotation: Quat.ZERO,
                        color: UIT.colors.greenHighlight,
                        alpha: 1.0,
                        solid: true,
                        ignoreRayIntersection: true,
                        visible: true
                    }
                },
                sliderRemainder: {
                    overlay: "cube",
                    properties: {
                        dimensions: { x: 0.02, y: 0.07, z: 0.01 },
                        localPosition: { x: 0, y: -0.015, z: 0 },
                        localRotation: Quat.ZERO,
                        color: UIT.colors.baseGrayShadow,
                        alpha: 1.0,
                        solid: true,
                        ignoreRayIntersection: true,
                        visible: true
                    }
                }
            },
            "imageSlider": {  // Values range between 0.0 and 1.0.
                overlay: "cube",
                properties: {
                    dimensions: { x: 0.0160, y: 0.1229, z: UIT.dimensions.buttonDimensions.z },
                    localRotation: Quat.ZERO,
                    color: { red: 128, green: 128, blue: 128 },
                    alpha: 1.0,
                    solid: true,
                    ignoreRayIntersection: false,
                    visible: true
                },
                useBaseColor: false,
                imageURL: null,
                imageOverlayURL: null
            },
            "sliderPointer": {
                overlay: "shape",
                properties: {
                    shape: "Cone",
                    dimensions: { x: 0.005, y: 0.005, z: 0.005 },
                    localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }),
                    color: { red: 180, green: 180, blue: 180 },
                    alpha: 1.0,
                    solid: true,
                    ignoreRayIntersection: true,
                    visible: true
                }
            },
            "colorCircle": {
                overlay: "shape",
                properties: {
                    shape: "Cylinder",
                    dimensions: { x: 0.1229, y: UIT.dimensions.buttonDimensions.z, z: 0.1229 },
                    localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: -90 }),
                    color: { red: 128, green: 128, blue: 128 },
                    alpha: 1.0,
                    solid: true,
                    ignoreRayIntersection: false,
                    visible: true
                },
                imageURL: null,
                imageOverlayURL: null
            },
            "circlePointer": {
                overlay: "shape",
                properties: {
                    shape: "Cone",
                    dimensions: { x: 0.005, y: 0.005, z: 0.005 },
                    localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }),
                    color: { red: 180, green: 180, blue: 180 },
                    alpha: 1.0,
                    solid: true,
                    ignoreRayIntersection: true,
                    visible: true
                }
            },
            "picklist": {
                overlay: "cube",
                properties: {
                    dimensions: { x: 0.06, y: 0.02, z: 0.01 },
                    localRotation: Quat.ZERO,
                    color: UIT.colors.baseGrayShadow,
                    alpha: 1.0,
                    solid: true,
                    ignoreRayIntersection: false,
                    visible: true
                },
                label: {
                    // Relative to picklist.
                    color: UIT.colors.white
                }
            },
            "picklistItem": {  // Note: When using, declare before picklist item that it's being used in.
                overlay: "cube",
                properties: {
                    dimensions: { x: 0.1416, y: 0.0280, z: UIT.dimensions.buttonDimensions.z },
                    localPosition: Vec3.ZERO,
                    localRotation: Quat.ZERO,
                    color: UIT.colors.baseGrayShadow,
                    alpha: 1.0,
                    solid: true,
                    ignoreRayIntersection: false,
                    visible: false
                },
                label: {
                    // Relative to picklistItem.
                    color: UIT.colors.white
                }
            }
        },

        BUTTON_UI_ELEMENTS = ["button", "menuButton", "toggleButton", "swatch"],

        MENU_HOVER_DELTA = { x: 0, y: 0, z: 0.006 },
        OPTION_HOVER_DELTA = { x: 0, y: 0, z: 0.002 },
        PICKLIST_HOVER_DELTA = { x: 0, y: 0, z: 0.006 },
        BUTTON_PRESS_DELTA = { x: 0, y: 0, z: -0.002 },

        MIN_BAR_SLIDER_DIMENSION = 0.0001,  // Avoid visual artifact for 0 slider values.

        PHYSICS_SLIDER_PRESETS = {
            // Slider values in the range 0.0 to 1.0.
            // Note: Friction values give the desired linear and angular damping values but friction values are a somewhat out,
            // especially for the balloon.
            presetDefault:    { gravity: 0.5,      bounce: 0.5,  friction: 0.5,      density: 0.5      },
            presetLead:       { gravity: 0.5,      bounce: 0.0,  friction: 0.5,      density: 1.0      },
            presetWood:       { gravity: 0.5,      bounce: 0.4,  friction: 0.5,      density: 0.5      },
            presetIce:        { gravity: 0.5,      bounce: 0.99, friction: 0.151004, density: 0.349485 },
            presetRubber:     { gravity: 0.5,      bounce: 0.99, friction: 0.5,      density: 0.5      },
            presetCotton:     { gravity: 0.587303, bounce: 0.0,  friction: 0.931878, density: 0.0      },
            presetTumbleweed: { gravity: 0.595893, bounce: 0.7,  friction: 0.5,      density: 0.0      },
            presetZeroG:      { gravity: 0.596844, bounce: 0.5,  friction: 0.5,      density: 0.5      },
            presetBalloon:    { gravity: 0.606313, bounce: 0.99, friction: 0.151004, density: 0.0      }
        },

        OPTONS_PANELS = {
            colorOptions: [
                {
                    id: "colorSwatchesLabel",
                    type: "image",
                    properties: {
                        color: UIT.colors.white,
                        url: "../assets/tools/color/swatches-label.svg",
                        scale: 0.0345,
                        localPosition: {
                            x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0345 / 2,
                            y: UIT.dimensions.panel.y / 2 - UIT.dimensions.topMargin - 0.0047 / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "colorRule1",
                    type: "horizontalRule",
                    properties: {
                        dimensions: { x: 0.0668, y: 0.001 },
                        localPosition: {
                            x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0668 / 2,
                            y: UIT.dimensions.panel.y / 2 - 0.0199,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "colorSwatch1",
                    type: "swatch",
                    properties: {
                        localPosition: {
                            x: -0.0935,
                            y: 0.0513,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    setting: {
                        key: "VREdit.colorTool.swatch1Color",
                        property: "color"
                    },
                    command: {
                        method: "setColorPerSwatch"
                    },
                    clear: {
                        method: "clearSwatch"
                    }
                },
                {
                    id: "colorSwatch2",
                    type: "swatch",
                    properties: {
                        localPosition: {
                            x: -0.0561,
                            y: 0.0513,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    setting: {
                        key: "VREdit.colorTool.swatch2Color",
                        property: "color"
                    },
                    command: {
                        method: "setColorPerSwatch"
                    },
                    clear: {
                        method: "clearSwatch"
                    }
                },
                {
                    id: "colorSwatch3",
                    type: "swatch",
                    properties: {
                        localPosition: {
                            x: -0.0935,
                            y: 0.0153,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    setting: {
                        key: "VREdit.colorTool.swatch3Color",
                        property: "color"
                    },
                    command: {
                        method: "setColorPerSwatch"
                    },
                    clear: {
                        method: "clearSwatch"
                    }
                },
                {
                    id: "colorSwatch4",
                    type: "swatch",
                    properties: {
                        localPosition: {
                            x: -0.0561,
                            y: 0.0153,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    setting: {
                        key: "VREdit.colorTool.swatch4Color",
                        property: "color"
                    },
                    command: {
                        method: "setColorPerSwatch"
                    },
                    clear: {
                        method: "clearSwatch"
                    }
                },
                {
                    id: "colorSwatch5",
                    type: "swatch",
                    properties: {
                        localPosition: {
                            x: -0.0935,
                            y: -0.0207,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    setting: {
                        key: "VREdit.colorTool.swatch5Color",
                        property: "color"
                    },
                    command: {
                        method: "setColorPerSwatch"
                    },
                    clear: {
                        method: "clearSwatch"
                    }
                },
                {
                    id: "colorSwatch6",
                    type: "swatch",
                    properties: {
                        localPosition: {
                            x: -0.0561,
                            y: -0.0207,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    setting: {
                        key: "VREdit.colorTool.swatch6Color",
                        property: "color"
                    },
                    command: {
                        method: "setColorPerSwatch"
                    },
                    clear: {
                        method: "clearSwatch"
                    }
                },
                {
                    id: "colorRule2",
                    type: "horizontalRule",
                    properties: {
                        dimensions: { x: 0.0668, y: 0.001 },
                        localPosition: {
                            x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0668 / 2,
                            y: -UIT.dimensions.panel.y / 2 + 0.0481,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "colorCircle",
                    type: "colorCircle",
                    properties: {
                        localPosition: {
                            x: 0.04675,
                            y: 0.01655,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    imageURL: "../assets/tools/color/color-circle.png",
                    imageOverlayURL: "../assets/tools/color/color-circle-black.png",
                    command: {
                        method: "setColorPerCircle"
                    }
                },
                {
                    id: "colorSlider",
                    type: "imageSlider",
                    properties: {
                        localPosition: {
                            x: 0.04675,
                            y: -0.0620,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        },
                        localRotation: Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 })
                    },
                    useBaseColor: true,
                    imageURL: "../assets/tools/color/slider-white.png",
                    // Alpha PNG created by overlaying two black-to-transparent gradients in order to achieve visual effect.
                    imageOverlayURL: "../assets/tools/color/slider-alpha.png",
                    command: {
                        method: "setColorPerSlider"
                    }
                },
                {
                    id: "colorRule3",
                    type: "horizontalRule",
                    properties: {
                        dimensions: { x: 0.1229, y: 0.001 },
                        localPosition: {
                            x: 0.04675,
                            y: -0.0781,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "pickColor",
                    type: "toggleButton",
                    properties: {
                        dimensions: { x: 0.0294, y: 0.0280, z: UIT.dimensions.buttonDimensions.z },
                        localPosition: {
                            x: -0.0935,
                            y: -0.064,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    label: {
                        url: "../assets/tools/color/pick-color-label.svg",
                        scale: 0.0120
                    },
                    command: {
                        method: "togglePickColor"
                    }
                },
                {
                    id: "currentColor",
                    type: "square",
                    properties: {
                        dimensions: { x: 0.0294, y: 0.0280, z: UIT.dimensions.imageOverlayOffset },
                        localPosition: {
                            x: -0.0561,
                            y: -0.064,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    setting: {
                        key: "VREdit.colorTool.currentColor",
                        property: "color",
                        defaultValue: { red: 128, green: 128, blue: 128 },
                        command: "setPickColor"
                    }
                }
            ],
            scaleOptions: [
                {
                    id: "stretchActionsLabel",
                    type: "image",
                    properties: {
                        color: UIT.colors.white,
                        url: "../assets/tools/common/actions-label.svg",
                        scale: 0.0276,
                        localPosition: {
                            x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0276 / 2,
                            y: UIT.dimensions.panel.y / 2 - UIT.dimensions.topMargin - 0.0047 / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "stretchRule1",
                    type: "horizontalRule",
                    properties: {
                        localPosition: {
                            x: 0,
                            y: UIT.dimensions.panel.y / 2 - 0.0199,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "stretchFinishButton",
                    type: "button",
                    properties: {
                        localPosition: {
                            x: 0,
                            y: UIT.dimensions.panel.y / 2 - 0.0280 - UIT.dimensions.buttonDimensions.y / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    label: {
                        url: "../assets/tools/common/finish-label.svg",
                        scale: 0.0318
                    },
                    command: {
                        method: "clearTool"
                    }
                },
                {
                    id: "stretchRule2",
                    type: "horizontalRule",
                    properties: {
                        localPosition: {
                            x: 0,
                            y: UIT.dimensions.panel.y / 2 - 0.1197,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "stretchInfoIcon",
                    type: "image",
                    properties: {
                        url: "../assets/tools/common/info-icon.svg",
                        dimensions: { x: 0.0321, y: 0.0320 },
                        localPosition: {
                            x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0321 / 2,
                            y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        color: UIT.colors.white  // Icon SVG is already lightGray color.
                    }
                },
                {
                    id: "stretchInfo",
                    type: "image",
                    properties: {
                        url: "../assets/tools/stretch/info-text.svg",
                        scale: 0.1340,
                        localPosition: {
                            x: -UIT.dimensions.panel.x / 2 + 0.0679 + 0.1340 / 2,
                            y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2,  // Center on info icon.
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        color: UIT.colors.white
                    }
                }
            ],
            cloneOptions: [
                {
                    id: "cloneActionsLabel",
                    type: "image",
                    properties: {
                        color: UIT.colors.white,
                        url: "../assets/tools/common/actions-label.svg",
                        scale: 0.0276,
                        localPosition: {
                            x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0276 / 2,
                            y: UIT.dimensions.panel.y / 2 - UIT.dimensions.topMargin - 0.0047 / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "cloneRule1",
                    type: "horizontalRule",
                    properties: {
                        localPosition: {
                            x: 0,
                            y: UIT.dimensions.panel.y / 2 - 0.0199,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "cloneFinishButton",
                    type: "button",
                    properties: {
                        localPosition: {
                            x: 0,
                            y: UIT.dimensions.panel.y / 2 - 0.0280 - UIT.dimensions.buttonDimensions.y / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    label: {
                        url: "../assets/tools/common/finish-label.svg",
                        scale: 0.0318
                    },
                    command: {
                        method: "clearTool"
                    }
                }
            ],
            groupOptions: [
                {
                    id: "groupActionsLabel",
                    type: "image",
                    properties: {
                        color: UIT.colors.white,
                        url: "../assets/tools/common/actions-label.svg",
                        scale: 0.0276,
                        localPosition: {
                            x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0276 / 2,
                            y: UIT.dimensions.panel.y / 2 - UIT.dimensions.topMargin - 0.0047 / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "groupRule1",
                    type: "horizontalRule",
                    properties: {
                        localPosition: {
                            x: 0,
                            y: UIT.dimensions.panel.y / 2 - 0.0199,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "groupButton",
                    type: "button",
                    properties: {
                        dimensions: {
                            x: UIT.dimensions.buttonDimensions.x,
                            y: 0.0680,
                            z: UIT.dimensions.buttonDimensions.z
                        },
                        localPosition: {
                            x: 0,
                            y: UIT.dimensions.panel.y / 2 - 0.0280 - 0.0680 / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        },
                        color: UIT.colors.baseGrayShadow
                    },
                    enabledColor: UIT.colors.greenHighlight,
                    highlightColor: UIT.colors.greenShadow,
                    label: {
                        url: "../assets/tools/group/group-label.svg",
                        scale: 0.0351,
                        color: UIT.colors.baseGray
                    },
                    labelEnabledColor: UIT.colors.white,
                    callback: {
                        method: "groupButton"
                    }
                },
                {
                    id: "ungroupButton",
                    type: "button",
                    properties: {
                        dimensions: {
                            x: UIT.dimensions.buttonDimensions.x,
                            y: 0.0680,
                            z: UIT.dimensions.buttonDimensions.z
                        },
                        localPosition: {
                            x: 0,
                            y: -UIT.dimensions.panel.y / 2 + 0.0120 + 0.0680 / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        },
                        color: UIT.colors.baseGrayShadow

                    },
                    enabledColor: UIT.colors.redHighlight,
                    highlightColor: UIT.colors.redAccent,
                    label: {
                        url: "../assets/tools/group/ungroup-label.svg",
                        scale: 0.0496,
                        color: UIT.colors.baseGray
                    },
                    labelEnabledColor: UIT.colors.white,
                    callback: {
                        method: "ungroupButton"
                    }
                }
            ],
            physicsOptions: [
                {
                    id: "physicsPropertiesLabel",
                    type: "image",
                    properties: {
                        color: UIT.colors.white,
                        url: "../assets/tools/physics/properties-label.svg",
                        scale: 0.0376,
                        localPosition: {
                            x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0376 / 2,
                            y: UIT.dimensions.panel.y / 2 - UIT.dimensions.topMargin - 0.0047 / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "physicsRule1",
                    type: "horizontalRule",
                    properties: {
                        dimensions: { x: 0.0668, y: 0.001 },
                        localPosition: {
                            x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0668 / 2,
                            y: UIT.dimensions.panel.y / 2 - 0.0199,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "gravityToggle",
                    type: "toggleButton",
                    properties: {
                        dimensions: { x: 0.0668, y: 0.0280, z: UIT.dimensions.buttonDimensions.z },
                        localPosition: {
                            x: -0.0748,
                            y: 0.0480,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    label: {
                        localPosition: {
                            x: 0,
                            y: 0.0034,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        url: "../assets/tools/physics/buttons/gravity-label.svg",
                        scale: 0.0240,
                        color: UIT.colors.white
                    },
                    sublabel: {
                        localPosition: {
                            x: 0,
                            y: -0.0034,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        url: "../assets/tools/physics/buttons/off-label.svg",
                        scale: 0.0104,
                        color: UIT.colors.white  // SVG has gray color.
                    },
                    onSublabel: {
                        url: "../assets/tools/physics/buttons/on-label.svg",
                        scale: 0.0081
                    },
                    offSublabel: {
                        url: "../assets/tools/physics/buttons/off-label.svg",
                        scale: 0.0104
                    },
                    setting: {
                        key: "VREdit.physicsTool.gravityOn",
                        defaultValue: false,
                        callback: "setGravityOn"
                    },
                    command: {
                        method: "setGravityOn"
                    }
                },
                {
                    id: "collideToggle",
                    type: "toggleButton",
                    properties: {
                        dimensions: { x: 0.0668, y: 0.0280, z: UIT.dimensions.buttonDimensions.z },
                        localPosition: {
                            x: -0.0748,
                            y: 0.0120,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    label: {
                        localPosition: {
                            x: 0,
                            y: 0.0034,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        url: "../assets/tools/physics/buttons/collisions-label.svg",
                        scale: 0.0338,
                        color: UIT.colors.white
                    },
                    sublabel: {
                        localPosition: {
                            x: 0,
                            y: -0.0034,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        url: "../assets/tools/physics/buttons/off-label.svg",
                        scale: 0.0104,
                        color: UIT.colors.white  // SVG has gray color.
                    },
                    onSublabel: {
                        url: "../assets/tools/physics/buttons/on-label.svg",
                        scale: 0.0081
                    },
                    offSublabel: {
                        url: "../assets/tools/physics/buttons/off-label.svg",
                        scale: 0.0104
                    },
                    setting: {
                        key: "VREdit.physicsTool.collideOn",
                        defaultValue: true,
                        callback: "setCollideOn"
                    },
                    command: {
                        method: "setCollideOn"
                    }
                },
                {
                    id: "grabToggle",
                    type: "toggleButton",
                    properties: {
                        dimensions: { x: 0.0668, y: 0.0280, z: UIT.dimensions.buttonDimensions.z },
                        localPosition: {
                            x: -0.0748,
                            y: -0.0240,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    label: {
                        localPosition: {
                            x: 0,
                            y: 0.0034,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        url: "../assets/tools/physics/buttons/grabbable-label.svg",
                        scale: 0.0334,
                        color: UIT.colors.white
                    },
                    sublabel: {
                        localPosition: {
                            x: 0,
                            y: -0.0034,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        url: "../assets/tools/physics/buttons/off-label.svg",
                        scale: 0.0104,
                        color: UIT.colors.white  // SVG has gray color.
                    },
                    onSublabel: {
                        url: "../assets/tools/physics/buttons/on-label.svg",
                        scale: 0.0081
                    },
                    offSublabel: {
                        url: "../assets/tools/physics/buttons/off-label.svg",
                        scale: 0.0104
                    },
                    setting: {
                        key: "VREdit.physicsTool.grabOn",
                        defaultValue: false,
                        callback: "setGrabOn"
                    },
                    command: {
                        method: "setGrabOn"
                    }
                },

                {
                    id: "physicsPresetsLabel",
                    type: "image",
                    properties: {
                        color: UIT.colors.white,
                        url: "../assets/tools/physics/presets-label.svg",
                        scale: 0.0270,
                        localPosition: {
                            x: UIT.dimensions.panel.x / 2 - UIT.dimensions.rightMargin - 0.1416 + 0.0270 / 2,
                            y: UIT.dimensions.panel.y / 2 - UIT.dimensions.topMargin - 0.0047 / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "physicsRule2",
                    type: "horizontalRule",
                    properties: {
                        dimensions: { x: 0.1416, y: 0.001 },
                        localPosition: {
                            x: UIT.dimensions.panel.x / 2 - UIT.dimensions.rightMargin - 0.1416 / 2,
                            y: UIT.dimensions.panel.y / 2 - 0.0199,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "presetDefault",
                    type: "picklistItem",
                    label: {
                        url: "../assets/tools/physics/presets/default-label.svg",
                        scale: 0.0436,
                        localPosition: {
                            x: -0.1416 / 2 + 0.017 + 0.0436 / 2,
                            y: 0,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    },
                    command: {
                        method: "pickPhysicsPreset",
                        value: "default"
                    }
                },
                {
                    id: "presetLead",
                    type: "picklistItem",
                    label: {
                        url: "../assets/tools/physics/presets/lead-label.svg",
                        scale: 0.0243,
                        localPosition: {
                            x: -0.1416 / 2 + 0.017 + 0.0243 / 2,
                            y: 0,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    },
                    command: { method: "pickPhysicsPreset" }
                },
                {
                    id: "presetWood",
                    type: "picklistItem",
                    label: {
                        url: "../assets/tools/physics/presets/wood-label.svg",
                        scale: 0.0316,
                        localPosition: {
                            x: -0.1416 / 2 + 0.017 + 0.0316 / 2,
                            y: 0,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    },
                    command: { method: "pickPhysicsPreset" }
                },
                {
                    id: "presetIce",
                    type: "picklistItem",
                    label: {
                        url: "../assets/tools/physics/presets/ice-label.svg",
                        scale: 0.0144,
                        localPosition: {
                            x: -0.1416 / 2 + 0.017 + 0.0144 / 2,
                            y: 0,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    },
                    command: { method: "pickPhysicsPreset" }
                },
                {
                    id: "presetRubber",
                    type: "picklistItem",
                    label: {
                        url: "../assets/tools/physics/presets/rubber-label.svg",
                        scale: 0.0400,
                        localPosition: {
                            x: -0.1416 / 2 + 0.017 + 0.0400 / 2,
                            y: 0,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    },
                    command: { method: "pickPhysicsPreset" }
                },
                {
                    id: "presetCotton",
                    type: "picklistItem",
                    label: {
                        url: "../assets/tools/physics/presets/cotton-label.svg",
                        scale: 0.0393,
                        localPosition: {
                            x: -0.1416 / 2 + 0.017 + 0.0393 / 2,
                            y: 0,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    },
                    command: { method: "pickPhysicsPreset" }
                },
                {
                    id: "presetTumbleweed",
                    type: "picklistItem",
                    label: {
                        url: "../assets/tools/physics/presets/tumbleweed-label.svg",
                        scale: 0.0687,
                        localPosition: {
                            x: -0.1416 / 2 + 0.017 + 0.0687 / 2,
                            y: 0,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    },
                    command: { method: "pickPhysicsPreset" }
                },
                {
                    id: "presetZeroG",
                    type: "picklistItem",
                    label: {
                        url: "../assets/tools/physics/presets/zero-g-label.svg",
                        scale: 0.0375,
                        localPosition: {
                            x: -0.1416 / 2 + 0.017 + 0.0375 / 2,
                            y: 0,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    },
                    command: { method: "pickPhysicsPreset" }
                },
                {
                    id: "presetBalloon",
                    type: "picklistItem",
                    label: {
                        url: "../assets/tools/physics/presets/balloon-label.svg",
                        scale: 0.0459,
                        localPosition: {
                            x: -0.1416 / 2 + 0.017 + 0.0459 / 2,
                            y: 0,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    },
                    command: { method: "pickPhysicsPreset" }
                },
                {
                    id: "physicsPresets",
                    type: "picklist",
                    properties: {
                        dimensions: { x: 0.1416, y: 0.0280, z: UIT.dimensions.buttonDimensions.z },
                        localPosition: {
                            x: UIT.dimensions.panel.x / 2 - UIT.dimensions.rightMargin - 0.1416 / 2,
                            y: 0.0480,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    label: {
                        url: "../assets/tools/physics/presets/default-label.svg",
                        scale: 0.0436,
                        localPosition: {
                            x: -0.1416 / 2 + 0.017 + 0.0436 / 2,
                            y: 0,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    },
                    sublabel: {
                        url: "../assets/tools/common/down-arrow.svg",
                        scale: 0.0080,
                        localPosition: {
                            x: 0.1416 / 2 - 0.0108 - 0.0080 / 2,
                            y: 0,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        color: UIT.colors.white  // SVG is colored baseGrayHighlight
                    },
                    customLabel: {
                        url: "../assets/tools/physics/presets/custom-label.svg",
                        scale: 0.0522,
                        localPosition: {
                            x: -0.1416 / 2 + 0.017 + 0.0522 / 2,
                            y: 0,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    },
                    setting: {
                        key: "VREdit.physicsTool.physicsPreset",
                        defaultValue: "presetDefault"
                    },
                    command: {
                        method: "togglePhysicsPresets"
                    },
                    items: [
                        "presetDefault",
                        "presetLead",
                        "presetWood",
                        "presetIce",
                        "presetRubber",
                        "presetCotton",
                        "presetTumbleweed",
                        "presetZeroG",
                        "presetBalloon"
                    ]
                },

                {
                    id: "gravitySlider",
                    type: "barSlider",
                    properties: {
                        dimensions: { x: 0.0294, y: 0.1000, z: UIT.dimensions.buttonDimensions.z },
                        localPosition: {
                            x: -0.0187,
                            y: -0.0240,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    label: {
                        localPosition: {
                            x: 0,
                            y: -0.04375,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        url: "../assets/tools/physics/sliders/gravity-label.svg",
                        scale: 0.0240
                    },
                    setting: {
                        key: "VREdit.physicsTool.gravity",
                        defaultValue: 0.5,
                        callback: "setGravity"
                    },
                    command: {
                        method: "setGravity"
                    }
                },
                {
                    id: "bounceSlider",
                    type: "barSlider",
                    properties: {
                        dimensions: { x: 0.0294, y: 0.1000, z: UIT.dimensions.buttonDimensions.z },
                        localPosition: {
                            x: 0.0187,
                            y: -0.0240,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    label: {
                        localPosition: {
                            x: 0,
                            y: -0.04375,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        url: "../assets/tools/physics/sliders/bounce-label.svg",
                        scale: 0.0233
                    },
                    setting: {
                        key: "VREdit.physicsTool.bounce",
                        defaultValue: 0.5,
                        callback: "setBounce"
                    },
                    command: {
                        method: "setBounce"
                    }
                },
                {
                    id: "frictionSlider",
                    type: "barSlider",
                    properties: {
                        dimensions: { x: 0.0294, y: 0.1000, z: UIT.dimensions.buttonDimensions.z },
                        localPosition: {
                            x: 0.0561,
                            y: -0.0240,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    label: {
                        localPosition: {
                            x: 0,
                            y: -0.04375,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        url: "../assets/tools/physics/sliders/friction-label.svg",
                        scale: 0.0258
                    },
                    setting: {
                        key: "VREdit.physicsTool.friction",
                        defaultValue: 0.5,
                        callback: "setFriction"
                    },
                    command: {
                        method: "setFriction"
                    }
                },
                {
                    id: "densitySlider",
                    type: "barSlider",
                    properties: {
                        dimensions: { x: 0.0294, y: 0.1000, z: UIT.dimensions.buttonDimensions.z },
                        localPosition: {
                            x: 0.0935,
                            y: -0.0240,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    label: {
                        localPosition: {
                            x: 0,
                            y: -0.04375,
                            z: UIT.dimensions.buttonDimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        url: "../assets/tools/physics/sliders/density-label.svg",
                        scale: 0.0241
                    },
                    setting: {
                        key: "VREdit.physicsTool.density",
                        defaultValue: 0.5,
                        callback: "setDensity"
                    },
                    command: {
                        method: "setDensity"
                    }
                }
            ],
            deleteOptions: [
                {
                    id: "deleteActionsLabel",
                    type: "image",
                    properties: {
                        color: UIT.colors.white,
                        url: "../assets/tools/common/actions-label.svg",
                        scale: 0.0276,
                        localPosition: {
                            x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0276 / 2,
                            y: UIT.dimensions.panel.y / 2 - UIT.dimensions.topMargin - 0.0047 / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "deleeteRule1",
                    type: "horizontalRule",
                    properties: {
                        localPosition: {
                            x: 0,
                            y: UIT.dimensions.panel.y / 2 - 0.0199,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "deleteFinishButton",
                    type: "button",
                    properties: {
                        localPosition: {
                            x: 0,
                            y: UIT.dimensions.panel.y / 2 - 0.0280 - UIT.dimensions.buttonDimensions.y / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.buttonDimensions.z / 2
                        }
                    },
                    label: {
                        url: "../assets/tools/common/finish-label.svg",
                        scale: 0.0318
                    },
                    command: {
                        method: "clearTool"
                    }
                },
                {
                    id: "deleteRule2",
                    type: "horizontalRule",
                    properties: {
                        localPosition: {
                            x: 0,
                            y: UIT.dimensions.panel.y / 2 - 0.1197,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        }
                    }
                },
                {
                    id: "deleteInfoIcon",
                    type: "image",
                    properties: {
                        url: "../assets/tools/common/info-icon.svg",
                        dimensions: { x: 0.0321, y: 0.0320 },
                        localPosition: {
                            x: -UIT.dimensions.panel.x / 2 + UIT.dimensions.leftMargin + 0.0321 / 2,
                            y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0320 / 2,
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        color: UIT.colors.white  // Icon SVG is already lightGray color.
                    }
                },
                {
                    id: "deleteInfo",
                    type: "image",
                    properties: {
                        url: "../assets/tools/delete/info-text.svg",
                        scale: 0.1416,
                        localPosition: {
                            x: -UIT.dimensions.panel.x / 2 + 0.0679 + 0.1340 / 2,
                            y: -UIT.dimensions.panel.y / 2 + 0.0200 + 0.0240 / 2 + 0.0063 / 2,  // Off-center w.r.t. info icon.
                            z: UIT.dimensions.panel.z / 2 + UIT.dimensions.imageOverlayOffset
                        },
                        color: UIT.colors.white
                    }
                }
            ]
        },

        MENU_ITEM_XS = [-0.08415, -0.02805, 0.02805, 0.08415],
        MENU_ITEM_YS = [0.058, 0.002, -0.054],

        MENU_ITEMS = [
            {
                id: "colorButton",
                type: "menuButton",
                properties: {
                    localPosition: {
                        x: MENU_ITEM_XS[0],
                        y: MENU_ITEM_YS[0],
                        z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2
                    }
                },
                icon: {
                    properties: {
                        url: "../assets/tools/color-icon.svg",
                        dimensions: { x: 0.0165, y: 0.0187 }
                    },
                    headerOffset: { x: -0.00825, y: 0.0020, z: 0 }
                },
                label: {
                    properties: {
                        url: "../assets/tools/color-label.svg",
                        scale: 0.0241
                    }
                },
                title: {
                    url: "../assets/tools/color-tool-heading.svg",
                    scale: 0.0631
                },
                toolOptions: "colorOptions",
                callback: {
                    method: "colorTool",
                    parameter: "currentColor.color"
                }
            },
            {
                id: "scaleButton",
                type: "menuButton",
                properties: {
                    localPosition: {
                        x: MENU_ITEM_XS[1],
                        y: MENU_ITEM_YS[0],
                        z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2
                    }
                },
                icon: {
                    properties: {
                        url: "../assets/tools/stretch-icon.svg",
                        dimensions: { x: 0.0167, y: 0.0167 }
                    },
                    headerOffset: { x: -0.00835, y: 0, z: 0 }
                },
                label: {
                    properties: {
                        url: "../assets/tools/stretch-label.svg",
                        scale: 0.0311
                    }
                },
                title: {
                    url: "../assets/tools/stretch-tool-heading.svg",
                    scale: 0.0737
                },
                toolOptions: "scaleOptions",
                callback: {
                    method: "scaleTool"
                }
            },
            {
                id: "cloneButton",
                type: "menuButton",
                properties: {
                    localPosition: {
                        x: MENU_ITEM_XS[2],
                        y: MENU_ITEM_YS[0],
                        z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2
                    }
                },
                icon: {
                    properties: {
                        url: "../assets/tools/clone-icon.svg",
                        dimensions: { x: 0.0154, y: 0.0155 }
                    },
                    headerOffset: { x: -0.0077, y: 0, z: 0 }
                },
                label: {
                    properties: {
                        url: "../assets/tools/clone-label.svg",
                        scale: 0.0231
                    }
                },
                title: {
                    url: "../assets/tools/clone-tool-heading.svg",
                    scale: 0.0621
                },
                toolOptions: "cloneOptions",
                callback: {
                    method: "cloneTool"
                }
            },
            {
                id: "groupButton",
                type: "menuButton",
                properties: {
                    localPosition: {
                        x: MENU_ITEM_XS[3],
                        y: MENU_ITEM_YS[0],
                        z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2
                    }
                },
                icon: {
                    properties: {
                        url: "../assets/tools/group-icon.svg",
                        dimensions: { x: 0.0161, y: 0.0114 }
                    },
                    headerOffset: { x: -0.00805, y: 0, z: 0 }
                },
                label: {
                    properties: {
                        url: "../assets/tools/group-label.svg",
                        scale: 0.0250
                    }
                },
                title: {
                    url: "../assets/tools/group-tool-heading.svg",
                    scale: 0.0647
                },
                toolOptions: "groupOptions",
                callback: {
                    method: "groupTool"
                }
            },
            {
                id: "physicsButton",
                type: "menuButton",
                properties: {
                    localPosition: {
                        x: MENU_ITEM_XS[0],
                        y: MENU_ITEM_YS[1],
                        z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2
                    }
                },
                icon: {
                    properties: {
                        url: "../assets/tools/physics-icon.svg",
                        dimensions: { x: 0.0180, y: 0.0198 }
                    },
                    headerOffset: { x: -0.009, y: 0, z: 0 }
                },
                label: {
                    properties: {
                        url: "../assets/tools/physics-label.svg",
                        scale: 0.0297
                    }
                },
                title: {
                    url: "../assets/tools/physics-tool-heading.svg",
                    scale: 0.0712
                },
                toolOptions: "physicsOptions",
                callback: {
                    method: "physicsTool"
                }
            },
            {
                id: "deleteButton",
                type: "menuButton",
                properties: {
                    localPosition: {
                        x: MENU_ITEM_XS[1],
                        y: MENU_ITEM_YS[1],
                        z: UIT.dimensions.panel.z / 2 + UI_ELEMENTS.menuButton.properties.dimensions.z / 2
                    }
                },
                icon: {
                    properties: {
                        url: "../assets/tools/delete-icon.svg",
                        dimensions: { x: 0.0161, y: 0.0161 }
                    },
                    headerOffset: { x: -0.00805, y: 0, z: 0 }
                },
                label: {
                    properties: {
                        url: "../assets/tools/delete-label.svg",
                        scale: 0.0254
                    }
                },
                title: {
                    url: "../assets/tools/delete-tool-heading.svg",
                    scale: 0.0653
                },
                toolOptions: "deleteOptions",
                callback: {
                    method: "deleteTool"
                }
            }
        ],
        COLOR_TOOL = 0,  // Indexes of corresponding MENU_ITEMS item.
        SCALE_TOOL = 1,
        CLONE_TOOL = 2,
        GROUP_TOOL = 3,
        PHYSICS_TOOL = 4,
        DELETE_TOOL = 5,

        NONE = -1,

        optionsItems,
        intersectionOverlays,
        intersectionEnabled,
        highlightedItem,  // TODO: Rename this and similar to "hovered".
        highlightedItems,
        highlightedSourceOverlays,
        highlightedSourceItems,
        highlightedElementType = null,
        isHighlightingButtonElement,
        isPicklistOpen,
        pressedItem = null,
        pressedSource = null,
        isPicklistPressed,
        isPicklistItemPressed,
        isTriggerClicked,
        wasTriggerClicked,
        isGripClicked,

        isGroupButtonEnabled,
        isUngroupButtonEnabled,
        groupButtonIndex,
        ungroupButtonIndex,

        hsvControl = {
            hsv: { h: 0, s: 0, v: 0 },
            circle: {},
            slider: {}
        },

        isDisplaying = false,
        isVisible = true,
        isOptionsOpen = false,
        isOptionsHeadingRaised = false,
        optionsHeadingURL,
        optionsHeadingScale,

        otherSide,

        // References.
        controlHand,

        // Forward declarations.
        doCommand;


    if (!this instanceof ToolsMenu) {
        return new ToolsMenu();
    }

    controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand();

    function setHand(hand) {
        // Assumes UI is not displaying.
        side = hand;
        if (side === LEFT_HAND) {
            controlHand = rightInputs.hand();
            attachmentJointName = "LeftHand";
            panelLateralOffset = -UIT.dimensions.handLateralOffset;
            menuOriginLocalPosition = PANEL_ORIGIN_POSITION_LEFT_HAND;
            menuOriginLocalRotation = PANEL_ORIGIN_ROTATION_LEFT_HAND;
        } else {
            controlHand = leftInputs.hand();
            attachmentJointName = "RightHand";
            panelLateralOffset = UIT.dimensions.handLateralOffset;
            menuOriginLocalPosition = PANEL_ORIGIN_POSITION_RIGHT_HAND;
            menuOriginLocalRotation = PANEL_ORIGIN_ROTATION_RIGHT_HAND;
        }
        otherSide = (side + 1) % 2;
    }

    setHand(side);

    function getOverlayIDs() {
        return [menuPanelOverlay, menuHeaderOverlay].concat(menuOverlays).concat(optionsOverlays);
    }

    function getIconInfo(tool) {
        // Provides details of tool icon, label, and sublabel images for the specified tool.
        return {
            icon: MENU_ITEMS[tool].icon,
            label: MENU_ITEMS[tool].label,
            sublabel: UI_ELEMENTS.menuButton.sublabel
        };
    }

    function openMenu() {
        var properties,
            itemID,
            buttonID,
            overlayID,
            i,
            length;

        // Update header.
        Overlays.editOverlay(menuHeaderBackOverlay, { visible: false });
        Overlays.editOverlay(menuHeaderTitleOverlay, {
            url: Script.resolvePath(MENU_HEADER_TITLE_PROPERTIES.url),
            scale: MENU_HEADER_TITLE_PROPERTIES.scale
        });
        Overlays.editOverlay(menuHeaderIconOverlay, { visible: false });

        // Display menu items.
        for (i = 0, length = MENU_ITEMS.length; i < length; i += 1) {
            properties = Object.clone(UI_ELEMENTS[MENU_ITEMS[i].type].properties);
            properties = Object.merge(properties, MENU_ITEMS[i].properties);
            properties.visible = isVisible;
            properties.parentID = menuPanelOverlay;
            itemID = Overlays.addOverlay(UI_ELEMENTS[MENU_ITEMS[i].type].overlay, properties);
            menuOverlays[i] = itemID;
            menuEnabled[i] = true;

            // Collision overlay.
            properties = Object.clone(UI_ELEMENTS.menuButton.hoverButton.properties);
            properties.parentID = itemID;
            buttonID = Overlays.addOverlay(UI_ELEMENTS.menuButton.hoverButton.overlay, properties);
            menuHoverOverlays[i] = buttonID;

            // Icon.
            properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.icon.type].properties);
            properties = Object.merge(properties, UI_ELEMENTS.menuButton.icon.properties);
            properties = Object.merge(properties, MENU_ITEMS[i].icon.properties);
            properties.url = Script.resolvePath(properties.url);
            properties.visible = isVisible;
            properties.parentID = buttonID;
            overlayID = Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.icon.type].overlay, properties);
            menuIconOverlays[i] = overlayID;

            // Label.
            properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.label.type].properties);
            properties = Object.merge(properties, UI_ELEMENTS.menuButton.label.properties);
            properties = Object.merge(properties, MENU_ITEMS[i].label.properties);
            properties.url = Script.resolvePath(properties.url);
            properties.visible = isVisible;
            properties.parentID = itemID;
            overlayID = Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.label.type].overlay, properties);
            menuLabelOverlays.push(overlayID);

            // Sublabel.
            properties = Object.clone(UI_ELEMENTS[UI_ELEMENTS.menuButton.sublabel.type].properties);
            properties = Object.merge(properties, UI_ELEMENTS.menuButton.sublabel.properties);
            properties.url = Script.resolvePath(properties.url);
            properties.visible = isVisible;
            properties.parentID = itemID;
            overlayID = Overlays.addOverlay(UI_ELEMENTS[UI_ELEMENTS.menuButton.sublabel.type].overlay, properties);
            menuLabelOverlays.push(overlayID);
        }
    }

    function closeMenu() {
        var i,
            length;

        for (i = 0, length = menuOverlays.length; i < length; i += 1) {
            Overlays.deleteOverlay(menuOverlays[i]);
        }

        menuOverlays = [];
        menuHoverOverlays = [];
        menuIconOverlays = [];
        menuLabelOverlays = [];
        pressedItem = null;
    }

    function openOptions(menuItem) {
        var properties,
            childProperties,
            auxiliaryProperties,
            parentID,
            sublabelModifier,
            value,
            imageOffset,
            IMAGE_OFFSET = 0.0005,
            CIRCLE_CURSOR_SPHERE_DIMENSIONS = { x: 0.01, y: 0.01, z: 0.01 },
            CIRCLE_CURSOR_GAP = 0.002,
            id,
            i,
            length;

        // Remove menu items.
        closeMenu();

        // Update header.
        optionsHeadingURL = Script.resolvePath(menuItem.title.url);
        optionsHeadingScale = menuItem.title.scale;
        Overlays.editOverlay(menuHeaderBackOverlay, { visible: isVisible });
        Overlays.editOverlay(menuHeaderTitleOverlay, {
            url: optionsHeadingURL,
            scale: optionsHeadingScale
        });
        Overlays.editOverlay(menuHeaderIconOverlay, {
            url: Script.resolvePath(menuItem.icon.properties.url),
            dimensions: menuItem.icon.properties.dimensions,
            localPosition: Vec3.sum(MENU_HEADER_ICON_OFFSET, menuItem.icon.headerOffset),
            visible: isVisible
        });

        // Open specified options panel.
        optionsItems = OPTONS_PANELS[menuItem.toolOptions];
        parentID = menuPanelOverlay;
        for (i = 0, length = optionsItems.length; i < length; i += 1) {
            properties = Object.clone(UI_ELEMENTS[optionsItems[i].type].properties);
            if (optionsItems[i].properties) {
                properties = Object.merge(properties, optionsItems[i].properties);
            }
            properties.parentID = parentID;
            if (properties.url) {
                properties.url = Script.resolvePath(properties.url);
            }
            sublabelModifier = null;
            if (optionsItems[i].setting) {
                optionsSettings[optionsItems[i].id] = { key: optionsItems[i].setting.key };
                value = Settings.getValue(optionsItems[i].setting.key);
                if (value === "" && optionsItems[i].setting.defaultValue !== undefined) {
                    value = optionsItems[i].setting.defaultValue;
                }
                optionsSettings[optionsItems[i].id].value = value;
                if (value !== "") {
                    properties[optionsItems[i].setting.property] = value;
                    if (optionsItems[i].type === "toggleButton") {
                        optionsToggles[optionsItems[i].id] = value;
                        properties.color = value
                            ? UI_ELEMENTS[optionsItems[i].type].onColor
                            : UI_ELEMENTS[optionsItems[i].type].offColor;
                        if (optionsItems[i].hasOwnProperty("onSublabel")) {
                            sublabelModifier = value ? optionsItems[i].onSublabel : optionsItems[i].offSublabel;
                        }
                    } else if (optionsItems[i].type === "picklist") {
                        if (value === "custom") {
                            optionsItems[i].label = optionsItems[i].customLabel;
                        } else {
                            optionsItems[i].label = optionsItems[optionsOverlaysIDs.indexOf(value)].label;
                        }
                    }
                    if (optionsItems[i].setting.command) {
                        doCommand(optionsItems[i].setting.command, value);
                    }
                    if (optionsItems[i].setting.callback) {
                        uiCommandCallback(optionsItems[i].setting.callback, value);
                    }
                }
            } else if (optionsItems[i].type === "toggleButton") {
                optionsToggles[optionsItems[i].id] = false;  // Default to off.
            }

            optionsOverlays.push(Overlays.addOverlay(UI_ELEMENTS[optionsItems[i].type].overlay, properties));
            optionsOverlaysIDs.push(optionsItems[i].id);
            if (optionsItems[i].label) {
                childProperties = Object.clone(UI_ELEMENTS.image.properties);
                childProperties = Object.merge(childProperties, UI_ELEMENTS[optionsItems[i].type].label);
                childProperties = Object.merge(childProperties, optionsItems[i].label);
                childProperties.url = Script.resolvePath(childProperties.url);
                childProperties.parentID = optionsOverlays[optionsOverlays.length - 1];
                id = Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties);
                optionsOverlaysLabels[i] = id;
            }
            if (optionsItems[i].sublabel) {
                childProperties = Object.clone(UI_ELEMENTS.image.properties);
                childProperties = Object.merge(childProperties, UI_ELEMENTS[optionsItems[i].type].label);
                childProperties = Object.merge(childProperties, optionsItems[i].sublabel);
                if (sublabelModifier) {
                    childProperties = Object.merge(childProperties, sublabelModifier);
                }
                childProperties.url = Script.resolvePath(childProperties.url);
                childProperties.parentID = optionsOverlays[optionsOverlays.length - 1];
                id = Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties);
                optionsOverlaysSublabels[i] = id;
            }

            if (optionsItems[i].type === "barSlider") {
                // Value and remainder bars.
                optionsSliderData[i] = {};
                auxiliaryProperties = Object.clone(UI_ELEMENTS.barSlider.sliderValue.properties);
                auxiliaryProperties.localPosition = { x: 0, y: (-0.5 + value / 2) * properties.dimensions.y, z: 0 };
                auxiliaryProperties.dimensions = {
                    x: properties.dimensions.x,
                    y: Math.max(value * properties.dimensions.y, MIN_BAR_SLIDER_DIMENSION),
                    z: properties.dimensions.z
                };
                auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1];
                optionsSliderData[i].value = Overlays.addOverlay(UI_ELEMENTS.barSlider.sliderValue.overlay,
                    auxiliaryProperties);
                optionsExtraOverlays.push(optionsSliderData[i].value);
                auxiliaryProperties = Object.clone(UI_ELEMENTS.barSlider.sliderRemainder.properties);
                auxiliaryProperties.localPosition = { x: 0, y: (0.5 - (1.0 - value) / 2) * properties.dimensions.y, z: 0 };
                auxiliaryProperties.dimensions = {
                    x: properties.dimensions.x,
                    y: Math.max((1.0 - value) * properties.dimensions.y, MIN_BAR_SLIDER_DIMENSION),
                    z: properties.dimensions.z
                };
                auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1];
                optionsSliderData[i].remainder = Overlays.addOverlay(UI_ELEMENTS.barSlider.sliderRemainder.overlay,
                    auxiliaryProperties);
                optionsExtraOverlays.push(optionsSliderData[i].remainder);

                // Zero indicator.
                childProperties = Object.clone(UI_ELEMENTS.barSlider.zeroIndicator.properties);
                childProperties.url = Script.resolvePath(childProperties.url);
                childProperties.dimensions = {
                    x: properties.dimensions.x,
                    y: UI_ELEMENTS.barSlider.zeroIndicator.properties.dimensions.y
                };
                childProperties.localPosition = {
                    x: 0,
                    y: 0,
                    z: properties.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset
                };
                childProperties.parentID = optionsOverlays[optionsOverlays.length - 1];
                id = Overlays.addOverlay(UI_ELEMENTS.barSlider.zeroIndicator.overlay, childProperties);
                optionsExtraOverlays.push(id);
            }

            if (optionsItems[i].type === "imageSlider") {
                imageOffset = 0.0;

                // Primary image.
                if (optionsItems[i].imageURL) {
                    childProperties = Object.clone(UI_ELEMENTS.image.properties);
                    childProperties.url = Script.resolvePath(optionsItems[i].imageURL);
                    childProperties.dimensions = { x: properties.dimensions.x, y: properties.dimensions.y };
                    imageOffset += IMAGE_OFFSET;
                    if (optionsItems[i].useBaseColor) {
                        childProperties.color = properties.color;
                    }
                    childProperties.localPosition = { x: 0, y: 0, z: properties.dimensions.z / 2 + imageOffset };
                    hsvControl.slider.localPosition = childProperties.localPosition;
                    childProperties.parentID = optionsOverlays[optionsOverlays.length - 1];
                    hsvControl.slider.colorOverlay = Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties);
                    hsvControl.slider.length = properties.dimensions.y;
                    optionsExtraOverlays.push(hsvControl.slider.colorOverlay);
                }

                // Overlay image.
                if (optionsItems[i].imageOverlayURL) {
                    childProperties = Object.clone(UI_ELEMENTS.image.properties);
                    childProperties.url = Script.resolvePath(optionsItems[i].imageOverlayURL);
                    childProperties.dimensions = { x: properties.dimensions.x, y: properties.dimensions.y };
                    childProperties.emissive = false;
                    imageOffset += IMAGE_OFFSET;
                    childProperties.localPosition = { x: 0, y: 0, z: properties.dimensions.z / 2 + imageOffset };
                    childProperties.parentID = optionsOverlays[optionsOverlays.length - 1];
                    id = Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties);
                    optionsExtraOverlays.push(id);
                }

                // Value pointers.
                optionsSliderData[i] = {};
                optionsSliderData[i].offset =
                    { x: -properties.dimensions.x / 2, y: 0, z: properties.dimensions.z / 2 + imageOffset };
                auxiliaryProperties = Object.clone(UI_ELEMENTS.sliderPointer.properties);
                auxiliaryProperties.localPosition = optionsSliderData[i].offset;
                hsvControl.slider.localPosition = auxiliaryProperties.localPosition;
                auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1];
                id = optionsSliderData[i].value = Overlays.addOverlay(UI_ELEMENTS.sliderPointer.overlay, auxiliaryProperties);
                optionsExtraOverlays.push(id);
                hsvControl.slider.pointerOverlay = optionsSliderData[i].value;
                auxiliaryProperties.localPosition = { x: 0, y: properties.dimensions.x, z: 0 };
                auxiliaryProperties.localRotation = Quat.fromVec3Degrees({ x: 0, y: 0, z: 180 });
                auxiliaryProperties.parentID = optionsSliderData[i].value;
                id = Overlays.addOverlay(UI_ELEMENTS.sliderPointer.overlay, auxiliaryProperties);
                optionsExtraOverlays.push(id);
            }

            if (optionsItems[i].type === "colorCircle") {
                imageOffset = 0.0;

                // Primary image.
                if (optionsItems[i].imageURL) {
                    childProperties = Object.clone(UI_ELEMENTS.image.properties);
                    childProperties.url = Script.resolvePath(optionsItems[i].imageURL);
                    childProperties.scale = properties.dimensions.x;
                    imageOffset += IMAGE_OFFSET;
                    childProperties.localPosition = { x: 0, y: properties.dimensions.y / 2 + imageOffset, z: 0 };
                    childProperties.localRotation = Quat.fromVec3Degrees({ x: -90, y: 90, z: 0 });
                    childProperties.parentID = optionsOverlays[optionsOverlays.length - 1];
                    id = Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties);
                    optionsExtraOverlays.push(id);
                }

                // Overlay image.
                if (optionsItems[i].imageOverlayURL) {
                    childProperties = Object.clone(UI_ELEMENTS.image.properties);
                    childProperties.url = Script.resolvePath(optionsItems[i].imageOverlayURL);
                    childProperties.scale = properties.dimensions.x;
                    imageOffset += IMAGE_OFFSET;
                    childProperties.emissive = false;
                    childProperties.localPosition = { x: 0, y: properties.dimensions.y / 2 + imageOffset, z: 0 };
                    childProperties.localRotation = Quat.fromVec3Degrees({ x: 90, y: 90, z: 0 });
                    childProperties.parentID = optionsOverlays[optionsOverlays.length - 1];
                    childProperties.alpha = 0.0;
                    hsvControl.circle.overlay = Overlays.addOverlay(UI_ELEMENTS.image.overlay, childProperties);
                    optionsExtraOverlays.push(hsvControl.circle.overlay);
                }

                // Value pointers.
                // Invisible sphere at target point with cones as decoration.
                optionsColorData[i] = {};
                optionsColorData[i].offset =
                    { x: 0, y: properties.dimensions.y / 2 + imageOffset, z: 0 };
                auxiliaryProperties = Object.clone(UI_ELEMENTS.sphere.properties);
                auxiliaryProperties.dimensions = CIRCLE_CURSOR_SPHERE_DIMENSIONS;
                auxiliaryProperties.localPosition = optionsColorData[i].offset;
                auxiliaryProperties.parentID = optionsOverlays[optionsOverlays.length - 1];
                auxiliaryProperties.visible = false;
                optionsColorData[i].value = Overlays.addOverlay(UI_ELEMENTS.sphere.overlay, auxiliaryProperties);
                hsvControl.circle.radius = childProperties.scale / 2;
                hsvControl.circle.localPosition = auxiliaryProperties.localPosition;
                hsvControl.circle.cursorOverlay = optionsColorData[i].value;

                auxiliaryProperties = Object.clone(UI_ELEMENTS.circlePointer.properties);
                auxiliaryProperties.parentID = optionsColorData[i].value;
                auxiliaryProperties.localPosition =
                    { x: -(auxiliaryProperties.dimensions.x + CIRCLE_CURSOR_GAP) / 2, y: 0, z: 0 };
                auxiliaryProperties.localRotation = Quat.fromVec3Degrees({ x: 0, y: 90, z: -90 });
                id = Overlays.addOverlay(UI_ELEMENTS.circlePointer.overlay, auxiliaryProperties);
                optionsExtraOverlays.push(id);
                auxiliaryProperties.localPosition =
                    { x: (auxiliaryProperties.dimensions.x + CIRCLE_CURSOR_GAP) / 2, y: 0, z: 0 };
                auxiliaryProperties.localRotation = Quat.fromVec3Degrees({ x: 0, y: 0, z: 90 });
                id = Overlays.addOverlay(UI_ELEMENTS.circlePointer.overlay, auxiliaryProperties);
                optionsExtraOverlays.push(id);
                auxiliaryProperties.localPosition =
                    { x: 0, y: 0, z: -(auxiliaryProperties.dimensions.x + CIRCLE_CURSOR_GAP) / 2 };
                auxiliaryProperties.localRotation = Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 });
                id = Overlays.addOverlay(UI_ELEMENTS.circlePointer.overlay, auxiliaryProperties);
                optionsExtraOverlays.push(id);
                auxiliaryProperties.localPosition =
                    { x: 0, y: 0, z: (auxiliaryProperties.dimensions.x + CIRCLE_CURSOR_GAP) / 2 };
                auxiliaryProperties.localRotation = Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 });
                id = Overlays.addOverlay(UI_ELEMENTS.circlePointer.overlay, auxiliaryProperties);
                optionsExtraOverlays.push(id);
            }

            if (optionsItems[i].type === "swatch" && swatchHighlightOverlay === null) {
                properties = Object.clone(UI_ELEMENTS.swatchHighlight.properties);
                properties.parentID = optionsOverlays[optionsOverlays.length - 1];
                swatchHighlightOverlay = Overlays.addOverlay(UI_ELEMENTS.swatchHighlight.overlay, properties);
            }

            optionsEnabled.push(true);
        }

        // Special handling for Group options.
        if (menuItem.toolOptions === "groupOptions") {
            optionsEnabled[groupButtonIndex] = false;
            optionsEnabled[ungroupButtonIndex] = false;
        }

        isOptionsOpen = true;
    }

    function closeOptions() {
        var i,
            length;

        if (swatchHighlightOverlay !== null) {
            Overlays.deleteOverlay(swatchHighlightOverlay);
            swatchHighlightOverlay = null;
        }

        for (i = 0, length = optionsOverlays.length; i < length; i += 1) {
            Overlays.deleteOverlay(optionsOverlays[i]);
        }
        optionsOverlays = [];

        optionsOverlaysIDs = [];
        optionsOverlaysLabels = [];
        optionsOverlaysSublabels = [];
        optionsSliderData = [];
        optionsColorData = [];
        optionsEnabled = [];
        optionsExtraOverlays = [];
        optionsItems = null;

        isPicklistOpen = false;

        pressedItem = null;

        isOptionsOpen = false;

        // Display menu items.
        openMenu();
    }

    function clearTool() {
        closeOptions();
    }

    function setPresetsLabelToCustom() {
        var label;
        if (optionsSettings.physicsPresets.value !== "custom") {
            optionsSettings.physicsPresets.value = "custom";
            label = optionsItems[optionsOverlaysIDs.indexOf("physicsPresets")].customLabel;
            Overlays.editOverlay(optionsOverlaysLabels[optionsOverlaysIDs.indexOf("physicsPresets")], {
                url: Script.resolvePath(label.url),
                scale: label.scale,
                localPosition: label.localPosition
            });
            Settings.setValue(optionsSettings.physicsPresets.key, "custom");
        }
    }

    function setBarSliderValue(item, fraction) {
        var overlayDimensions,
            otherFraction;

        overlayDimensions = optionsItems[item].properties.dimensions;
        if (overlayDimensions === undefined) {
            overlayDimensions = UI_ELEMENTS.barSlider.properties.dimensions;
        }

        otherFraction = 1.0 - fraction;

        Overlays.editOverlay(optionsSliderData[item].value, {
            localPosition: { x: 0, y: (-0.5 + fraction / 2) * overlayDimensions.y, z: 0 },
            dimensions: {
                x: overlayDimensions.x,
                y: Math.max(fraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION),
                z: overlayDimensions.z
            }
        });
        Overlays.editOverlay(optionsSliderData[item].remainder, {
            localPosition: { x: 0, y: (0.5 - otherFraction / 2) * overlayDimensions.y, z: 0 },
            dimensions: {
                x: overlayDimensions.x,
                y: Math.max(otherFraction * overlayDimensions.y, MIN_BAR_SLIDER_DIMENSION),
                z: overlayDimensions.z
            }
        });
    }

    function hsvToRGB(hsv) {
        // https://en.wikipedia.org/wiki/HSL_and_HSV
        var c, h, x, rgb, m;

        c = hsv.v * hsv.s;
        h = hsv.h * 6.0;
        x = c * (1 - Math.abs(h % 2 - 1));
        if (0 <= h && h <= 1) {
            rgb = { red: c, green: x, blue: 0 };
        } else if (1 < h && h <= 2) {
            rgb = { red: x, green: c, blue: 0 };
        } else if (2 < h && h <= 3) {
            rgb = { red: 0, green: c, blue: x };
        } else if (3 < h && h <= 4) {
            rgb = { red: 0, green: x, blue: c };
        } else if (4 < h && h <= 5) {
            rgb = { red: x, green: 0, blue: c };
        } else {
            rgb = { red: c, green: 0, blue: x };
        }
        m = hsv.v - c;
        rgb = {
            red: Math.round((rgb.red + m) * 255),
            green: Math.round((rgb.green + m) * 255),
            blue: Math.round((rgb.blue + m) * 255)
        };
        return rgb;
    }

    function rgbToHSV(rgb) {
        // https://en.wikipedia.org/wiki/HSL_and_HSV
        var mMax, mMin, c, h, v, s;

        mMax = Math.max(rgb.red, rgb.green, rgb.blue);
        mMin = Math.min(rgb.red, rgb.green, rgb.blue);
        c = mMax - mMin;

        if (c === 0) {
            h = 0;
        } else if (mMax === rgb.red) {
            h = ((rgb.green - rgb.blue) / c) % 6;
        } else if (mMax === rgb.green) {
            h = (rgb.blue - rgb.red) / c + 2;
        } else {
            h = (rgb.red - rgb.green) / c + 4;
        }
        h = h / 6;
        v = mMax / 255;
        s = v === 0 ? 0 : c / mMax;
        return { h: h, s: s, v: v };
    }

    function updateColorCircle() {
        var theta, r, x, y;

        // V overlay alpha per v.
        Overlays.editOverlay(hsvControl.circle.overlay, { alpha: 1.0 - hsvControl.hsv.v });

        // Cursor position per h & s.
        theta = 2 * Math.PI * hsvControl.hsv.h;
        r = hsvControl.hsv.s * hsvControl.circle.radius;
        x = r * Math.cos(theta);
        y = r * Math.sin(theta);
        Overlays.editOverlay(hsvControl.circle.cursorOverlay, {
            // Coordinates based on rotate cylinder entity. TODO: Use FBX model instead of cylinder entity.
            localPosition: { x: -y, y: hsvControl.circle.localPosition.y, z: -x }
        });
    }

    function updateColorSlider() {
        // Base color per h & s.
        Overlays.editOverlay(hsvControl.slider.colorOverlay, {
            color: hsvToRGB({ h: hsvControl.hsv.h, s: hsvControl.hsv.s, v: 1.0 })
        });

        // Slider position per v.
        Overlays.editOverlay(hsvControl.slider.pointerOverlay, {
            localPosition: {
                x: hsvControl.slider.localPosition.x,
                y: (hsvControl.hsv.v - 0.5) * hsvControl.slider.length,
                z: hsvControl.slider.localPosition.z
            }
        });
    }

    function setColorPicker(rgb) {
        hsvControl.hsv = rgbToHSV(rgb);
        updateColorCircle();
        updateColorSlider();
    }

    function setCurrentColor(rgb) {
        Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf("currentColor")], {
            color: rgb
        });
        if (optionsSettings.currentColor) {
            optionsSettings.currentColor.value = rgb;
            Settings.setValue(optionsSettings.currentColor.key, rgb);
        }
    }

    function evaluateParameter(parameter) {
        var parameters,
            overlayID,
            overlayProperty;

        parameters = parameter.split(".");
        overlayID = parameters[0];
        overlayProperty = parameters[1];

        return Overlays.getProperty(optionsOverlays[optionsOverlaysIDs.indexOf(overlayID)], overlayProperty);
    }

    doCommand = function (command, parameter) {
        var index,
            value,
            properties,
            items,
            parentID,
            label,
            values,
            isHighlightingPicklist,
            i,
            length;

        switch (command) {

        case "setPickColor":
            setColorPicker(parameter);
            break;

        case "setColorPerCircle":
            hsvControl.hsv.h = parameter.h;
            hsvControl.hsv.s = parameter.s;
            updateColorSlider();
            value = hsvToRGB(hsvControl.hsv);
            setCurrentColor(value);
            uiCommandCallback("setColor", value);
            break;

        case "setColorPerSlider":
            hsvControl.hsv.v = parameter;
            updateColorCircle();
            value = hsvToRGB(hsvControl.hsv);
            setCurrentColor(value);
            uiCommandCallback("setColor", value);
            break;

        case "setColorPerSwatch":
            index = optionsOverlaysIDs.indexOf(parameter);
            value = optionsSettings[parameter].value;
            if (value !== "") {
                // Set current color to swatch color.
                setCurrentColor(value);
                setColorPicker(value);
                uiCommandCallback("setColor", value);
            } else {
                // Swatch has no color; set swatch color to current color.
                value = optionsSettings.currentColor.value;
                Overlays.editOverlay(optionsOverlays[index], {
                    color: value
                });
                optionsSettings[parameter].value = value;
                Settings.setValue(optionsSettings[parameter].key, value);
            }
            break;

        case "togglePickColor":
            optionsToggles.pickColor = !optionsToggles.pickColor;
            index = optionsOverlaysIDs.indexOf("pickColor");
            Overlays.editOverlay(optionsOverlays[index], {
                color: optionsToggles.pickColor
                    ? UI_ELEMENTS[optionsItems[index].type].onHoverColor
                    : UI_ELEMENTS[optionsItems[index].type].offHoverColor
            });
            uiCommandCallback("pickColorTool", optionsToggles.pickColor);
            break;

        case "setColorFromPick":
            optionsToggles.pickColor = false;
            index = optionsOverlaysIDs.indexOf("pickColor");
            Overlays.editOverlay(optionsOverlays[index], {
                color: UI_ELEMENTS[optionsItems[index].type].offColor
            });
            setCurrentColor(parameter);
            setColorPicker(parameter);
            break;

        case "setGravityOn":
        case "setGrabOn":
        case "setCollideOn":
            value = !optionsSettings[parameter].value;
            optionsSettings[parameter].value = value;
            optionsToggles[parameter] = value;
            Settings.setValue(optionsSettings[parameter].key, value);
            index = optionsOverlaysIDs.indexOf(parameter);
            Overlays.editOverlay(optionsOverlays[index], {
                color: value
                    ? UI_ELEMENTS[optionsItems[index].type].onHoverColor
                    : UI_ELEMENTS[optionsItems[index].type].offHoverColor
            });
            properties = Object.clone(value ? optionsItems[index].onSublabel : optionsItems[index].offSublabel);
            properties.url = Script.resolvePath(properties.url);
            Overlays.editOverlay(optionsOverlaysSublabels[index], properties);
            uiCommandCallback(command, value);
            break;

        case "togglePhysicsPresets":
            if (isPicklistOpen) {
                // Close picklist.
                index = optionsOverlaysIDs.indexOf("physicsPresets");

                // Lower picklist.
                isHighlightingPicklist = highlightedElementType === "picklist";
                Overlays.editOverlay(optionsOverlays[index], {
                    localPosition: isHighlightingPicklist
                        ? Vec3.sum(optionsItems[index].properties.localPosition, OPTION_HOVER_DELTA)
                        : optionsItems[index].properties.localPosition,
                    color: isHighlightingPicklist
                        ? UIT.colors.highlightColor
                        : UI_ELEMENTS.picklist.properties.color
                });
                Overlays.editOverlay(optionsOverlaysSublabels[index], {
                    url: Script.resolvePath("../assets/tools/common/down-arrow.svg")
                });

                // Hide options.
                items = optionsItems[index].items;
                for (i = 0, length = items.length; i < length; i += 1) {
                    index = optionsOverlaysIDs.indexOf(items[i]);
                    Overlays.editOverlay(optionsOverlays[index], {
                        localPosition: Vec3.ZERO,
                        visible: false
                    });
                    Overlays.editOverlay(optionsOverlaysLabels[index], {
                        visible: false
                    });
                }
            }

            isPicklistOpen = !isPicklistOpen;

            if (isPicklistOpen) {
                // Open picklist.
                index = optionsOverlaysIDs.indexOf("physicsPresets");
                parentID = optionsOverlays[index];

                // Raise picklist.
                Overlays.editOverlay(parentID, {
                    localPosition: Vec3.sum(optionsItems[index].properties.localPosition, PICKLIST_HOVER_DELTA)
                });
                Overlays.editOverlay(optionsOverlaysSublabels[index], {
                    url: Script.resolvePath("../assets/tools/common/up-arrow.svg")
                });

                // Show options.
                items = optionsItems[index].items;
                for (i = 0, length = items.length; i < length; i += 1) {
                    index = optionsOverlaysIDs.indexOf(items[i]);
                    Overlays.editOverlay(optionsOverlays[index], {
                        parentID: parentID,
                        localPosition: { x: 0, y: (i + 1) * UI_ELEMENTS.picklistItem.properties.dimensions.y, z: 0 },
                        visible: true
                    });
                    Overlays.editOverlay(optionsOverlaysLabels[index], {
                        visible: true
                    });
                }
            }
            break;

        case "pickPhysicsPreset":
            // Close picklist.
            doCommand("togglePhysicsPresets");

            // Update picklist label.
            label = optionsItems[optionsOverlaysIDs.indexOf(parameter)].label;
            Overlays.editOverlay(optionsOverlaysLabels[optionsOverlaysIDs.indexOf("physicsPresets")], {
                url: Script.resolvePath(label.url),
                scale: label.scale,
                localPosition: label.localPosition
            });
            optionsSettings.physicsPresets.value = parameter;
            Settings.setValue(optionsSettings.physicsPresets.key, parameter);

            // Update sliders.
            values = PHYSICS_SLIDER_PRESETS[parameter];
            setBarSliderValue(optionsOverlaysIDs.indexOf("gravitySlider"), values.gravity);
            Settings.setValue(optionsSettings.gravitySlider.key, values.gravity);
            uiCommandCallback("setGravity", values.gravity);
            setBarSliderValue(optionsOverlaysIDs.indexOf("bounceSlider"), values.bounce);
            Settings.setValue(optionsSettings.bounceSlider.key, values.bounce);
            uiCommandCallback("setBounce", values.bounce);
            setBarSliderValue(optionsOverlaysIDs.indexOf("frictionSlider"), values.friction);
            Settings.setValue(optionsSettings.frictionSlider.key, values.friction);
            uiCommandCallback("setFriction", values.friction);
            setBarSliderValue(optionsOverlaysIDs.indexOf("densitySlider"), values.density);
            Settings.setValue(optionsSettings.densitySlider.key, values.density);
            uiCommandCallback("setDensity", values.density);

            break;

        case "setGravity":
            setPresetsLabelToCustom();
            Settings.setValue(optionsSettings.gravitySlider.key, parameter);
            uiCommandCallback("setGravity", parameter);
            break;
        case "setBounce":
            setPresetsLabelToCustom();
            Settings.setValue(optionsSettings.bounceSlider.key, parameter);
            uiCommandCallback("setBounce", parameter);
            break;
        case "setFriction":
            setPresetsLabelToCustom();
            Settings.setValue(optionsSettings.frictionSlider.key, parameter);
            uiCommandCallback("setFriction", parameter);
            break;
        case "setDensity":
            setPresetsLabelToCustom();
            Settings.setValue(optionsSettings.densitySlider.key, parameter);
            uiCommandCallback("setDensity", parameter);
            break;

        case "closeOptions":
            closeOptions();
            break;

        case "clearTool":
            uiCommandCallback("clearTool");
            break;

        default:
            App.log(side, "ERROR: ToolsMenu: Unexpected command! " + command);
        }
    };

    function doGripClicked(command, parameter) {
        switch (command) {
        case "clearSwatch":
            // Remove highlight ring and change swatch to current color.
            Overlays.editOverlay(swatchHighlightOverlay, { visible: false });
            Overlays.editOverlay(optionsOverlays[optionsOverlaysIDs.indexOf(parameter)], {
                color: optionsSettings.currentColor.value,
                dimensions: UI_ELEMENTS.swatch.properties.dimensions
            });
            optionsSettings[parameter].value = "";  // Emulate Settings.getValue() returning "" for nonexistent setting.
            Settings.setValue(optionsSettings[parameter].key, null);  // Delete settings value.
            break;
        default:
            App.log(side, "ERROR: ToolsMenu: Unexpected command! " + command);
        }
    }

    function adjustSliderFraction(fraction) {
        // Makes slider values achieve and saturate at 0.0 and 1.0.
        return Math.min(1.0, Math.max(0.0, fraction * 1.01 - 0.005));
    }

    function setVisible(visible) {
        var i,
            length;

        for (i = 0, length = staticOverlays.length; i < length; i += 1) {
            Overlays.editOverlay(staticOverlays[i], { visible: visible });
        }

        if (isOptionsOpen) {
            Overlays.editOverlay(menuHeaderBackOverlay, { visible: visible });
            Overlays.editOverlay(menuHeaderIconOverlay, { visible: visible });
            for (i = 0, length = optionsOverlays.length; i < length; i += 1) {
                Overlays.editOverlay(optionsOverlays[i], { visible: visible });
            }
            for (i = 0, length = optionsOverlaysLabels.length; i < length; i += 1) {
                Overlays.editOverlay(optionsOverlaysLabels[i], { visible: visible });
            }
            for (i = 0, length = optionsOverlaysSublabels.length; i < length; i += 1) {
                Overlays.editOverlay(optionsOverlaysSublabels[i], { visible: visible });
            }
            for (i = 0, length = optionsExtraOverlays.length; i < length; i += 1) {
                Overlays.editOverlay(optionsExtraOverlays[i], { visible: visible });
            }
        } else {
            for (i = 0, length = menuOverlays.length; i < length; i += 1) {
                Overlays.editOverlay(menuOverlays[i], { visible: visible });
            }
            for (i = 0, length = menuIconOverlays.length; i < length; i += 1) {
                Overlays.editOverlay(menuIconOverlays[i], { visible: visible });
            }
            for (i = 0, length = menuLabelOverlays.length; i < length; i += 1) {
                Overlays.editOverlay(menuLabelOverlays[i], { visible: visible });
            }
            if (!visible) {
                for (i = 0, length = menuHoverOverlays.length; i < length; i += 1) {
                    Overlays.editOverlay(menuHoverOverlays[i], { visible: false });
                }
            }
        }

        isVisible = visible;
    }

    function update(intersection, groupsCount, entitiesCount) {
        var intersectedItem = NONE,
            intersectionItems,
            color,
            localPosition,
            parameter,
            parameterValue,
            enableGroupButton,
            enableUngroupButton,
            sliderProperties,
            overlayDimensions,
            basePoint,
            fraction,
            delta,
            radius,
            x,
            y,
            s,
            h;

        isTriggerClicked = controlHand.triggerClicked();

        // Handle heading.
        if (isOptionsOpen && intersection.overlayID === menuHeaderOverlay) {
            if (isTriggerClicked && !wasTriggerClicked) {
                // Lower and unhighlight heading; go back to Tools menu.
                Overlays.editOverlay(menuHeaderHeadingOverlay, {
                    localPosition: MENU_HEADER_HEADING_PROPERTIES.localPosition,
                    color: UIT.colors.baseGray,
                    emissive: false
                });
                isOptionsHeadingRaised = false;
                doCommand("clearTool");
            } else if (!isOptionsHeadingRaised) {
                // Hover heading.
                Feedback.play(otherSide, Feedback.HOVER_BUTTON);
                Overlays.editOverlay(menuHeaderHeadingOverlay, {
                    localPosition: Vec3.sum(MENU_HEADER_HEADING_PROPERTIES.localPosition, MENU_HEADER_HOVER_OFFSET),
                    color: UIT.colors.greenHighlight,
                    emissive: true  // TODO: This has no effect.
                });
                Overlays.editOverlay(menuHeaderBackOverlay, {
                    color: UIT.colors.white
                });
                Overlays.editOverlay(menuHeaderTitleOverlay, {
                    url: Script.resolvePath(MENU_HEADER_TITLE_BACK_URL),
                    scale: MENU_HEADER_TITLE_BACK_SCALE
                });
                Overlays.editOverlay(menuHeaderIconOverlay, {
                    visible: false
                });
                isOptionsHeadingRaised = true;
            }
        } else {
            if (isOptionsHeadingRaised) {
                // Unhover heading.
                Overlays.editOverlay(menuHeaderHeadingOverlay, {
                    localPosition: MENU_HEADER_HEADING_PROPERTIES.localPosition,
                    color: UIT.colors.baseGray,
                    emissive: false
                });
                Overlays.editOverlay(menuHeaderBackOverlay, {
                    color: MENU_HEADER_BACK_PROPERTIES.color
                });
                Overlays.editOverlay(menuHeaderTitleOverlay, {
                    url: optionsHeadingURL,
                    scale: optionsHeadingScale
                });
                Overlays.editOverlay(menuHeaderIconOverlay, {
                    visible: isVisible
                });
                isOptionsHeadingRaised = false;
            }
        }

        // Intersection details for menus and options.
        intersectionOverlays = null;
        intersectionEnabled = null;
        if (intersection.overlayID) {
            intersectedItem = menuOverlays.indexOf(intersection.overlayID);
            if (intersectedItem !== NONE) {
                intersectionItems = MENU_ITEMS;
                intersectionOverlays = menuOverlays;
                intersectionEnabled = menuEnabled;
            } else {
                intersectedItem = optionsOverlays.indexOf(intersection.overlayID);
                if (intersectedItem !== NONE) {
                    intersectionItems = optionsItems;
                    intersectionOverlays = optionsOverlays;
                    intersectionEnabled = optionsEnabled;
                }
            }
        }

        // Highlight clickable item.
        if (intersectedItem !== highlightedItem || intersectionOverlays !== highlightedSourceOverlays) {

            if (highlightedItem !== NONE) {
                // Unhover old item.
                switch (highlightedElementType) {
                case "menuButton":
                    Overlays.editOverlay(menuHoverOverlays[highlightedItem], {
                        localPosition: UI_ELEMENTS.menuButton.hoverButton.properties.localPosition,
                        visible: false
                    });
                    Overlays.editOverlay(menuIconOverlays[highlightedItem], {
                        color: UI_ELEMENTS.menuButton.icon.properties.color
                    });
                    break;
                case "button":
                    if (highlightedSourceItems[highlightedItem].enabledColor !== undefined && optionsEnabled[highlightedItem]) {
                        color = highlightedSourceItems[highlightedItem].enabledColor;
                    } else {
                        color = highlightedSourceItems[highlightedItem].properties.color !== undefined
                            ? highlightedSourceItems[highlightedItem].properties.color
                            : UI_ELEMENTS.button.properties.color;
                    }
                    Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], {
                        color: color,
                        localPosition: highlightedSourceItems[highlightedItem].properties.localPosition
                    });
                    break;
                case "toggleButton":
                    color = optionsToggles[highlightedSourceItems[highlightedItem].id]
                        ? UI_ELEMENTS.toggleButton.onColor
                        : UI_ELEMENTS.toggleButton.offColor;
                    Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], {
                        color: color,
                        localPosition: highlightedSourceItems[highlightedItem].properties.localPosition
                    });
                    break;
                case "swatch":
                    Overlays.editOverlay(swatchHighlightOverlay, { visible: false });
                    color = optionsSettings[highlightedSourceItems[highlightedItem].id].value;
                    Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], {
                        dimensions: UI_ELEMENTS.swatch.properties.dimensions,
                        color: color === "" ? EMPTY_SWATCH_COLOR : color,
                        localPosition: highlightedSourceItems[highlightedItem].properties.localPosition
                    });
                    break;
                case "barSlider":
                case "imageSlider":
                case "colorCircle":
                    // Lower old slider or color circle.
                    Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], {
                        localPosition: highlightedSourceItems[highlightedItem].properties.localPosition
                    });
                    break;
                case "picklist":
                    if (highlightedSourceItems[highlightedItem].type !== "picklistItem" && !isPicklistOpen) {
                        Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], {
                            localPosition: highlightedSourceItems[highlightedItem].properties.localPosition,
                            color: UI_ELEMENTS.picklist.properties.color
                        });
                    } else {
                        Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], {
                            color: UIT.colors.darkGray
                        });
                    }
                    break;
                case "picklistItem":
                    Overlays.editOverlay(highlightedSourceOverlays[highlightedItem], {
                        color: UI_ELEMENTS.picklistItem.properties.color
                    });
                    break;
                case null:
                    // Nothing to do.
                    break;
                default:
                    App.log(side, "ERROR: ToolsMenu: Unexpected hover item! " + highlightedElementType);
                }

                // Update status variables.
                highlightedItem = NONE;
                isHighlightingButtonElement = false;
                highlightedElementType = null;
            }

            if (intersectedItem !== NONE && intersectionItems[intersectedItem] &&
                    (intersectionItems[intersectedItem].command !== undefined
                    || intersectionItems[intersectedItem].callback !== undefined)) {

                // Update status variables.
                highlightedItem = intersectedItem;
                highlightedItems = intersectionItems;
                isHighlightingButtonElement = BUTTON_UI_ELEMENTS.indexOf(intersectionItems[highlightedItem].type) !== NONE;
                highlightedElementType = intersectionItems[highlightedItem].type;

                // Hover new item.
                switch (highlightedElementType) {
                case "menuButton":
                    Feedback.play(otherSide, Feedback.HOVER_MENU_ITEM);
                    Overlays.editOverlay(menuHoverOverlays[highlightedItem], {
                        localPosition: Vec3.sum(UI_ELEMENTS.menuButton.hoverButton.properties.localPosition, MENU_HOVER_DELTA),
                        visible: true
                    });
                    Overlays.editOverlay(menuIconOverlays[highlightedItem], {
                        color: UI_ELEMENTS.menuButton.icon.highlightColor
                    });
                    break;
                case "button":
                    if (intersectionEnabled[highlightedItem]) {
                        Feedback.play(otherSide, Feedback.HOVER_BUTTON);
                        localPosition = intersectionItems[highlightedItem].properties.localPosition;
                        Overlays.editOverlay(intersectionOverlays[highlightedItem], {
                            color: intersectionItems[highlightedItem].highlightColor !== undefined
                                ? intersectionItems[highlightedItem].highlightColor
                                : UIT.colors.greenHighlight,
                            localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA)
                        });
                    }
                    break;
                case "toggleButton":
                    if (intersectionEnabled[highlightedItem]) {
                        Feedback.play(otherSide, Feedback.HOVER_BUTTON);
                        localPosition = intersectionItems[highlightedItem].properties.localPosition;
                        Overlays.editOverlay(intersectionOverlays[highlightedItem], {
                            color: optionsToggles[intersectionItems[highlightedItem].id]
                                ? UI_ELEMENTS.toggleButton.onHoverColor
                                : UI_ELEMENTS.toggleButton.offHoverColor,
                            localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA)
                        });
                    }
                    break;
                case "swatch":
                    Feedback.play(otherSide, Feedback.HOVER_BUTTON);
                    localPosition = intersectionItems[highlightedItem].properties.localPosition;
                    if (optionsSettings[intersectionItems[highlightedItem].id].value === "") {
                        // Swatch is empty; highlight it with current color.
                        Overlays.editOverlay(intersectionOverlays[highlightedItem], {
                            color: optionsSettings.currentColor.value,
                            localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA)
                        });
                    } else {
                        // Swatch is full; highlight it with ring.
                        Overlays.editOverlay(intersectionOverlays[highlightedItem], {
                            dimensions: Vec3.subtract(UI_ELEMENTS.swatch.properties.dimensions,
                                { x: SWATCH_HIGHLIGHT_DELTA, y: 0, z: SWATCH_HIGHLIGHT_DELTA }),
                            localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA)
                        });
                        Overlays.editOverlay(swatchHighlightOverlay, {
                            parentID: intersectionOverlays[highlightedItem],
                            localPosition: UI_ELEMENTS.swatchHighlight.properties.localPosition,
                            visible: true
                        });
                    }
                    break;
                case "barSlider":
                case "imageSlider":
                case "colorCircle":
                    Feedback.play(otherSide, Feedback.HOVER_BUTTON);
                    localPosition = intersectionItems[highlightedItem].properties.localPosition;
                    Overlays.editOverlay(intersectionOverlays[highlightedItem], {
                        localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA)
                    });
                    break;
                case "picklist":
                    Feedback.play(otherSide, Feedback.HOVER_BUTTON);
                    if (!isPicklistOpen) {
                        localPosition = intersectionItems[highlightedItem].properties.localPosition;
                        Overlays.editOverlay(intersectionOverlays[highlightedItem], {
                            localPosition: Vec3.sum(localPosition, OPTION_HOVER_DELTA),
                            color: UIT.colors.greenHighlight
                        });
                    } else {
                        Overlays.editOverlay(intersectionOverlays[highlightedItem], {
                            color: UIT.colors.greenHighlight
                        });
                    }
                    break;
                case "picklistItem":
                    Feedback.play(otherSide, Feedback.HOVER_BUTTON);
                    Overlays.editOverlay(intersectionOverlays[highlightedItem], {
                        color: UIT.colors.greenHighlight
                    });
                    break;
                case null:
                    // Nothing to do.
                    break;
                default:
                    App.log(side, "ERROR: ToolsMenu: Unexpected hover item! " + highlightedElementType);
                }
            }

            highlightedSourceOverlays = intersectionOverlays;
            highlightedSourceItems = intersectionItems;
        }

        // Press/unpress button.
        if ((pressedItem && intersectedItem !== pressedItem.index) || intersectionOverlays !== pressedSource
                || isTriggerClicked !== (pressedItem !== null)) {
            if (pressedItem) {
                // Unpress previous button.
                Overlays.editOverlay(pressedSource[pressedItem.index], {
                    localPosition: isHighlightingButtonElement && highlightedItem === pressedItem.index
                        ? Vec3.sum(pressedItem.localPosition, OPTION_HOVER_DELTA)
                        : pressedItem.localPosition
                });
                pressedItem = null;
                pressedSource = null;
            }
            if (isHighlightingButtonElement && (intersectionEnabled === null || intersectionEnabled[intersectedItem])
                    && isTriggerClicked && !wasTriggerClicked) {
                // Press new button.
                localPosition = intersectionItems[intersectedItem].properties.localPosition;
                if (highlightedElementType !== "menuButton") {
                    Overlays.editOverlay(intersectionOverlays[intersectedItem], {
                        localPosition: Vec3.sum(localPosition, BUTTON_PRESS_DELTA)
                    });
                }
                pressedSource = intersectionOverlays;
                pressedItem = {
                    index: intersectedItem,
                    localPosition: localPosition
                };

                // Button press actions.
                if (intersectionOverlays === menuOverlays) {
                    openOptions(intersectionItems[intersectedItem]);
                }
                if (intersectionItems[intersectedItem].command) {
                    parameter = intersectionItems[intersectedItem].id;
                    doCommand(intersectionItems[intersectedItem].command.method, parameter);
                }
                if (intersectionItems[intersectedItem].callback) {
                    if (intersectionItems[intersectedItem].callback.parameter) {
                        parameterValue = evaluateParameter(intersectionItems[intersectedItem].callback.parameter);
                    }
                    uiCommandCallback(intersectionItems[intersectedItem].callback.method, parameterValue);
                }
            }
        }

        // Picklist update.
        if (intersectionItems && ((highlightedElementType === "picklist"
                && controlHand.triggerClicked() !== isPicklistPressed)
                    || (intersectionItems[intersectedItem].type !== "picklist" && isPicklistPressed))) {
            isPicklistPressed = controlHand.triggerClicked();
            if (isPicklistPressed) {
                doCommand(intersectionItems[intersectedItem].command.method, intersectionItems[intersectedItem].id);
            }
        }
        if (intersectionItems && ((highlightedElementType === "picklistItem"
                && controlHand.triggerClicked() !== isPicklistItemPressed)
                    || (intersectionItems[intersectedItem].type !== "picklistItem" && isPicklistItemPressed))) {
            isPicklistItemPressed = controlHand.triggerClicked();
            if (isPicklistItemPressed) {
                doCommand(intersectionItems[intersectedItem].command.method, intersectionItems[intersectedItem].id);
            }
        }
        if (intersectionItems && isPicklistOpen && controlHand.triggerClicked()
                && highlightedElementType !== "picklist" && highlightedElementType !== "picklistItem") {
            doCommand("togglePhysicsPresets");
        }

        // Grip click.
        if (controlHand.gripClicked() !== isGripClicked) {
            isGripClicked = !isGripClicked;
            if (isGripClicked && intersectionItems && intersectedItem && intersectionItems[intersectedItem].clear) {
                controlHand.setGripClickedHandled();
                parameter = intersectionItems[intersectedItem].id;
                doGripClicked(intersectionItems[intersectedItem].clear.method, parameter);
            }
        }

        // Bar slider update.
        if (intersectionItems && highlightedElementType === "barSlider" && controlHand.triggerClicked()) {
            sliderProperties = Overlays.getProperties(intersection.overlayID, ["position", "orientation"]);
            overlayDimensions = intersectionItems[intersectedItem].properties.dimensions;
            if (overlayDimensions === undefined) {
                overlayDimensions = UI_ELEMENTS.barSlider.properties.dimensions;
            }
            basePoint = Vec3.sum(sliderProperties.position,
                Vec3.multiplyQbyV(sliderProperties.orientation, { x: 0, y: -overlayDimensions.y / 2, z: 0 }));
            fraction = Vec3.dot(Vec3.subtract(intersection.intersection, basePoint),
                Vec3.multiplyQbyV(sliderProperties.orientation, Vec3.UNIT_Y)) / overlayDimensions.y;
            fraction = adjustSliderFraction(fraction);
            setBarSliderValue(intersectedItem, fraction);
            if (intersectionItems[intersectedItem].command) {
                doCommand(intersectionItems[intersectedItem].command.method, fraction);
            }
        }

        // Image slider update.
        if (intersectionItems && highlightedElementType === "imageSlider" && controlHand.triggerClicked()) {
            sliderProperties = Overlays.getProperties(intersection.overlayID, ["position", "orientation"]);
            overlayDimensions = intersectionItems[intersectedItem].properties.dimensions;
            if (overlayDimensions === undefined) {
                overlayDimensions = UI_ELEMENTS.imageSlider.properties.dimensions;
            }
            basePoint = Vec3.sum(sliderProperties.position,
                Vec3.multiplyQbyV(sliderProperties.orientation, { x: 0, y: -overlayDimensions.y / 2, z: 0 }));
            fraction = Vec3.dot(Vec3.subtract(intersection.intersection, basePoint),
                Vec3.multiplyQbyV(sliderProperties.orientation, Vec3.UNIT_Y)) / overlayDimensions.y;
            fraction = adjustSliderFraction(fraction);
            Overlays.editOverlay(optionsSliderData[intersectedItem].value, {
                localPosition: Vec3.sum(optionsSliderData[intersectedItem].offset,
                    { x: 0, y: (fraction - 0.5) * overlayDimensions.y, z: 0 })
            });
            if (intersectionItems[intersectedItem].command) {
                doCommand(intersectionItems[intersectedItem].command.method, fraction);
            }
        }

        // Color circle update.
        if (intersectionItems && highlightedElementType === "colorCircle" && controlHand.triggerClicked()) {
            sliderProperties = Overlays.getProperties(intersection.overlayID, ["position", "orientation"]);
            delta = Vec3.multiplyQbyV(Quat.inverse(sliderProperties.orientation),
                Vec3.subtract(intersection.intersection, sliderProperties.position));
            radius = Vec3.length(delta);
            if (radius > hsvControl.circle.radius) {
                delta = Vec3.multiply(hsvControl.circle.radius / radius, delta);
            }
            Overlays.editOverlay(optionsColorData[intersectedItem].value, {
                localPosition: Vec3.sum(optionsColorData[intersectedItem].offset,
                    { x: delta.x, y: 0, z: delta.z })
            });
            if (intersectionItems[intersectedItem].command) {
                // Cartesian planar coordinates.
                x = -delta.z;  // Coordinates based on rotate cylinder entity. TODO: Use FBX model instead of cylinder entity.
                y = -delta.x;  // ""
                s = Math.sqrt(x * x + y * y) / hsvControl.circle.radius;
                h = Math.atan2(y, x) / (2 * Math.PI);
                if (h < 0) {
                    h = h + 1;
                }
                doCommand(intersectionItems[intersectedItem].command.method, { h: h, s: s });
            }
        }

        // Special handling for Group options.
        if (optionsItems && optionsItems === OPTONS_PANELS.groupOptions) {
            enableGroupButton = groupsCount > 1;
            if (enableGroupButton !== isGroupButtonEnabled) {
                isGroupButtonEnabled = enableGroupButton;
                Overlays.editOverlay(optionsOverlays[groupButtonIndex], {
                    color: isGroupButtonEnabled
                        ? (highlightedItem === groupButtonIndex
                            ? OPTONS_PANELS.groupOptions[groupButtonIndex].highlightColor
                            : OPTONS_PANELS.groupOptions[groupButtonIndex].enabledColor)
                        : OPTONS_PANELS.groupOptions[groupButtonIndex].properties.color
                });
                Overlays.editOverlay(optionsOverlaysLabels[groupButtonIndex], {
                    color: isGroupButtonEnabled
                        ? OPTONS_PANELS.groupOptions[groupButtonIndex].labelEnabledColor
                        : OPTONS_PANELS.groupOptions[groupButtonIndex].label.color
                });
                optionsEnabled[groupButtonIndex] = enableGroupButton;
            }

            enableUngroupButton = groupsCount === 1 && entitiesCount > 1;
            if (enableUngroupButton !== isUngroupButtonEnabled) {
                isUngroupButtonEnabled = enableUngroupButton;
                Overlays.editOverlay(optionsOverlays[ungroupButtonIndex], {
                    color: isUngroupButtonEnabled
                        ? (highlightedItem === ungroupButtonIndex
                            ? OPTONS_PANELS.groupOptions[ungroupButtonIndex].highlightColor
                            : OPTONS_PANELS.groupOptions[ungroupButtonIndex].enabledColor)
                        : OPTONS_PANELS.groupOptions[ungroupButtonIndex].properties.color
                });
                Overlays.editOverlay(optionsOverlaysLabels[ungroupButtonIndex], {
                    color: isUngroupButtonEnabled
                        ? OPTONS_PANELS.groupOptions[ungroupButtonIndex].labelEnabledColor
                        : OPTONS_PANELS.groupOptions[ungroupButtonIndex].label.color
                });
                optionsEnabled[ungroupButtonIndex] = enableUngroupButton;
            }
        }

        wasTriggerClicked = isTriggerClicked;
    }

    function display() {
        // Creates and shows menu entities.
        var handJointIndex,
            properties,
            id,
            i,
            length;

        if (isDisplaying) {
            return;
        }

        // Joint index.
        handJointIndex = MyAvatar.getJointIndex(attachmentJointName);
        if (handJointIndex === NONE) {
            // Don't display if joint isn't available (yet) to attach to.
            // User can clear this condition by toggling the app off and back on once avatar finishes loading.
            App.log(side, "ERROR: ToolsMenu: Hand joint index isn't available!");
            return;
        }

        // Menu origin.
        properties = Object.clone(MENU_ORIGIN_PROPERTIES);
        properties.parentJointIndex = handJointIndex;
        properties.localPosition = Vec3.sum(menuOriginLocalPosition, { x: panelLateralOffset, y: 0, z: 0 });
        properties.localRotation = menuOriginLocalRotation;
        menuOriginOverlay = Overlays.addOverlay("sphere", properties);

        // Header.
        properties = Object.clone(MENU_HEADER_PROPERTIES);
        properties.parentID = menuOriginOverlay;
        menuHeaderOverlay = Overlays.addOverlay("cube", properties);
        properties = Object.clone(MENU_HEADER_HEADING_PROPERTIES);
        properties.parentID = menuHeaderOverlay;
        menuHeaderHeadingOverlay = Overlays.addOverlay("cube", properties);
        properties = Object.clone(MENU_HEADER_BAR_PROPERTIES);
        properties.parentID = menuHeaderHeadingOverlay;
        menuHeaderBarOverlay = Overlays.addOverlay("cube", properties);

        // Heading content.
        properties = Object.clone(MENU_HEADER_BACK_PROPERTIES);
        properties.parentID = menuHeaderHeadingOverlay;
        properties.url = Script.resolvePath(properties.url);
        menuHeaderBackOverlay = Overlays.addOverlay("image3d", properties);
        properties = Object.clone(MENU_HEADER_TITLE_PROPERTIES);
        properties.parentID = menuHeaderHeadingOverlay;
        properties.url = Script.resolvePath(properties.url);
        menuHeaderTitleOverlay = Overlays.addOverlay("image3d", properties);
        properties = Object.clone(MENU_HEADER_ICON_PROPERTIES);
        properties.parentID = menuHeaderHeadingOverlay;
        properties.url = Script.resolvePath(properties.url);
        menuHeaderIconOverlay = Overlays.addOverlay("image3d", properties);

        // Panel background.
        properties = Object.clone(MENU_PANEL_PROPERTIES);
        properties.parentID = menuOriginOverlay;
        menuPanelOverlay = Overlays.addOverlay("cube", properties);

        // Always-visible overlays.
        staticOverlays = [menuHeaderOverlay, menuHeaderHeadingOverlay, menuHeaderBarOverlay, menuHeaderTitleOverlay,
            menuPanelOverlay];

        // Menu items.
        openMenu();

        // Initial values.
        optionsItems = null;
        intersectionOverlays = null;
        intersectionEnabled = null;
        highlightedItem = NONE;
        highlightedSourceOverlays = null;
        isHighlightingButtonElement = false;
        highlightedElementType = null;
        pressedItem = null;
        pressedSource = null;
        isPicklistOpen = false;
        isPicklistPressed = false;
        isPicklistItemPressed = false;
        isTriggerClicked = false;
        wasTriggerClicked = false;
        isGripClicked = false;

        // Special handling for Group options.
        isGroupButtonEnabled = false;
        isUngroupButtonEnabled = false;
        for (i = 0, length = OPTONS_PANELS.groupOptions.length; i < length; i += 1) {
            id = OPTONS_PANELS.groupOptions[i].id;
            if (id === "groupButton") {
                groupButtonIndex = i;
            }
            if (id === "ungroupButton") {
                ungroupButtonIndex = i;
            }
        }

        isDisplaying = true;
    }

    function clear() {
        // Deletes menu entities.
        if (!isDisplaying) {
            return;
        }
        Overlays.deleteOverlay(menuOriginOverlay);  // Automatically deletes all other overlays because they're children.
        menuOverlays = [];
        menuHoverOverlays = [];
        menuIconOverlays = [];
        menuLabelOverlays = [];
        optionsOverlays = [];
        optionsOverlaysLabels = [];
        optionsOverlaysSublabels = [];
        optionsExtraOverlays = [];
        isDisplaying = false;
    }

    function destroy() {
        clear();
    }

    return {
        COLOR_TOOL: COLOR_TOOL,
        SCALE_TOOL: SCALE_TOOL,
        CLONE_TOOL: CLONE_TOOL,
        GROUP_TOOL: GROUP_TOOL,
        PHYSICS_TOOL: PHYSICS_TOOL,
        DELETE_TOOL: DELETE_TOOL,
        iconInfo: getIconInfo,
        setHand: setHand,
        overlayIDs: getOverlayIDs,
        clearTool: clearTool,
        doCommand: doCommand,
        setVisible: setVisible,
        update: update,
        display: display,
        clear: clear,
        destroy: destroy
    };
};

ToolsMenu.prototype = {};