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