diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 898e9ad0cd..cc1f74c539 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -963,7 +963,7 @@ Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); Setting::Handle sessionRunTime{ "sessionRunTime", 0 }; -const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 70.0f; +const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 60.0f; const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f; const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 0b18947f76..78744b320b 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -57,6 +57,10 @@ class QScriptEngine; * @property {Uuid} tabletScreenID - The UUID of the tablet's screen overlay. * @property {Uuid} homeButtonID - The UUID of the tablet's "home" button overlay. * @property {Uuid} homeButtonHighlightID - The UUID of the tablet's "home" button highlight overlay. + * @property {Uuid} miniTabletID - The UUID of the mini tablet's body model overlay. null if not in HMD mode. + * @property {Uuid} miniTabletScreenID - The UUID of the mini tablet's screen overlay. null if not in HMD mode. + * @property {number} miniTabletHand - The hand that the mini tablet is displayed on: 0 for left hand, + * 1 for right hand, -1 if not in HMD mode. */ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Dependency { Q_OBJECT @@ -68,6 +72,9 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen Q_PROPERTY(QUuid homeButtonID READ getCurrentHomeButtonID WRITE setCurrentHomeButtonID) Q_PROPERTY(QUuid tabletScreenID READ getCurrentTabletScreenID WRITE setCurrentTabletScreenID) Q_PROPERTY(QUuid homeButtonHighlightID READ getCurrentHomeButtonHighlightID WRITE setCurrentHomeButtonHighlightID) + Q_PROPERTY(QUuid miniTabletID READ getCurrentMiniTabletID WRITE setCurrentMiniTabletID) + Q_PROPERTY(QUuid miniTabletScreenID READ getCurrentMiniTabletScreenID WRITE setCurrentMiniTabletScreenID) + Q_PROPERTY(int miniTabletHand READ getCurrentMiniTabletHand WRITE setCurrentMiniTabletHand) public: @@ -368,6 +375,15 @@ public: void setCurrentTabletScreenID(QUuid tabletID) { _tabletScreenID = tabletID; } QUuid getCurrentTabletScreenID() const { return _tabletScreenID; } + void setCurrentMiniTabletID(QUuid miniTabletID) { _miniTabletID = miniTabletID; } + QUuid getCurrentMiniTabletID() const { return _miniTabletID; } + + void setCurrentMiniTabletScreenID(QUuid miniTabletScreenID) { _miniTabletScreenID = miniTabletScreenID; } + QUuid getCurrentMiniTabletScreenID() const { return _miniTabletScreenID; } + + void setCurrentMiniTabletHand(int miniTabletHand) { _miniTabletHand = miniTabletHand; } + int getCurrentMiniTabletHand() const { return _miniTabletHand; } + private: bool _showTablet { false }; bool _tabletContextualMode { false }; @@ -376,6 +392,9 @@ private: QUuid _homeButtonID; QUuid _tabletEntityID; QUuid _homeButtonHighlightID; + QUuid _miniTabletID; + QUuid _miniTabletScreenID; + int _miniTabletHand { -1 }; // Get the position of the HMD glm::vec3 getPosition() const; diff --git a/libraries/networking/src/MessagesClient.cpp b/libraries/networking/src/MessagesClient.cpp index 2302c22a48..d6f9d041ea 100644 --- a/libraries/networking/src/MessagesClient.cpp +++ b/libraries/networking/src/MessagesClient.cpp @@ -116,32 +116,33 @@ void MessagesClient::handleMessagesPacket(QSharedPointer receiv void MessagesClient::sendMessage(QString channel, QString message, bool localOnly) { auto nodeList = DependencyManager::get(); + QUuid senderID = nodeList->getSessionUUID(); if (localOnly) { - QUuid senderID = nodeList->getSessionUUID(); emit messageReceived(channel, message, senderID, true); } else { SharedNodePointer messagesMixer = nodeList->soloNodeOfType(NodeType::MessagesMixer); - if (messagesMixer) { - QUuid senderID = nodeList->getSessionUUID(); auto packetList = encodeMessagesPacket(channel, message, senderID); nodeList->sendPacketList(std::move(packetList), *messagesMixer); + } else { + emit messageReceived(channel, message, senderID, true); } } } void MessagesClient::sendData(QString channel, QByteArray data, bool localOnly) { auto nodeList = DependencyManager::get(); + QUuid senderID = nodeList->getSessionUUID(); if (localOnly) { - QUuid senderID = nodeList->getSessionUUID(); emit dataReceived(channel, data, senderID, true); } else { SharedNodePointer messagesMixer = nodeList->soloNodeOfType(NodeType::MessagesMixer); - if (messagesMixer) { QUuid senderID = nodeList->getSessionUUID(); auto packetList = encodeMessagesDataPacket(channel, data, senderID); nodeList->sendPacketList(std::move(packetList), *messagesMixer); + } else { + emit dataReceived(channel, data, senderID, true); } } } diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index aaf5ca7260..6da037a712 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -31,7 +31,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/dialTone.js", "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", - "system/emote.js" + "system/emote.js", + "system/miniTablet.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", diff --git a/scripts/system/assets/models/miniTabletBlank.fbx b/scripts/system/assets/models/miniTabletBlank.fbx new file mode 100644 index 0000000000..a2faa2a80a Binary files /dev/null and b/scripts/system/assets/models/miniTabletBlank.fbx differ diff --git a/scripts/system/assets/sounds/button-click.wav b/scripts/system/assets/sounds/button-click.wav new file mode 100644 index 0000000000..30a097ce45 Binary files /dev/null and b/scripts/system/assets/sounds/button-click.wav differ diff --git a/scripts/system/assets/sounds/button-hover.wav b/scripts/system/assets/sounds/button-hover.wav new file mode 100644 index 0000000000..cd76d0174c Binary files /dev/null and b/scripts/system/assets/sounds/button-hover.wav differ diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 7a916392b9..acc1b8a15d 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -25,7 +25,9 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); (function() { Script.include("/~/system/libraries/pointersUtils.js"); + var NEAR_MAX_RADIUS = 0.1; + var NEAR_TABLET_MAX_RADIUS = 0.05; var TARGET_UPDATE_HZ = 60; // 50hz good enough, but we're using update var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ; @@ -211,6 +213,24 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); if (controllerLocations[h].valid) { var nearbyOverlays = Overlays.findOverlays(controllerLocations[h].position, NEAR_MAX_RADIUS * sensorScaleFactor); + + // Tablet and mini-tablet must be within NEAR_TABLET_MAX_RADIUS in order to be grabbed. + // Mini tablet can only be grabbed the hand it's displayed on. + var tabletIndex = nearbyOverlays.indexOf(HMD.tabletID); + var miniTabletIndex = nearbyOverlays.indexOf(HMD.miniTabletID); + if (tabletIndex !== -1 || miniTabletIndex !== -1) { + var closebyOverlays = + Overlays.findOverlays(controllerLocations[h].position, NEAR_TABLET_MAX_RADIUS * sensorScaleFactor); + // Assumes that the tablet and mini-tablet are not displayed at the same time. + if (tabletIndex !== -1 && closebyOverlays.indexOf(HMD.tabletID) === -1) { + nearbyOverlays.splice(tabletIndex, 1); + } + if (miniTabletIndex !== -1 + && ((closebyOverlays.indexOf(HMD.miniTabletID) === -1) || h !== HMD.miniTabletHand)) { + nearbyOverlays.splice(miniTabletIndex, 1); + } + } + nearbyOverlays.sort(function (a, b) { var aPosition = Overlays.getProperty(a, "position"); var aDistance = Vec3.distance(aPosition, controllerLocations[h].position); @@ -218,6 +238,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); var bDistance = Vec3.distance(bPosition, controllerLocations[h].position); return aDistance - bDistance; }); + nearbyOverlayIDs.push(nearbyOverlays); } else { nearbyOverlayIDs.push([]); @@ -449,14 +470,14 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); filter: Picks.PICK_ENTITIES | Picks.PICK_OVERLAYS, enabled: true }); - this.handleHandMessage = function(channel, message, sender) { - var data; + this.handleHandMessage = function(channel, data, sender) { + var message; if (sender === MyAvatar.sessionUUID) { try { if (channel === 'Hifi-Hand-RayPick-Blacklist') { - data = JSON.parse(message); - var action = data.action; - var id = data.id; + message = JSON.parse(data); + var action = message.action; + var id = message.id; var index = _this.blacklist.indexOf(id); if (action === 'add' && index === -1) { @@ -471,9 +492,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } } } - } catch (e) { - print("WARNING: handControllerGrab.js -- error parsing Hifi-Hand-RayPick-Blacklist message: " + message); + print("WARNING: handControllerGrab.js -- error parsing message: " + data); } } }; diff --git a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js index 368d5c483b..763a0a0a27 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js @@ -163,7 +163,10 @@ Script.include("/~/system/libraries/utils.js"); var handPosition = controllerData.controllerLocations[this.hand].position; var distance = Vec3.distance(overlayPosition, handPosition); if (distance <= NEAR_GRAB_RADIUS * sensorScaleFactor) { - return overlays[i]; + if (overlays[i] !== HMD.miniTabletID || controllerData.secondaryValues[this.hand] === 0) { + // Don't grab mini tablet with grip. + return overlays[i]; + } } } return null; diff --git a/scripts/system/controllers/controllerModules/stylusInput.js b/scripts/system/controllers/controllerModules/stylusInput.js index a512fd89db..0ca6cd1b04 100644 --- a/scripts/system/controllers/controllerModules/stylusInput.js +++ b/scripts/system/controllers/controllerModules/stylusInput.js @@ -35,13 +35,6 @@ Script.include("/~/system/libraries/controllers.js"); }; } - function getEntityDistance(controllerPosition, entityProps) { - return { - id: entityProps.id, - distance: Vec3.distance(entityProps.position, controllerPosition) - }; - } - function StylusInput(hand) { this.hand = hand; @@ -123,6 +116,14 @@ Script.include("/~/system/libraries/controllers.js"); } } + // Add the mini tablet. + if (HMD.miniTabletScreenID && Overlays.getProperty(HMD.miniTabletScreenID, "visible")) { + stylusTarget = getOverlayDistance(controllerPosition, HMD.miniTabletScreenID); + if (stylusTarget) { + stylusTargets.push(stylusTarget); + } + } + var WEB_DISPLAY_STYLUS_DISTANCE = 0.5; var nearStylusTarget = isNearStylusTarget(stylusTargets, WEB_DISPLAY_STYLUS_DISTANCE * sensorScaleFactor); diff --git a/scripts/system/html/css/img/mt-expand-hover.svg b/scripts/system/html/css/img/mt-expand-hover.svg new file mode 100644 index 0000000000..a8e84c42ad --- /dev/null +++ b/scripts/system/html/css/img/mt-expand-hover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/system/html/css/img/mt-expand-normal.svg b/scripts/system/html/css/img/mt-expand-normal.svg new file mode 100644 index 0000000000..aac349ebda --- /dev/null +++ b/scripts/system/html/css/img/mt-expand-normal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/system/html/css/img/mt-goto-hover.svg b/scripts/system/html/css/img/mt-goto-hover.svg new file mode 100644 index 0000000000..4cad54331a --- /dev/null +++ b/scripts/system/html/css/img/mt-goto-hover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/system/html/css/img/mt-goto-normal.svg b/scripts/system/html/css/img/mt-goto-normal.svg new file mode 100644 index 0000000000..ead63329fb --- /dev/null +++ b/scripts/system/html/css/img/mt-goto-normal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/system/html/css/img/mt-mute-hover.svg b/scripts/system/html/css/img/mt-mute-hover.svg new file mode 100644 index 0000000000..9a18ccd933 --- /dev/null +++ b/scripts/system/html/css/img/mt-mute-hover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/system/html/css/img/mt-mute-normal.svg b/scripts/system/html/css/img/mt-mute-normal.svg new file mode 100644 index 0000000000..472f03f138 --- /dev/null +++ b/scripts/system/html/css/img/mt-mute-normal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/system/html/css/miniTablet.css b/scripts/system/html/css/miniTablet.css new file mode 100644 index 0000000000..7598332d28 --- /dev/null +++ b/scripts/system/html/css/miniTablet.css @@ -0,0 +1,92 @@ +/* +miniTablet.css + +Created by David Rowe on 20 Aug 2018. +Copyright 2018 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 +*/ + +* { + box-sizing: border-box; + padding: 0; + margin: 0; + user-select: none; +} + +html { + background-color: #404040; +} + +body { + height: 100%; +} + +section { + background-color: #404040; + position: relative; + padding: 16px 16px; +} + +.button { + width: 116px; + height: 84px; + margin-top: 16px; + text-align: center; +} + + .button:first-child { + margin-top: 0; + } + +img { + width: 40px; +} + +#mute { + padding-top: 19px; + background-size: 100% 100%; + background-image: url("./img/mt-mute-normal.svg"); +} + + #mute:hover { + background-image: url("./img/mt-mute-hover.svg"); + } + +#goto { + padding-top: 19px; + background-size: 100% 100%; + background-image: url("./img/mt-goto-normal.svg"); +} + + #goto:hover { + background-image: url("./img/mt-goto-hover.svg"); + } + + #goto:hover.unhover { + background-image: url("./img/mt-goto-normal.svg"); + } + +#expand { + position: absolute; + right: 1px; + bottom: -1px; + width: 50px; + height: 50px; + background-size: 100% 100%; + background-image: url("./img/mt-expand-normal.svg"); +} + + #expand:hover { + background-image: url("./img/mt-expand-hover.svg"); + } + + #expand:hover.unhover { + background-image: url("./img/mt-expand-normal.svg"); + } + + #expand img { + width:34px; + margin-top: 7px; + } diff --git a/scripts/system/html/img/expand.svg b/scripts/system/html/img/expand.svg new file mode 100644 index 0000000000..f57e624374 --- /dev/null +++ b/scripts/system/html/img/expand.svg @@ -0,0 +1,85 @@ + + + +image/svg+xml + + + + + + + + + \ No newline at end of file diff --git a/scripts/system/html/js/miniTablet.js b/scripts/system/html/js/miniTablet.js new file mode 100644 index 0000000000..c48201cef5 --- /dev/null +++ b/scripts/system/html/js/miniTablet.js @@ -0,0 +1,157 @@ +// +// miniTablet.js +// +// Created by David Rowe on 20 Aug 2018. +// Copyright 2018 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 EventBridge */ +/* eslint-env browser */ + +(function () { + + "use strict"; + + var // EventBridge + READY_MESSAGE = "ready", // Engine <== Dialog + HOVER_MESSAGE = "hover", // Engine <== Dialog + UNHOVER_MESSAGE = "unhover", // Engine <== Dialog + MUTE_MESSAGE = "mute", // Engine <=> Dialog + GOTO_MESSAGE = "goto", // Engine <=> Dialog + EXPAND_MESSAGE = "expand", // Engine <== Dialog + + muteButton, + muteImage, + gotoButton, + gotoImage, + expandButton, + + // Work around buttons staying hovered when mini tablet is replaced by tablet proper then subsequently redisplayed. + isUnhover = true; + + + function setUnhover() { + if (!isUnhover) { + gotoButton.classList.add("unhover"); + expandButton.classList.add("unhover"); + isUnhover = true; + } + } + + function clearUnhover() { + if (isUnhover) { + gotoButton.classList.remove("unhover"); + expandButton.classList.remove("unhover"); + isUnhover = false; + } + } + + + function onScriptEventReceived(data) { + var message; + + try { + message = JSON.parse(data); + } catch (e) { + console.error("EventBridge message error"); + return; + } + + switch (message.type) { + case MUTE_MESSAGE: + muteImage.src = message.icon; + break; + case GOTO_MESSAGE: + gotoImage.src = message.icon; + break; + } + } + + function onBodyHover() { + EventBridge.emitWebEvent(JSON.stringify({ + type: HOVER_MESSAGE, + target: "body" + })); + } + + function onBodyUnhover() { + EventBridge.emitWebEvent(JSON.stringify({ + type: UNHOVER_MESSAGE, + target: "body" + })); + } + + function onButtonHover() { + EventBridge.emitWebEvent(JSON.stringify({ + type: HOVER_MESSAGE, + target: "button" + })); + clearUnhover(); + } + + function onMuteButtonClick() { + EventBridge.emitWebEvent(JSON.stringify({ + type: MUTE_MESSAGE + })); + } + + function onGotoButtonClick() { + setUnhover(); + EventBridge.emitWebEvent(JSON.stringify({ + type: GOTO_MESSAGE + })); + } + + function onExpandButtonClick() { + setUnhover(); + EventBridge.emitWebEvent(JSON.stringify({ + type: EXPAND_MESSAGE + })); + } + + function connectEventBridge() { + EventBridge.scriptEventReceived.connect(onScriptEventReceived); + EventBridge.emitWebEvent(JSON.stringify({ + type: READY_MESSAGE + })); + } + + function disconnectEventBridge() { + EventBridge.scriptEventReceived.disconnect(onScriptEventReceived); + } + + + function onUnload() { + disconnectEventBridge(); + } + + function onLoad() { + muteButton = document.getElementById("mute"); + muteImage = document.getElementById("mute-img"); + gotoButton = document.getElementById("goto"); + gotoImage = document.getElementById("goto-img"); + expandButton = document.getElementById("expand"); + + connectEventBridge(); + + document.body.addEventListener("mouseenter", onBodyHover, false); + document.body.addEventListener("mouseleave", onBodyUnhover, false); + + muteButton.addEventListener("mouseenter", onButtonHover, false); + gotoButton.addEventListener("mouseenter", onButtonHover, false); + expandButton.addEventListener("mouseenter", onButtonHover, false); + muteButton.addEventListener("click", onMuteButtonClick, true); + gotoButton.addEventListener("click", onGotoButtonClick, true); + expandButton.addEventListener("click", onExpandButtonClick, true); + + document.body.onunload = function () { + onUnload(); + }; + } + + onLoad(); + +}()); diff --git a/scripts/system/html/miniTablet.html b/scripts/system/html/miniTablet.html new file mode 100644 index 0000000000..69703f6d2e --- /dev/null +++ b/scripts/system/html/miniTablet.html @@ -0,0 +1,32 @@ + + + + + + + + + + +
+
+ +
+
+ +
+
+ +
+
+ + + diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index f7b5f6db8d..f8928dd74a 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -356,13 +356,14 @@ getTabletWidthFromSettings = function () { var DEFAULT_TABLET_WIDTH = 0.4375; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var toolbarMode = tablet.toolbarMode; - var DEFAULT_TABLET_SCALE = 70; - var tabletScalePercentage = DEFAULT_TABLET_SCALE; + 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_TABLET_SCALE; + tabletScalePercentage = Settings.getValue("hmdTabletScale") || DEFAULT_HMD_TABLET_SCALE; } else { - tabletScalePercentage = Settings.getValue("desktopTabletScale") || DEFAULT_TABLET_SCALE; + tabletScalePercentage = Settings.getValue("desktopTabletScale") || DEFAULT_DESKTOP_TABLET_SCALE; } } return DEFAULT_TABLET_WIDTH * (tabletScalePercentage / 100); diff --git a/scripts/system/miniTablet.js b/scripts/system/miniTablet.js new file mode 100644 index 0000000000..fdc00c1ebf --- /dev/null +++ b/scripts/system/miniTablet.js @@ -0,0 +1,1036 @@ +// +// miniTablet.js +// +// Created by David Rowe on 9 Aug 2018. +// Copyright 2018 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 getTabletWidthFromSettings, TRIGGER_OFF_VALUE */ + +(function () { + + "use strict"; + + Script.include("./libraries/utils.js"); + Script.include("./libraries/controllerDispatcherUtils.js"); + + var UI, + ui = null, + State, + miniState = null, + + // Hands. + LEFT_HAND = 0, + RIGHT_HAND = 1, + HAND_NAMES = ["LeftHand", "RightHand"], + + // Miscellaneous. + HIFI_OBJECT_MANIPULATION_CHANNEL = "Hifi-Object-Manipulation", + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"), + DEBUG = false; + + + function debug(message) { + if (!DEBUG) { + return; + } + print("DEBUG: " + message); + } + + function error(message) { + print("ERROR: " + message); + } + + function handJointName(hand) { + var jointName; + if (hand === LEFT_HAND) { + if (Camera.mode === "first person") { + jointName = "_CONTROLLER_LEFTHAND"; + } else if (Camera.mode === "third person") { + jointName = "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"; + } else { + jointName = "LeftHand"; + } + } else { + if (Camera.mode === "first person") { + jointName = "_CONTROLLER_RIGHTHAND"; + } else if (Camera.mode === "third person") { + jointName = "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"; + } else { + jointName = "RightHand"; + } + } + return jointName; + } + + function handJointIndex(hand) { + return MyAvatar.getJointIndex(handJointName(hand)); + } + + function otherHand(hand) { + return hand === LEFT_HAND ? RIGHT_HAND : LEFT_HAND; + } + + + UI = function () { + + if (!(this instanceof UI)) { + return new UI(); + } + + var // Base overlay + miniOverlay = null, + MINI_MODEL = Script.resolvePath("./assets/models/miniTabletBlank.fbx"), + MINI_DIMENSIONS = { x: 0.0637, y: 0.0965, z: 0.0045 }, // Proportional to tablet proper. + MINI_POSITIONS = [ + { + x: -0.01, // Distance across hand. + y: 0.08, // Distance from joint. + z: 0.06 // Distance above palm. + }, + { + x: 0.01, // Distance across hand. + y: 0.08, // Distance from joint. + z: 0.06 // Distance above palm. + } + ], + DEGREES_180 = 180, + MINI_PICTH = 40, + MINI_ROTATIONS = [ + Quat.fromVec3Degrees({ x: 0, y: DEGREES_180 - MINI_PICTH, z: 90 }), + Quat.fromVec3Degrees({ x: 0, y: DEGREES_180 + MINI_PICTH, z: -90 }) + ], + + // UI overlay. + uiHand = LEFT_HAND, + miniUIOverlay = null, + MINI_UI_HTML = Script.resolvePath("./html/miniTablet.html"), + MINI_UI_DIMENSIONS = { x: 0.059, y: 0.0865 }, + MINI_UI_WIDTH_PIXELS = 150, + METERS_TO_INCHES = 39.3701, + MINI_UI_DPI = MINI_UI_WIDTH_PIXELS / (MINI_UI_DIMENSIONS.x * METERS_TO_INCHES), + MINI_UI_OFFSET = 0.001, // Above model surface. + MINI_UI_LOCAL_POSITION = { x: 0.0002, y: 0.0024, z: -(MINI_DIMENSIONS.z / 2 + MINI_UI_OFFSET) }, + MINI_UI_LOCAL_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }), + miniUIOverlayEnabled = false, + MINI_UI_OVERLAY_ENABLED_DELAY = 500, + miniOverlayObject = null, + + // Button icons. + MUTE_ON_ICON = Script.resourcesPath() + "icons/tablet-icons/mic-mute-a.svg", + MUTE_OFF_ICON = Script.resourcesPath() + "icons/tablet-icons/mic-unmute-i.svg", + GOTO_ICON = Script.resourcesPath() + "icons/tablet-icons/goto-i.svg", + + // Expansion to tablet. + MINI_EXPAND_HANDLES = [ // Normalized coordinates in range [-0.5, 0.5] about center of mini tablet. + { x: 0.5, y: -0.65, z: 0 }, + { x: -0.5, y: -0.65, z: 0 } + ], + MINI_EXPAND_DELTA_ROTATION = Quat.fromVec3Degrees({ x: -5, y: 0, z: 0 }), + MINI_EXPAND_HANDLES_OTHER = [ // Different handles when expanding after being grabbed by other hand, + { x: 0.5, y: -0.4, z: 0 }, + { x: -0.5, y: -0.4, z: 0 } + ], + MINI_EXPAND_DELTA_ROTATION_OTHER = Quat.IDENTITY, + miniExpandHand, + miniExpandHandles = MINI_EXPAND_HANDLES, + miniExpandDeltaRotation = MINI_EXPAND_HANDLES_OTHER, + miniExpandLocalPosition, + miniExpandLocalRotation = Quat.IDENTITY, + miniInitialWidth, + miniTargetWidth, + miniTargetLocalRotation, + + // Laser pointing at. + isBodyHovered = false, + + // EventBridge. + READY_MESSAGE = "ready", // Engine <== Dialog + HOVER_MESSAGE = "hover", // Engine <== Dialog + UNHOVER_MESSAGE = "unhover", // Engine <== Dialog + MUTE_MESSAGE = "mute", // Engine <=> Dialog + GOTO_MESSAGE = "goto", // Engine <=> Dialog + EXPAND_MESSAGE = "expand", // Engine <== Dialog + + // Sounds. + HOVER_SOUND = "./assets/sounds/button-hover.wav", + HOVER_VOLUME = 0.5, + CLICK_SOUND = "./assets/sounds/button-click.wav", + CLICK_VOLUME = 0.8, + hoverSound = SoundCache.getSound(Script.resolvePath(HOVER_SOUND)), + clickSound = SoundCache.getSound(Script.resolvePath(CLICK_SOUND)); + + + function updateMutedStatus() { + var isMuted = Audio.muted; + miniOverlayObject.emitScriptEvent(JSON.stringify({ + type: MUTE_MESSAGE, + on: isMuted, + icon: isMuted ? MUTE_ON_ICON : MUTE_OFF_ICON + })); + } + + function setGotoIcon() { + miniOverlayObject.emitScriptEvent(JSON.stringify({ + type: GOTO_MESSAGE, + icon: GOTO_ICON + })); + } + + function onWebEventReceived(data) { + var message; + + try { + message = JSON.parse(data); + } catch (e) { + console.error("EventBridge message error"); + return; + } + + switch (message.type) { + case READY_MESSAGE: + // Send initial button statuses. + updateMutedStatus(); + setGotoIcon(); + break; + case HOVER_MESSAGE: + if (message.target === "body") { + // Laser status. + isBodyHovered = true; + } else if (message.target === "button") { + // Audio feedback. + playSound(hoverSound, HOVER_VOLUME); + } + break; + case UNHOVER_MESSAGE: + if (message.target === "body") { + // Laser status. + isBodyHovered = false; + } + break; + case MUTE_MESSAGE: + // Toggle mute. + playSound(clickSound, CLICK_VOLUME); + Audio.muted = !Audio.muted; + break; + case GOTO_MESSAGE: + // Goto. + playSound(clickSound, CLICK_VOLUME); + miniState.setState(miniState.MINI_EXPANDING, { hand: uiHand, goto: true }); + break; + case EXPAND_MESSAGE: + // Expand tablet; + playSound(clickSound, CLICK_VOLUME); + miniState.setState(miniState.MINI_EXPANDING, { hand: uiHand, goto: false }); + break; + } + } + + + function updateMiniTabletID() { + HMD.miniTabletID = miniOverlay; + HMD.miniTabletScreenID = miniUIOverlay; + HMD.miniTabletHand = miniOverlay ? uiHand : -1; + } + + function playSound(sound, volume) { + Audio.playSound(sound, { + position: uiHand === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(), + volume: volume, + localOnly: true + }); + } + + + function getUIPositionAndRotation(hand) { + return { + position: MINI_POSITIONS[hand], + rotation: MINI_ROTATIONS[hand] + }; + } + + function getMiniTabletID() { + return miniOverlay; + } + + function getMiniTabletProperties() { + var properties = Overlays.getProperties(miniOverlay, ["position", "orientation"]); + return { + position: properties.position, + orientation: properties.orientation + }; + } + + function isLaserPointingAt() { + return isBodyHovered; + } + + + function show(hand) { + var initialScale = 0.01; // Start very small. + + uiHand = hand; + + Overlays.editOverlay(miniOverlay, { + parentID: MyAvatar.SELF_ID, + parentJointIndex: handJointIndex(hand), + localPosition: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_POSITIONS[hand]), + localRotation: MINI_ROTATIONS[hand], + dimensions: Vec3.multiply(initialScale, MINI_DIMENSIONS), + grabbable: true, + visible: true + }); + Overlays.editOverlay(miniUIOverlay, { + localPosition: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_UI_LOCAL_POSITION), + localRotation: MINI_UI_LOCAL_ROTATION, + dimensions: Vec3.multiply(initialScale, MINI_UI_DIMENSIONS), + dpi: MINI_UI_DPI / initialScale, + visible: true + }); + + updateMiniTabletID(); + + if (!miniUIOverlayEnabled) { + // Overlay content is created the first time it is visible to the user. The initial creation displays artefacts. + // Delay showing UI overlay until after giving it time for its content to be created. + Script.setTimeout(function () { + Overlays.editOverlay(miniUIOverlay, { alpha: 1.0 }); + }, MINI_UI_OVERLAY_ENABLED_DELAY); + } + } + + function size(scaleFactor) { + // Scale UI in place. + Overlays.editOverlay(miniOverlay, { + dimensions: Vec3.multiply(scaleFactor, MINI_DIMENSIONS) + }); + Overlays.editOverlay(miniUIOverlay, { + localPosition: Vec3.multiply(scaleFactor, MINI_UI_LOCAL_POSITION), + dimensions: Vec3.multiply(scaleFactor, MINI_UI_DIMENSIONS), + dpi: MINI_UI_DPI / scaleFactor + }); + updateRotation(); + } + + function startExpandingTablet(hand) { + // Expansion details. + if (hand === uiHand) { + miniExpandHandles = MINI_EXPAND_HANDLES; + miniExpandDeltaRotation = MINI_EXPAND_DELTA_ROTATION; + } else { + miniExpandHandles = MINI_EXPAND_HANDLES_OTHER; + miniExpandDeltaRotation = MINI_EXPAND_DELTA_ROTATION_OTHER; + } + + // Grab details. + var properties = Overlays.getProperties(miniOverlay, ["localPosition", "localRotation"]); + miniExpandHand = hand; + miniExpandLocalRotation = properties.localRotation; + miniExpandLocalPosition = Vec3.sum(properties.localPosition, + Vec3.multiplyQbyV(miniExpandLocalRotation, + Vec3.multiplyVbyV(miniExpandHandles[miniExpandHand], MINI_DIMENSIONS))); + + // Start expanding. + miniInitialWidth = MINI_DIMENSIONS.x; + miniTargetWidth = getTabletWidthFromSettings(); + miniTargetLocalRotation = Quat.multiply(miniExpandLocalRotation, miniExpandDeltaRotation); + } + + function sizeAboutHandles(scaleFactor) { + // Scale UI and move per handles. + var tabletScaleFactor, + dimensions, + localRotation, + localPosition; + + tabletScaleFactor = MyAvatar.sensorToWorldScale + * (1 + scaleFactor * (miniTargetWidth - miniInitialWidth) / miniInitialWidth); + dimensions = Vec3.multiply(tabletScaleFactor, MINI_DIMENSIONS); + localRotation = Quat.mix(miniExpandLocalRotation, miniTargetLocalRotation, scaleFactor); + localPosition = + Vec3.sum(miniExpandLocalPosition, + Vec3.multiplyQbyV(miniExpandLocalRotation, + Vec3.multiply(-tabletScaleFactor, + Vec3.multiplyVbyV(miniExpandHandles[miniExpandHand], MINI_DIMENSIONS))) + ); + localPosition = Vec3.sum(localPosition, + Vec3.multiplyQbyV(miniExpandLocalRotation, { x: 0, y: 0.5 * -dimensions.y, z: 0 })); + localPosition = Vec3.sum(localPosition, + Vec3.multiplyQbyV(localRotation, { x: 0, y: 0.5 * dimensions.y, z: 0 })); + Overlays.editOverlay(miniOverlay, { + localPosition: localPosition, + localRotation: localRotation, + dimensions: dimensions + }); + // FIXME: Temporary code change to try not displaying UI when mini tablet is expanding to become the tablet proper. + Overlays.editOverlay(miniUIOverlay, { + /* + localPosition: Vec3.multiply(tabletScaleFactor, MINI_UI_LOCAL_POSITION), + dimensions: Vec3.multiply(tabletScaleFactor, MINI_UI_DIMENSIONS), + dpi: MINI_UI_DPI / tabletScaleFactor + */ + visible: false + }); + } + + function updateRotation() { + // Update the rotation of the tablet about its face normal so that its base is horizontal. + var COS_5_DEGREES = 0.996, + RADIANS_TO_DEGREES = DEGREES_180 / Math.PI, + defaultLocalRotation, + handOrientation, + defaultOrientation, + faceNormal, + desiredOrientation, + defaultYAxis, + desiredYAxis, + cross, + dot, + deltaAngle, + deltaRotation, + localRotation; + + if (Overlays.getProperty(miniOverlay, "parentJointIndex") !== handJointIndex(uiHand)) { + // Overlay has been grabbed by other hand but this script hasn't received notification yet. + return; + } + + defaultLocalRotation = MINI_ROTATIONS[uiHand]; + handOrientation = + Quat.multiply(MyAvatar.orientation, MyAvatar.getAbsoluteJointRotationInObjectFrame(handJointIndex(uiHand))); + defaultOrientation = Quat.multiply(handOrientation, defaultLocalRotation); + faceNormal = Vec3.multiplyQbyV(defaultOrientation, Vec3.UNIT_Z); + + if (Math.abs(Vec3.dot(faceNormal, Vec3.UNIT_Y)) > COS_5_DEGREES) { + // Don't rotate mini tablet if almost flat in the x-z plane. + return; + } else { + // Rotate the tablet so that its base is parallel with the x-z plane. + desiredOrientation = Quat.lookAt(Vec3.ZERO, Vec3.multiplyQbyV(defaultOrientation, Vec3.UNIT_Z), Vec3.UNIT_Y); + defaultYAxis = Vec3.multiplyQbyV(defaultOrientation, Vec3.UNIT_Y); + desiredYAxis = Vec3.multiplyQbyV(desiredOrientation, Vec3.UNIT_Y); + cross = Vec3.cross(defaultYAxis, desiredYAxis); + dot = Vec3.dot(defaultYAxis, desiredYAxis); + deltaAngle = Math.atan2(Vec3.length(cross), dot) * RADIANS_TO_DEGREES; + if (Vec3.dot(cross, Vec3.multiplyQbyV(desiredOrientation, Vec3.UNIT_Z)) > 0) { + deltaAngle = -deltaAngle; + } + deltaRotation = Quat.angleAxis(deltaAngle, Vec3.multiplyQbyV(defaultLocalRotation, Vec3.UNIT_Z)); + localRotation = Quat.multiply(deltaRotation, defaultLocalRotation); + } + Overlays.editOverlay(miniOverlay, { + localRotation: localRotation + }); + } + + function release() { + Overlays.editOverlay(miniOverlay, { + parentID: Uuid.NULL, // Release hold so that hand can grab tablet proper. + grabbable: false + }); + } + + function hide() { + Overlays.editOverlay(miniOverlay, { + visible: false + }); + Overlays.editOverlay(miniUIOverlay, { + visible: false + }); + } + + function create() { + miniOverlay = Overlays.addOverlay("model", { + url: MINI_MODEL, + dimensions: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_DIMENSIONS), + solid: true, + grabbable: true, + showKeyboardFocusHighlight: false, + displayInFront: true, + visible: false + }); + miniUIOverlay = Overlays.addOverlay("web3d", { + url: MINI_UI_HTML, + parentID: miniOverlay, + localPosition: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_UI_LOCAL_POSITION), + localRotation: MINI_UI_LOCAL_ROTATION, + dimensions: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_UI_DIMENSIONS), + dpi: MINI_UI_DPI / MyAvatar.sensorToWorldScale, + alpha: 0, // Hide overlay while its content is being created. + grabbable: false, + showKeyboardFocusHighlight: false, + displayInFront: true, + visible: false + }); + + miniUIOverlayEnabled = false; // This and alpha = 0 hides overlay while its content is being created. + + miniOverlayObject = Overlays.getOverlayObject(miniUIOverlay); + miniOverlayObject.webEventReceived.connect(onWebEventReceived); + } + + function destroy() { + if (miniOverlayObject) { + miniOverlayObject.webEventReceived.disconnect(onWebEventReceived); + Overlays.deleteOverlay(miniUIOverlay); + Overlays.deleteOverlay(miniOverlay); + miniOverlayObject = null; + miniUIOverlay = null; + miniOverlay = null; + updateMiniTabletID(); + } + } + + create(); + + return { + getUIPositionAndRotation: getUIPositionAndRotation, + getMiniTabletID: getMiniTabletID, + getMiniTabletProperties: getMiniTabletProperties, + isLaserPointingAt: isLaserPointingAt, + updateMutedStatus: updateMutedStatus, + show: show, + size: size, + startExpandingTablet: startExpandingTablet, + sizeAboutHandles: sizeAboutHandles, + updateRotation: updateRotation, + release: release, + hide: hide, + destroy: destroy + }; + + }; + + + State = function () { + + if (!(this instanceof State)) { + return new State(); + } + + var + // States. + MINI_DISABLED = 0, + MINI_HIDDEN = 1, + MINI_HIDING = 2, + MINI_SHOWING = 3, + MINI_VISIBLE = 4, + MINI_EXPANDING = 5, + TABLET_OPEN = 6, + STATE_STRINGS = ["MINI_DISABLED", "MINI_HIDDEN", "MINI_HIDING", "MINI_SHOWING", "MINI_VISIBLE", "MINI_EXPANDING", + "TABLET_OPEN"], + STATE_MACHINE, + miniState = MINI_DISABLED, + miniHand, + + // Mini tablet scaling. + MINI_SCALE_DURATION = 250, + MINI_SCALE_TIMEOUT = 20, + miniScaleTimer = null, + miniScaleStart, + + // Expansion to tablet. + MINI_EXPAND_DURATION = 250, + MINI_EXPAND_TIMEOUT = 20, + miniExpandTimer = null, + miniExpandStart, + miniExpandedStartTime, + + // Tablet targets. + isGoto = false, + TABLET_ADDRESS_DIALOG = "hifi/tablet/TabletAddressDialog.qml", + + // Trigger values. + leftTriggerOn = 0, + rightTriggerOn = 0, + MAX_TRIGGER_ON_TIME = 100, + + // Visibility. + MAX_HAND_CAMERA_ANGLE = 30, + MAX_CAMERA_HAND_ANGLE = 30, + DEGREES_180 = 180, + MAX_HAND_CAMERA_ANGLE_COS = Math.cos(Math.PI * MAX_HAND_CAMERA_ANGLE / DEGREES_180), + MAX_CAMERA_HAND_ANGLE_COS = Math.cos(Math.PI * MAX_CAMERA_HAND_ANGLE / DEGREES_180), + HIDING_DELAY = 1000, // ms + lastVisible = [0, 0]; + + + function enterMiniDisabled() { + // Stop updates. + Script.update.disconnect(updateState); + + // Stop monitoring mute changes. + Audio.mutedChanged.disconnect(ui.updateMutedStatus); + + // Don't keep overlays prepared if in desktop mode. + ui.destroy(); + ui = null; + } + + function exitMiniDisabled() { + // Create UI so that it's ready to be displayed without seeing artefacts from creating the UI. + ui = new UI(); + + // Start monitoring mute changes. + Audio.mutedChanged.connect(ui.updateMutedStatus); + + // Start updates. + Script.update.connect(updateState); + } + + function shouldShowMini(hand) { + // Should show mini tablet if it would be oriented toward the camera. + var show, + pose, + isLeftTriggerOff, + isRightTriggerOff, + wasLeftTriggerOff = true, + wasRightTriggerOff = true, + jointIndex, + handPosition, + handOrientation, + uiPositionAndOrientation, + miniPosition, + miniOrientation, + miniToCameraDirection, + cameraToHand; + + // Shouldn't show mini tablet if hand isn't being controlled. + pose = Controller.getPoseValue(hand === LEFT_HAND ? Controller.Standard.LeftHand : Controller.Standard.RightHand); + show = pose.valid; + + // Shouldn't show mini tablet on hand if that hand's trigger or grip are pressed (i.e., laser is searching or hand + // is grabbing something) or the other hand's trigger is pressed unless it is pointing at the mini tablet. Allow + // the triggers to be pressed briefly to allow for the grabbing process. + if (show) { + isLeftTriggerOff = Controller.getValue(Controller.Standard.LT) < TRIGGER_OFF_VALUE + && Controller.getValue(Controller.Standard.LeftGrip) < TRIGGER_OFF_VALUE; + if (!isLeftTriggerOff) { + if (leftTriggerOn === 0) { + leftTriggerOn = Date.now(); + } else { + wasLeftTriggerOff = Date.now() - leftTriggerOn < MAX_TRIGGER_ON_TIME; + } + } else { + leftTriggerOn = 0; + } + isRightTriggerOff = Controller.getValue(Controller.Standard.RT) < TRIGGER_OFF_VALUE + && Controller.getValue(Controller.Standard.RightGrip) < TRIGGER_OFF_VALUE; + if (!isRightTriggerOff) { + if (rightTriggerOn === 0) { + rightTriggerOn = Date.now(); + } else { + wasRightTriggerOff = Date.now() - rightTriggerOn < MAX_TRIGGER_ON_TIME; + } + } else { + rightTriggerOn = 0; + } + + show = (hand === LEFT_HAND ? wasLeftTriggerOff : wasRightTriggerOff) + && ((hand === LEFT_HAND ? wasRightTriggerOff : wasLeftTriggerOff) || ui.isLaserPointingAt()); + } + + // Should show mini tablet if it would be oriented toward the camera. + if (show) { + // Calculate current visibility of mini tablet. + jointIndex = handJointIndex(hand); + handPosition = Vec3.sum(MyAvatar.position, + Vec3.multiplyQbyV(MyAvatar.orientation, MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex))); + handOrientation = + Quat.multiply(MyAvatar.orientation, MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex)); + uiPositionAndOrientation = ui.getUIPositionAndRotation(hand); + miniPosition = Vec3.sum(handPosition, Vec3.multiply(MyAvatar.sensorToWorldScale, + Vec3.multiplyQbyV(handOrientation, uiPositionAndOrientation.position))); + miniOrientation = Quat.multiply(handOrientation, uiPositionAndOrientation.rotation); + miniToCameraDirection = Vec3.normalize(Vec3.subtract(Camera.position, miniPosition)); + show = Vec3.dot(miniToCameraDirection, Quat.getForward(miniOrientation)) > MAX_HAND_CAMERA_ANGLE_COS; + show = show || (-Vec3.dot(miniToCameraDirection, Quat.getForward(handOrientation)) > MAX_HAND_CAMERA_ANGLE_COS); + cameraToHand = -Vec3.dot(miniToCameraDirection, Quat.getForward(Camera.orientation)); + show = show && (cameraToHand > MAX_CAMERA_HAND_ANGLE_COS); + + // Persist showing for a while after it would otherwise be hidden. + if (show) { + lastVisible[hand] = Date.now(); + } else { + show = Date.now() - lastVisible[hand] <= HIDING_DELAY; + } + } + + return { + show: show, + cameraToHand: cameraToHand + }; + } + + function enterMiniHidden() { + ui.release(); + ui.hide(); + } + + function updateMiniHidden() { + var showLeft, + showRight; + + // Don't show mini tablet if tablet proper is already displayed, in toolbar mode, or away. + if (HMD.showTablet || tablet.toolbarMode || MyAvatar.isAway) { + return; + } + + // Show mini tablet if it would be pointing at the camera. + showLeft = shouldShowMini(LEFT_HAND); + showRight = shouldShowMini(RIGHT_HAND); + if (showLeft.show && showRight.show) { + // Both hands would be pointing at camera; show the one the camera is gazing at. + if (showLeft.cameraToHand > showRight.cameraToHand) { + setState(MINI_SHOWING, LEFT_HAND); + } else { + setState(MINI_SHOWING, RIGHT_HAND); + } + } else if (showLeft.show) { + setState(MINI_SHOWING, LEFT_HAND); + } else if (showRight.show) { + setState(MINI_SHOWING, RIGHT_HAND); + } + } + + function scaleMiniDown() { + var scaleFactor = (Date.now() - miniScaleStart) / MINI_SCALE_DURATION; + if (scaleFactor < 1) { + ui.size((1 - scaleFactor) * MyAvatar.sensorToWorldScale); + miniScaleTimer = Script.setTimeout(scaleMiniDown, MINI_SCALE_TIMEOUT); + return; + } + miniScaleTimer = null; + setState(MINI_HIDDEN); + } + + function enterMiniHiding() { + miniScaleStart = Date.now(); + miniScaleTimer = Script.setTimeout(scaleMiniDown, MINI_SCALE_TIMEOUT); + } + + function updateMiniHiding() { + if (HMD.showTablet) { + setState(MINI_HIDDEN); + } + } + + function exitMiniHiding() { + if (miniScaleTimer) { + Script.clearTimeout(miniScaleTimer); + miniScaleTimer = null; + } + } + + function scaleMiniUp() { + var scaleFactor = (Date.now() - miniScaleStart) / MINI_SCALE_DURATION; + if (scaleFactor < 1) { + ui.size(scaleFactor * MyAvatar.sensorToWorldScale); + miniScaleTimer = Script.setTimeout(scaleMiniUp, MINI_SCALE_TIMEOUT); + return; + } + miniScaleTimer = null; + ui.size(MyAvatar.sensorToWorldScale); + setState(MINI_VISIBLE); + } + + function enterMiniShowing(hand) { + miniHand = hand; + ui.show(miniHand); + miniScaleStart = Date.now(); + miniScaleTimer = Script.setTimeout(scaleMiniUp, MINI_SCALE_TIMEOUT); + } + + function updateMiniShowing() { + if (HMD.showTablet) { + setState(MINI_HIDDEN); + } + } + + function exitMiniShowing() { + if (miniScaleTimer) { + Script.clearTimeout(miniScaleTimer); + miniScaleTimer = null; + } + } + + function updateMiniVisible() { + var showLeft, + showRight; + + // Hide mini tablet if tablet proper has been displayed by other means. + if (HMD.showTablet) { + setState(MINI_HIDDEN); + return; + } + + // Check that the mini tablet should still be visible and if so then ensure it's on the hand that the camera is + // gazing at. + showLeft = shouldShowMini(LEFT_HAND); + showRight = shouldShowMini(RIGHT_HAND); + if (showLeft.show && showRight.show) { + if (showLeft.cameraToHand > showRight.cameraToHand) { + if (miniHand !== LEFT_HAND) { + setState(MINI_HIDING); + } + } else { + if (miniHand !== RIGHT_HAND) { + setState(MINI_HIDING); + } + } + } else if (showLeft.show) { + if (miniHand !== LEFT_HAND) { + setState(MINI_HIDING); + } + } else if (showRight.show) { + if (miniHand !== RIGHT_HAND) { + setState(MINI_HIDING); + } + } else { + setState(MINI_HIDING); + } + + // If state hasn't changed, update mini tablet rotation. + if (miniState === MINI_VISIBLE) { + ui.updateRotation(); + } + } + + function expandMini() { + var scaleFactor = (Date.now() - miniExpandStart) / MINI_EXPAND_DURATION; + if (scaleFactor < 1) { + ui.sizeAboutHandles(scaleFactor); + miniExpandTimer = Script.setTimeout(expandMini, MINI_EXPAND_TIMEOUT); + return; + } + miniExpandTimer = null; + setState(TABLET_OPEN); + } + + function enterMiniExpanding(data) { + isGoto = data.goto; + ui.startExpandingTablet(data.hand); + miniExpandStart = Date.now(); + miniExpandTimer = Script.setTimeout(expandMini, MINI_EXPAND_TIMEOUT); + } + + function updateMiniExanding() { + // Hide mini tablet immediately if tablet proper has been displayed by other means. + if (HMD.showTablet) { + setState(MINI_HIDDEN); + } + } + + function exitMiniExpanding() { + if (miniExpandTimer !== null) { + Script.clearTimeout(miniExpandTimer); + miniExpandTimer = null; + } + } + + function enterTabletOpen() { + var miniTabletProperties; + + if (isGoto) { + tablet.loadQMLSource(TABLET_ADDRESS_DIALOG); + } else { + tablet.gotoHomeScreen(); + } + + ui.release(); + miniTabletProperties = ui.getMiniTabletProperties(); + + Overlays.editOverlay(HMD.tabletID, { + position: miniTabletProperties.position, + orientation: miniTabletProperties.orientation + }); + + HMD.openTablet(true); + + miniExpandedStartTime = Date.now(); + } + + function updateTabletOpen() { + // Give the tablet proper time to rez before hiding expanded mini tablet. + // The mini tablet is also hidden elsewhere if the tablet proper is grabbed. + var TABLET_OPENING_DELAY = 500; + if (Date.now() >= miniExpandedStartTime + TABLET_OPENING_DELAY) { + setState(MINI_HIDDEN); + } + } + + STATE_MACHINE = { + MINI_DISABLED: { // Mini tablet cannot be shown because in desktop mode. + enter: enterMiniDisabled, + update: null, + exit: exitMiniDisabled + }, + MINI_HIDDEN: { // Mini tablet could be shown but isn't because hand is oriented to show it or aren't in HMD mode. + enter: enterMiniHidden, + update: updateMiniHidden, + exit: null + }, + MINI_HIDING: { // Mini tablet is reducing from MINI_VISIBLE to MINI_HIDDEN. + enter: enterMiniHiding, + update: updateMiniHiding, + exit: exitMiniHiding + }, + MINI_SHOWING: { // Mini tablet is expanding from MINI_HIDDN to MINI_VISIBLE. + enter: enterMiniShowing, + update: updateMiniShowing, + exit: exitMiniShowing + }, + MINI_VISIBLE: { // Mini tablet is visible and attached to hand. + enter: null, + update: updateMiniVisible, + exit: null + }, + MINI_EXPANDING: { // Mini tablet is expanding before showing tablet proper. + enter: enterMiniExpanding, + update: updateMiniExanding, + exit: exitMiniExpanding + }, + TABLET_OPEN: { // Tablet proper is being displayed. + enter: enterTabletOpen, + update: updateTabletOpen, + exit: null + } + }; + + function setState(state, data) { + if (state !== miniState) { + debug("State transition from " + STATE_STRINGS[miniState] + " to " + STATE_STRINGS[state] + + ( data ? " " + JSON.stringify(data) : "")); + if (STATE_MACHINE[STATE_STRINGS[miniState]].exit) { + STATE_MACHINE[STATE_STRINGS[miniState]].exit(data); + } + if (STATE_MACHINE[STATE_STRINGS[state]].enter) { + STATE_MACHINE[STATE_STRINGS[state]].enter(data); + } + miniState = state; + } else { + error("Null state transition: " + state + "!"); + } + } + + function getState() { + return miniState; + } + + function getHand() { + return miniHand; + } + + function updateState() { + if (STATE_MACHINE[STATE_STRINGS[miniState]].update) { + STATE_MACHINE[STATE_STRINGS[miniState]].update(); + } + } + + function create() { + // Nothing to do. + } + + function destroy() { + if (miniState !== MINI_DISABLED) { + setState(MINI_DISABLED); + } + } + + create(); + + return { + MINI_DISABLED: MINI_DISABLED, + MINI_HIDDEN: MINI_HIDDEN, + MINI_HIDING: MINI_HIDING, + MINI_SHOWING: MINI_SHOWING, + MINI_VISIBLE: MINI_VISIBLE, + MINI_EXPANDING: MINI_EXPANDING, + TABLET_OPEN: TABLET_OPEN, + setState: setState, + getState: getState, + getHand: getHand, + destroy: destroy + }; + }; + + + function onMessageReceived(channel, data, senderID, localOnly) { + var message, + miniHand, + hand; + + if (channel !== HIFI_OBJECT_MANIPULATION_CHANNEL) { + return; + } + + try { + message = JSON.parse(data); + } catch (e) { + return; + } + + if (message.grabbedEntity !== HMD.tabletID && message.grabbedEntity !== ui.getMiniTabletID()) { + return; + } + + if (message.action === "grab" && message.grabbedEntity === HMD.tabletID && HMD.active) { + // Tablet may have been grabbed after it replaced expanded mini tablet. + miniState.setState(miniState.MINI_HIDDEN); + } else if (message.action === "grab" && miniState.getState() === miniState.MINI_VISIBLE) { + miniHand = miniState.getHand(); + hand = message.joint === HAND_NAMES[miniHand] ? miniHand : otherHand(miniHand); + miniState.setState(miniState.MINI_EXPANDING, { hand: hand, goto: false }); + } + } + + function onWentAway() { + // Mini tablet only available when user is not away. + if (HMD.active) { + miniState.setState(miniState.MINI_HIDDEN); + } + } + + function onDisplayModeChanged() { + // Mini tablet only available when HMD is active. + if (HMD.active) { + miniState.setState(miniState.MINI_HIDDEN); + } else { + miniState.setState(miniState.MINI_DISABLED); + } + } + + + function setUp() { + miniState = new State(); + + Messages.subscribe(HIFI_OBJECT_MANIPULATION_CHANNEL); + Messages.messageReceived.connect(onMessageReceived); + + MyAvatar.wentAway.connect(onWentAway); + HMD.displayModeChanged.connect(onDisplayModeChanged); + if (HMD.active) { + miniState.setState(miniState.MINI_HIDDEN); + } + } + + function tearDown() { + miniState.setState(miniState.MINI_DISABLED); + + HMD.displayModeChanged.disconnect(onDisplayModeChanged); + MyAvatar.wentAway.disconnect(onWentAway); + + Messages.messageReceived.disconnect(onMessageReceived); + Messages.unsubscribe(HIFI_OBJECT_MANIPULATION_CHANNEL); + + miniState.destroy(); + miniState = null; + } + + setUp(); + Script.scriptEnding.connect(tearDown); + +}()); diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index 80ddbeca8b..9a22014895 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -19,11 +19,12 @@ var tabletRezzed = false; var activeHand = null; var DEFAULT_WIDTH = 0.4375; - var DEFAULT_TABLET_SCALE = 70; + var DEFAULT_DESKTOP_TABLET_SCALE = 75; + var DEFAULT_HMD_TABLET_SCALE = 60; var preMakeTime = Date.now(); var validCheckTime = Date.now(); var debugTablet = false; - var tabletScalePercentage = 70.0; + var tabletScalePercentage = DEFAULT_HMD_TABLET_SCALE; var UIWebTablet = null; var MSECS_PER_SEC = 1000.0; var MUTE_MICROPHONE_MENU_ITEM = "Mute Microphone"; @@ -66,14 +67,14 @@ } function getTabletScalePercentageFromSettings() { - checkTablet() + checkTablet(); var toolbarMode = gTablet.toolbarMode; - var tabletScalePercentage = DEFAULT_TABLET_SCALE; + var tabletScalePercentage = DEFAULT_HMD_TABLET_SCALE; if (!toolbarMode) { if (HMD.active) { - tabletScalePercentage = Settings.getValue("hmdTabletScale") || DEFAULT_TABLET_SCALE; + tabletScalePercentage = Settings.getValue("hmdTabletScale") || DEFAULT_HMD_TABLET_SCALE; } else { - tabletScalePercentage = Settings.getValue("desktopTabletScale") || DEFAULT_TABLET_SCALE; + tabletScalePercentage = Settings.getValue("desktopTabletScale") || DEFAULT_DESKTOP_TABLET_SCALE; } } return tabletScalePercentage;