diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index 3dc3069ef5..09aefffdfe 100644 Binary files a/interface/resources/fonts/hifi-glyphs.ttf and b/interface/resources/fonts/hifi-glyphs.ttf differ diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index a33fbabdf2..e20ecd70e1 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -76,7 +76,7 @@ OriginalDesktop.Desktop { WebEngine.settings.localContentCanAccessRemoteUrls = true; [ // Allocate the standard buttons in the correct order. They will get images, etc., via scripts. - "hmdToggle", "mute", "mod", "bubble", "help", + "hmdToggle", "mute", "pal", "bubble", "help", "hudToggle", "com.highfidelity.interface.system.editButton", "marketplace", "snapshot", "goto" ].forEach(function (name) { diff --git a/interface/resources/qml/hifi/LetterboxMessage.qml b/interface/resources/qml/hifi/LetterboxMessage.qml new file mode 100644 index 0000000000..290cff6634 --- /dev/null +++ b/interface/resources/qml/hifi/LetterboxMessage.qml @@ -0,0 +1,52 @@ +// +// LetterboxMessage.qml +// qml/hifi +// +// Created by Zach Fox and Howard Stearns on 1/5/2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../styles-uit" + +Item { + property alias text: popupText.text + property real radius: hifi.dimensions.borderRadius + visible: false + id: letterbox + anchors.fill: parent + Rectangle { + anchors.fill: parent + color: "black" + opacity: 0.5 + radius: radius + } + Rectangle { + width: Math.max(parent.width * 0.75, 400) + height: popupText.contentHeight*1.5 + anchors.centerIn: parent + radius: radius + color: "white" + FiraSansSemiBold { + id: popupText + size: hifi.fontSizes.textFieldInput + color: hifi.colors.darkGray + horizontalAlignment: Text.AlignHCenter + anchors.fill: parent + anchors.leftMargin: 15 + anchors.rightMargin: 15 + wrapMode: Text.WordWrap + } + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: { + letterbox.visible = false + } + } +} diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 8a583bfe4c..7ad322bfa4 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -11,21 +11,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* TODO: - - prototype: - - only show kick/mute when canKick - - margins everywhere - - column head centering - - column head font - - proper button .svg on toolbar - - mvp: - - Show all participants, including ignored, and populate initial ignore/mute status. - - If name is elided, hover should scroll name left so the full name can be read. - - */ - import QtQuick 2.5 import QtQuick.Controls 1.4 import "../styles-uit" @@ -326,6 +311,11 @@ Item { visible: iAmAdmin color: hifi.colors.lightGrayText } + function letterbox(message) { + letterboxMessage.text = message; + letterboxMessage.visible = true + + } // This Rectangle refers to the [?] popup button next to "NAMES" Rectangle { color: hifi.colors.tableBackgroundLight @@ -349,7 +339,9 @@ Item { anchors.fill: parent acceptedButtons: Qt.LeftButton hoverEnabled: true - onClicked: namesPopup.visible = true + onClicked: letterbox("Bold names in the list are Avatar Display Names.\n" + + "If a Display Name isn't set, a unique Session Display Name is assigned." + + "\n\nAdministrators of this domain can also see the Username or Machine ID associated with each avatar present.") onEntered: helpText.color = hifi.colors.baseGrayHighlight onExited: helpText.color = hifi.colors.darkGray } @@ -378,87 +370,14 @@ Item { anchors.fill: parent acceptedButtons: Qt.LeftButton hoverEnabled: true - onClicked: adminPopup.visible = true + onClicked: letterbox('Silencing a user mutes their microphone. Silenced users can unmute themselves by clicking the "UNMUTE" button on their HUD.\n\n' + + "Banning a user will remove them from this domain and prevent them from returning. You can un-ban users from your domain's settings page.)") onEntered: adminHelpText.color = "#94132e" onExited: adminHelpText.color = hifi.colors.redHighlight } } - // Explanitory popup upon clicking "[?]" next to "NAMES" - Item { - visible: false - id: namesPopup - anchors.fill: pal - Rectangle { - anchors.fill: parent - color: "black" - opacity: 0.5 - radius: hifi.dimensions.borderRadius - } - Rectangle { - width: Math.max(parent.width * 0.75, 400) - height: popupText.contentHeight*1.5 - anchors.centerIn: parent - radius: hifi.dimensions.borderRadius - color: "white" - FiraSansSemiBold { - id: popupText - text: "Bold names in the list are Avatar Display Names.\n" + - "If a Display Name isn't set, a unique Session Display Name is assigned." + - "\n\nAdministrators of this domain can also see the Username or Machine ID associated with each avatar present." - size: hifi.fontSizes.textFieldInput - color: hifi.colors.darkGray - horizontalAlignment: Text.AlignHCenter - anchors.fill: parent - anchors.leftMargin: 15 - anchors.rightMargin: 15 - wrapMode: Text.WordWrap - } - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - onClicked: { - namesPopup.visible = false - } - } - } - // Explanitory popup upon clicking "[?]" next to "ADMIN" - Item { - visible: false - id: adminPopup - anchors.fill: pal - Rectangle { - anchors.fill: parent - color: "black" - opacity: 0.5 - radius: hifi.dimensions.borderRadius - } - Rectangle { - width: Math.max(parent.width * 0.75, 400) - height: adminPopupText.contentHeight*1.5 - anchors.centerIn: parent - radius: hifi.dimensions.borderRadius - color: "white" - FiraSansSemiBold { - id: adminPopupText - text: 'Silencing a user mutes their microphone. Silenced users can unmute themselves by clicking the "UNMUTE" button on their HUD.\n\n' + - "Banning a user will remove them from this domain and prevent them from returning. You can un-ban users from your domain's settings page." - size: hifi.fontSizes.textFieldInput - color: hifi.colors.darkGray - horizontalAlignment: Text.AlignHCenter - anchors.fill: parent - anchors.leftMargin: 15 - anchors.rightMargin: 15 - wrapMode: Text.WordWrap - } - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - onClicked: { - adminPopup.visible = false - } - } + LetterboxMessage { + id: letterboxMessage } function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml @@ -495,14 +414,20 @@ Item { sortModel(); break; case 'select': - var sessionId = message.params[0]; + var sessionIds = message.params[0]; var selected = message.params[1]; - var userIndex = findSessionIndex(sessionId); - if (selected) { - table.selection.clear(); // for now, no multi-select - table.selection.select(userIndex); + var userIndex = findSessionIndex(sessionIds[0]); + if (sessionIds.length > 1) { + letterbox('Only one user can be selected at a time.'); + } else if (userIndex < 0) { + letterbox('The last editor is not among this list of users.'); } else { - table.selection.deselect(userIndex); + if (selected) { + table.selection.clear(); // for now, no multi-select + table.selection.select(userIndex); + } else { + table.selection.deselect(userIndex); + } } break; // Received an "updateUsername()" request from the JS diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 212c06d410..b2f8f3c037 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6159,7 +6159,7 @@ void Application::loadScriptURLDialog() const { void Application::toggleLogDialog() { if (! _logDialog) { - _logDialog = new LogDialog(_glWidget, getLogger()); + _logDialog = new LogDialog(nullptr, getLogger()); } if (_logDialog->isVisible()) { diff --git a/interface/src/ui/LogDialog.cpp b/interface/src/ui/LogDialog.cpp index 3d88751d54..1e56d0cfd9 100644 --- a/interface/src/ui/LogDialog.cpp +++ b/interface/src/ui/LogDialog.cpp @@ -44,7 +44,7 @@ const QString HIGHLIGHT_COLOR = "#3366CC"; int qTextCursorMeta = qRegisterMetaType("QTextCursor"); int qTextBlockMeta = qRegisterMetaType("QTextBlock"); -LogDialog::LogDialog(QWidget* parent, AbstractLoggerInterface* logger) : QDialog(parent, Qt::Dialog) { +LogDialog::LogDialog(QWidget* parent, AbstractLoggerInterface* logger) : QDialog(parent, Qt::Window) { _logger = logger; setWindowTitle("Log"); diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 0f303561ca..aeefc777df 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -20,7 +20,7 @@ var DEFAULT_SCRIPTS = [ "system/hmd.js", "system/marketplaces/marketplace.js", "system/edit.js", - "system/mod.js", + "system/pal.js", //"system/mod.js", // older UX, if you prefer "system/selectAudioDevice.js", "system/notifications.js", "system/controllers/controllerDisplayManager.js", diff --git a/scripts/system/assets/models/Avatar-Overlay-v1.fbx b/scripts/system/assets/models/Avatar-Overlay-v1.fbx new file mode 100644 index 0000000000..db710702f2 Binary files /dev/null and b/scripts/system/assets/models/Avatar-Overlay-v1.fbx differ diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 507b93f834..62c0f62212 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -20,6 +20,9 @@ // When partially squeezing over a HUD element, a laser or the reticle is shown where the active hand // controller beam intersects the HUD. +var systemLaserOn = false; + + Script.include("../libraries/controllers.js"); // UTILITIES ------------- @@ -121,6 +124,12 @@ function ignoreMouseActivity() { if (!Reticle.allowMouseCapture) { return true; } + + // if the lasers are on, then reticle/mouse should be hidden and we can ignore it for seeking or depth updating + if (systemLaserOn) { + return true; + } + var pos = Reticle.position; if (!pos || (pos.x == -1 && pos.y == -1)) { return true; @@ -261,6 +270,12 @@ var ONE_MINUS_WEIGHTING = 1 - WEIGHTING; var AVERAGE_MOUSE_VELOCITY_FOR_SEEK_TO = 20; function isShakingMouse() { // True if the person is waving the mouse around trying to find it. var now = Date.now(), mouse = Reticle.position, isShaking = false; + + // if the lasers are on, then we ignore mouse shaking + if (systemLaserOn) { + return false; + } + if (lastIntegration && (lastIntegration !== now)) { var velocity = Vec3.length(Vec3.subtract(mouse, lastMouse)) / (now - lastIntegration); averageMouseVelocity = (ONE_MINUS_WEIGHTING * averageMouseVelocity) + (WEIGHTING * velocity); @@ -275,9 +290,14 @@ function isShakingMouse() { // True if the person is waving the mouse around try var NON_LINEAR_DIVISOR = 2; var MINIMUM_SEEK_DISTANCE = 0.1; function updateSeeking(doNotStartSeeking) { + // if the lasers are on, then we never do seeking + if (systemLaserOn) { + isSeeking = false; + return; + } + if (!doNotStartSeeking && (!Reticle.visible || isShakingMouse())) { if (!isSeeking) { - print('Start seeking mouse.'); isSeeking = true; } } // e.g., if we're about to turn it on with first movement. @@ -287,7 +307,6 @@ function updateSeeking(doNotStartSeeking) { averageMouseVelocity = lastIntegration = 0; var lookAt2D = HMD.getHUDLookAtPosition2D(); if (!lookAt2D) { // If this happens, something has gone terribly wrong. - print('Cannot seek without lookAt position'); isSeeking = false; return; // E.g., if parallel to location in HUD } @@ -303,7 +322,6 @@ function updateSeeking(doNotStartSeeking) { } var okX = !updateDimension('x'), okY = !updateDimension('y'); // Evaluate both. Don't short-circuit. if (okX && okY) { - print('Finished seeking mouse'); isSeeking = false; } else { Reticle.setPosition(copy); // Not setReticlePosition @@ -322,7 +340,7 @@ function updateMouseActivity(isClick) { return; } // Bug: mouse clicks should keep going. Just not hand controller clicks handControllerLockOut.update(now); - Reticle.visible = true; + Reticle.visible = !systemLaserOn; } function expireMouseCursor(now) { if (!isPointingAtOverlay() && mouseCursorActivity.expired(now)) { @@ -476,7 +494,6 @@ var LASER_ALPHA = 0.5; var LASER_SEARCH_COLOR_XYZW = {x: 10 / 255, y: 10 / 255, z: 255 / 255, w: LASER_ALPHA}; var LASER_TRIGGER_COLOR_XYZW = {x: 250 / 255, y: 10 / 255, z: 10 / 255, w: LASER_ALPHA}; var SYSTEM_LASER_DIRECTION = {x: 0, y: 0, z: -1}; -var systemLaserOn = false; function clearSystemLaser() { if (!systemLaserOn) { return; diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 6ea281e467..197d8f550a 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -25,6 +25,7 @@ +
@@ -94,4 +95,4 @@
- \ No newline at end of file + diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 60aa2ebe25..1af9c1e1d6 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -39,6 +39,7 @@ function loaded() { elInView = document.getElementById("in-view") elRadius = document.getElementById("radius"); elTeleport = document.getElementById("teleport"); + elPal = document.getElementById("pal"); elEntityTable = document.getElementById("entity-table"); elInfoToggle = document.getElementById("info-toggle"); elInfoToggleGlyph = elInfoToggle.firstChild; @@ -274,6 +275,9 @@ function loaded() { elTeleport.onclick = function () { EventBridge.emitWebEvent(JSON.stringify({ type: 'teleport' })); } + elPal.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ type: 'pal' })); + } elDelete.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); refreshEntities(); diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 3e9d3b5648..085d4f5e27 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -98,7 +98,6 @@ EntityListTool = function(opts) { webView.emitScriptEvent(JSON.stringify(data)); } - webView.webEventReceived.connect(function(data) { data = JSON.parse(data); if (data.type == "selectionUpdate") { @@ -120,6 +119,23 @@ EntityListTool = function(opts) { if (selectionManager.hasSelection()) { MyAvatar.position = selectionManager.worldPosition; } + } else if (data.type == "pal") { + var sessionIds = {}; // Collect the sessionsIds of all selected entitities, 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]}), 'local'); + } } else if (data.type == "delete") { deleteSelectedEntities(); } else if (data.type == "toggleLocked") { @@ -140,4 +156,4 @@ EntityListTool = function(opts) { }); return that; -}; \ No newline at end of file +}; diff --git a/scripts/system/pal.js b/scripts/system/pal.js index e727f9a1e3..c0450a0b94 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -11,7 +11,19 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -// FIXME when we make this a defaultScript: (function() { // BEGIN LOCAL_SCOPE +// hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed +// something, will revisit as this is sorta horrible. +const UNSELECTED_TEXTURES = {"idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png"), + "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png") +}; +const SELECTED_TEXTURES = { "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png"), + "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png") +}; + +const UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6}; +const SELECTED_COLOR = {red: 0xf3, green: 0x91, blue: 0x29}; + +(function() { // BEGIN LOCAL_SCOPE Script.include("/~/system/libraries/controllers.js"); @@ -19,8 +31,19 @@ Script.include("/~/system/libraries/controllers.js"); // Overlays. // var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier. -function ExtendedOverlay(key, type, properties, selected) { // A wrapper around overlays to store the key it is associated with. + +function ExtendedOverlay(key, type, properties, selected, hasModel) { // A wrapper around overlays to store the key it is associated with. overlays[key] = this; + if (hasModel) { + var modelKey = key + "-m"; + this.model = new ExtendedOverlay(modelKey, "model", { + url: Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx"), + textures: textures(selected), + ignoreRayIntersection: true + }, false, false); + } else { + this.model = undefined; + } this.key = key; this.selected = selected || false; // not undefined this.activeOverlay = Overlays.addOverlay(type, properties); // We could use different overlays for (un)selected... @@ -34,14 +57,24 @@ ExtendedOverlay.prototype.deleteOverlay = function () { // remove display and da ExtendedOverlay.prototype.editOverlay = function (properties) { // change display of this overlay Overlays.editOverlay(this.activeOverlay, properties); }; -const UNSELECTED_COLOR = {red: 20, green: 250, blue: 20}; -const SELECTED_COLOR = {red: 250, green: 20, blue: 20}; -function color(selected) { return selected ? SELECTED_COLOR : UNSELECTED_COLOR; } + +function color(selected) { + return selected ? SELECTED_COLOR : UNSELECTED_COLOR; +} + +function textures(selected) { + return selected ? SELECTED_TEXTURES : UNSELECTED_TEXTURES; +} + ExtendedOverlay.prototype.select = function (selected) { if (this.selected === selected) { return; } + this.editOverlay({color: color(selected)}); + if (this.model) { + this.model.editOverlay({textures: textures(selected)}); + } this.selected = selected; }; // Class methods: @@ -91,7 +124,7 @@ function HighlightedEntity(id, entityProperties) { }, lineWidth: 1.0, ignoreRayIntersection: true, - drawInFront: true + drawInFront: false // Arguable. For now, let's not distract with mysterious wires around the scene. }); HighlightedEntity.overlays.push(this); } @@ -167,12 +200,12 @@ pal.fromQml.connect(function (message) { // messages are {method, params}, like // function addAvatarNode(id) { var selected = ExtendedOverlay.isSelected(id); - return new ExtendedOverlay(id, "sphere", { // 3d so we don't go cross-eyed looking at it, but on top of everything - solid: true, - alpha: 0.8, - color: color(selected), - drawInFront: true - }, selected); + return new ExtendedOverlay(id, "sphere", { + drawInFront: true, + solid: true, + alpha: 0.8, + color: color(selected), + ignoreRayIntersection: false}, selected, true); } function populateUserList() { var data = []; @@ -227,6 +260,7 @@ function updateOverlays() { if (!id) { return; // don't update ourself } + var overlay = ExtendedOverlay.get(id); if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. print('Adding non-PAL avatar node', id); @@ -235,11 +269,36 @@ function updateOverlays() { var avatar = AvatarList.getAvatar(id); var target = avatar.position; var distance = Vec3.distance(target, eye); + var offset = 0.2; + + // base offset on 1/2 distance from hips to head if we can + var headIndex = avatar.getJointIndex("Head"); + if (headIndex > 0) { + offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; + } + + // get diff between target and eye (a vector pointing to the eye from avatar position) + var diff = Vec3.subtract(target, eye); + + // move a bit in front, towards the camera + target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); + + // now bump it up a bit + target.y = target.y + offset; + overlay.ping = pingPong; overlay.editOverlay({ position: target, - dimensions: 0.05 * distance // constant apparent size + dimensions: 0.032 * distance }); + if (overlay.model) { + overlay.model.ping = pingPong; + overlay.model.editOverlay({ + position: target, + scale: 0.2 * distance, // constant apparent size + rotation: Camera.orientation + }); + } }); pingPong = !pingPong; ExtendedOverlay.some(function (overlay) { // Remove any that weren't updated. (User is gone.) @@ -262,7 +321,7 @@ function removeOverlays() { function handleClick(pickRay) { ExtendedOverlay.applyPickRay(pickRay, function (overlay) { // Don't select directly. Tell qml, who will give us back a list of ids. - var message = {method: 'select', params: [overlay.key, !overlay.selected]}; + var message = {method: 'select', params: [[overlay.key], !overlay.selected]}; pal.sendToQml(message); return true; }); @@ -333,6 +392,31 @@ function onClicked() { pal.setVisible(!pal.visible); } +// +// Message from other scripts, such as edit.js +// +var CHANNEL = 'com.highfidelity.pal'; +function receiveMessage(channel, messageString, senderID) { + if ((channel !== CHANNEL) || + (senderID !== MyAvatar.sessionUUID)) { + return; + } + var message = JSON.parse(messageString); + switch (message.method) { + case 'select': + if (!pal.visible) { + onClicked(); + } + pal.sendToQml(message); // Accepts objects, not just strings. + break; + default: + print('Unrecognized PAL message', messageString); + } +} +Messages.subscribe(CHANNEL); +Messages.messageReceived.connect(receiveMessage); + + var AVERAGING_RATIO = 0.05; var LOUDNESS_FLOOR = 11.0; var LOUDNESS_SCALE = 2.8 / 5.0; @@ -412,8 +496,10 @@ Script.scriptEnding.connect(function () { Users.usernameFromIDReply.disconnect(usernameFromIDReply); Window.domainChanged.disconnect(clearIgnoredInQMLAndClosePAL); Window.domainConnectionRefused.disconnect(clearIgnoredInQMLAndClosePAL); + Messages.unsubscribe(CHANNEL); + Messages.messageReceived.disconnect(receiveMessage); off(); }); -// FIXME: }()); // END LOCAL_SCOPE +}()); // END LOCAL_SCOPE diff --git a/scripts/tutorials/entity_scripts/chair.js b/scripts/tutorials/entity_scripts/chair.js new file mode 100644 index 0000000000..e28e253657 --- /dev/null +++ b/scripts/tutorials/entity_scripts/chair.js @@ -0,0 +1,169 @@ +// chair.js +// +// Restrictions right now: +// Chair objects need to be set as not colliding with avatars, so that they pull avatar +// avatar into collision with them. Also they need to be at or above standing height +// (like a stool). +// +// 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 +// + +(function(){ + + var CHECK_INTERVAL_MSECS = 250; // When sitting, check for need to stand up + var SETTINGS_INTERVAL_MSECS = 1000; // Periodically check user data for updates + var DEFAULT_SIT_DISTANCE = 1.0; // How far away from the chair can you sit? + var HYSTERESIS = 1.1; + + var sitTarget = { x: 0, y: 0, z: 0 }; // Offset where your butt should go relative + // to the object's center. + var SITTING = 0; + var STANDING = 1; + + var state = STANDING; + var sitDistance = DEFAULT_SIT_DISTANCE; + + var entity; + var props; + var checkTimer = false; + var settingsTimer = false; + var sitting = false; + + var _this; + + var WANT_DEBUG = false; + function debugPrint(string) { + if (WANT_DEBUG) { + print(string); + } + } + + function howFarAway(position) { + return Vec3.distance(MyAvatar.position, position); + } + + function isSeatOpen(position, distance) { + closest = true; + AvatarList.getAvatarIdentifiers().forEach(function(avatarSessionUUID) { + var avatar = AvatarList.getAvatar(avatarSessionUUID); + if (avatarSessionUUID && Vec3.distance(avatar.position, position) < distance) { + debugPrint("Seat Occupied!"); + closest = false; + } + }); + return closest; + } + + function enterSitPose() { + var rot; + var UPPER_LEG_ANGLE = 240; + var LOWER_LEG_ANGLE = -80; + rot = Quat.safeEulerAngles(MyAvatar.getJointRotation("RightUpLeg")); + MyAvatar.setJointData("RightUpLeg", Quat.fromPitchYawRollDegrees(UPPER_LEG_ANGLE, rot.y, rot.z), MyAvatar.getJointTranslation("RightUpLeg")); + rot = Quat.safeEulerAngles(MyAvatar.getJointRotation("RightLeg")); + MyAvatar.setJointData("RightLeg", Quat.fromPitchYawRollDegrees(LOWER_LEG_ANGLE, rot.y, rot.z), MyAvatar.getJointTranslation("RightLeg")); + rot = Quat.safeEulerAngles(MyAvatar.getJointRotation("LeftUpLeg")); + MyAvatar.setJointData("LeftUpLeg", Quat.fromPitchYawRollDegrees(UPPER_LEG_ANGLE, rot.y, rot.z), MyAvatar.getJointTranslation("LeftUpLeg")); + rot = Quat.safeEulerAngles(MyAvatar.getJointRotation("LeftLeg")); + MyAvatar.setJointData("LeftLeg", Quat.fromPitchYawRollDegrees(LOWER_LEG_ANGLE, rot.y, rot.z), MyAvatar.getJointTranslation("LeftLeg")); + } + + function leaveSitPose() { + MyAvatar.clearJointData("RightUpLeg"); + MyAvatar.clearJointData("LeftUpLeg"); + MyAvatar.clearJointData("RightLeg"); + MyAvatar.clearJointData("LeftLeg"); + } + + function sitDown(position, rotation) { + var eulers = Quat.safeEulerAngles(MyAvatar.orientation); + eulers.y = Quat.safeEulerAngles(rotation).y; + MyAvatar.position = Vec3.sum(position, Vec3.multiplyQbyV(props.rotation, sitTarget)); + MyAvatar.orientation = Quat.fromPitchYawRollDegrees(eulers.x, eulers.y, eulers.z); + + enterSitPose(); + state = SITTING; + } + + this.preload = function(entityID) { + // Load the sound and range from the entity userData fields, and note the position of the entity. + debugPrint("chair preload"); + entity = entityID; + _this = this; + settingsTimer = Script.setInterval(this.checkSettings, SETTINGS_INTERVAL_MSECS); + }; + + this.maybeStand = function() { + props = Entities.getEntityProperties(entity, [ "position", "rotation" ]); + // First, check if the entity is far enough away to not need to do anything with it + var howFar = howFarAway(props.position); + if ((state === SITTING) && (howFar > sitDistance * HYSTERESIS)) { + leaveSitPose(); + Script.clearInterval(checkTimer); + checkTimer = null; + state = STANDING; + debugPrint("Standing"); + } + } + + this.clickDownOnEntity = function(entityID, mouseEvent) { + // If entity is clicked, sit + props = Entities.getEntityProperties(entity, [ "position", "rotation" ]); + if ((state === STANDING) && isSeatOpen(props.position, sitDistance)) { + sitDown(props.position, props.rotation); + checkTimer = Script.setInterval(this.maybeStand, CHECK_INTERVAL_MSECS); + debugPrint("Sitting from mouse click"); + } + } + + this.startFarTrigger = function() { + // If entity is far clicked, sit + props = Entities.getEntityProperties(entity, [ "position", "rotation" ]); + if ((state === STANDING) && isSeatOpen(props.position, sitDistance)) { + sitDown(props.position, props.rotation); + checkTimer = Script.setInterval(this.maybeStand, CHECK_INTERVAL_MSECS); + debugPrint("Sitting from far trigger"); + } + } + + this.checkSettings = function() { + var dataProps = Entities.getEntityProperties(entity, [ "userData" ]); + if (dataProps.userData) { + var data = JSON.parse(dataProps.userData); + if (data.sitDistance) { + if (!(sitDistance === data.sitDistance)) { + debugPrint("Read new sit distance: " + data.sitDistance); + } + sitDistance = data.sitDistance; + } + if (data.sitTarget) { + if (data.sitTarget.y && (data.sitTarget.y != sitTarget.y)) { + debugPrint("Read new sitTarget.y: " + data.sitTarget.y); + sitTarget.y = data.sitTarget.y; + } + if (data.sitTarget.x && (data.sitTarget.x != sitTarget.x)) { + debugPrint("Read new sitTarget.x: " + data.sitTarget.x); + sitTarget.x = data.sitTarget.x; + } + if (data.sitTarget.z && (data.sitTarget.z != sitTarget.z)) { + debugPrint("Read new sitTarget.z: " + data.sitTarget.z); + sitTarget.z = data.sitTarget.z; + } + } + } + } + + this.unload = function(entityID) { + debugPrint("chair unload"); + if (checkTimer) { + Script.clearInterval(checkTimer); + } + if (settingsTimer) { + Script.clearInterval(settingsTimer); + } + }; + +})