diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 01715497b1..19ebd4ea87 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -68,6 +68,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); packetReceiver.registerListener(PacketType::NodeMuteRequest, this, "handleNodeMuteRequestPacket"); packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket"); + packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket"); connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled); } @@ -221,6 +222,20 @@ void AudioMixer::handleKillAvatarPacket(QSharedPointer packet, } } +void AudioMixer::handleRequestsDomainListDataPacket(QSharedPointer message, SharedNodePointer senderNode) { + auto nodeList = DependencyManager::get(); + nodeList->getOrCreateLinkedData(senderNode); + + if (senderNode->getLinkedData()) { + AudioMixerClientData* nodeData = dynamic_cast(senderNode->getLinkedData()); + if (nodeData != nullptr) { + bool isRequesting; + message->readPrimitive(&isRequesting); + nodeData->setRequestsDomainListData(isRequesting); + } + } +} + void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { sendingNode->parseIgnoreRequestMessage(packet); } diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 59cdec7732..d9759653fb 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -61,6 +61,7 @@ private slots: void handleMuteEnvironmentPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode); void handleNodeKilled(SharedNodePointer killedNode); + void handleRequestsDomainListDataPacket(QSharedPointer message, SharedNodePointer senderNode); void handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleKillAvatarPacket(QSharedPointer packet, SharedNodePointer sendingNode); diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 07e98c044b..e637fd0409 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -92,6 +92,8 @@ public: glm::vec3 getPosition() { return getAvatarAudioStream() ? getAvatarAudioStream()->getPosition() : glm::vec3(0); } glm::vec3 getAvatarBoundingBoxCorner() { return getAvatarAudioStream() ? getAvatarAudioStream()->getAvatarBoundingBoxCorner() : glm::vec3(0); } glm::vec3 getAvatarBoundingBoxScale() { return getAvatarAudioStream() ? getAvatarAudioStream()->getAvatarBoundingBoxScale() : glm::vec3(0); } + bool getRequestsDomainListData() { return _requestsDomainListData; } + void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; } signals: void injectorStreamFinished(const QUuid& streamIdentifier); @@ -122,6 +124,7 @@ private: bool _shouldFlushEncoder { false }; bool _shouldMuteClient { false }; + bool _requestsDomainListData { false }; }; #endif // hifi_AudioMixerClientData_h diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index 42e8c5bdef..28d3358eb5 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -209,8 +209,13 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& node) { // and that it isn't being ignored by our listening node // and that it isn't ignoring our listening node AudioMixerClientData* otherData = static_cast(otherNode->getLinkedData()); + + // When this is true, the AudioMixer will send Audio data to a client about avatars that have ignored them + bool getsAnyIgnored = nodeData->getRequestsDomainListData() && node->getCanKick(); + if (otherData - && !node->isIgnoringNodeWithID(otherNode->getUUID()) && !otherNode->isIgnoringNodeWithID(node->getUUID())) { + && (!node->isIgnoringNodeWithID(otherNode->getUUID()) || (otherData->getRequestsDomainListData() && otherNode->getCanKick())) + && (!otherNode->isIgnoringNodeWithID(node->getUUID()) || getsAnyIgnored)) { // check to see if we're ignoring in radius bool insideIgnoreRadius = false; @@ -219,7 +224,7 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& node) { // We'll always be inside the radius in that case. insideIgnoreRadius = true; // Check to see if the space bubble is enabled - } else if ((node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) && (*otherNode != *node)) { + } else if ((node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled())) { // Define the minimum bubble size static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f); AudioMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 7d9cc7c5b7..206b9bbdd9 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -299,7 +299,7 @@ void AvatarMixer::broadcastAvatarData() { AvatarMixerClientData* otherData = reinterpret_cast(otherNode->getLinkedData()); AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); // Check to see if the space bubble is enabled - if ((node->isIgnoreRadiusEnabled() && !getsIgnoredByMe) || (otherNode->isIgnoreRadiusEnabled() && !getsAnyIgnored)) { + if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) { // Define the minimum bubble size static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f); // Define the scale of the box for the current node @@ -326,11 +326,11 @@ void AvatarMixer::broadcastAvatarData() { // Perform the collision check between the two bounding boxes if (nodeBox.touches(otherNodeBox)) { nodeData->ignoreOther(node, otherNode); - return false; + return getsAnyIgnored; } } // Not close enough to ignore - nodeData->removeFromRadiusIgnoringSet(otherNode->getUUID()); + nodeData->removeFromRadiusIgnoringSet(node, otherNode->getUUID()); return true; } }, diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 5732f63eb6..b600f7c925 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -58,6 +58,15 @@ void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointe } } +void AvatarMixerClientData::removeFromRadiusIgnoringSet(SharedNodePointer self, const QUuid& other) { + if (isRadiusIgnoring(other)) { + _radiusIgnoredOthers.erase(other); + auto exitingSpaceBubblePacket = NLPacket::create(PacketType::ExitingSpaceBubble, NUM_BYTES_RFC4122_UUID); + exitingSpaceBubblePacket->write(other.toRfc4122()); + DependencyManager::get()->sendUnreliablePacket(*exitingSpaceBubblePacket, *self); + } +} + void AvatarMixerClientData::readViewFrustumPacket(const QByteArray& message) { _currentViewFrustum.fromByteArray(message); } diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index a6d4e31e81..c9306f73c7 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -89,7 +89,7 @@ public: glm::vec3 getGlobalBoundingBoxCorner() { return _avatar ? _avatar->getGlobalBoundingBoxCorner() : glm::vec3(0); } bool isRadiusIgnoring(const QUuid& other) { return _radiusIgnoredOthers.find(other) != _radiusIgnoredOthers.end(); } void addToRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.insert(other); } - void removeFromRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.erase(other); } + void removeFromRadiusIgnoringSet(SharedNodePointer self, const QUuid& other); void ignoreOther(SharedNodePointer self, SharedNodePointer other); void readViewFrustumPacket(const QByteArray& message); 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/Stats.qml b/interface/resources/qml/Stats.qml index 895055618b..086c1d15d2 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -83,6 +83,18 @@ Item { text: "Missed Frame Count: " + root.appdropped; visible: root.appdropped > 0; } + StatText { + text: "Long Render Count: " + root.longrenders; + visible: root.longrenders > 0; + } + StatText { + text: "Long Submit Count: " + root.longsubmits; + visible: root.longsubmits > 0; + } + StatText { + text: "Long Frame Count: " + root.longframes; + visible: root.longframes > 0; + } StatText { text: "Packets In/Out: " + root.packetInCount + "/" + root.packetOutCount } 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 833cf4efe2..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" @@ -104,6 +89,7 @@ Item { border.width: 2 // "ADMIN" text RalewaySemiBold { + id: adminTabText text: "ADMIN" // Text size size: hifi.fontSizes.tableHeading + 2 @@ -325,7 +311,12 @@ Item { visible: iAmAdmin color: hifi.colors.lightGrayText } - // This Rectangle refers to the [?] popup button + function letterbox(message) { + letterboxMessage.text = message; + letterboxMessage.visible = true + + } + // This Rectangle refers to the [?] popup button next to "NAMES" Rectangle { color: hifi.colors.tableBackgroundLight width: 20 @@ -348,50 +339,46 @@ 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 } } - // Explanitory popup upon clicking "[?]" - Item { - visible: false - id: namesPopup - anchors.fill: pal - Rectangle { + // This Rectangle refers to the [?] popup button next to "ADMIN" + Rectangle { + visible: iAmAdmin + color: adminTab.color + width: 20 + height: 28 + anchors.right: adminTab.right + anchors.rightMargin: 31 + anchors.top: adminTab.top + anchors.topMargin: 2 + RalewayRegular { + id: adminHelpText + text: "[?]" + size: hifi.fontSizes.tableHeading + 2 + font.capitalization: Font.AllUppercase + color: hifi.colors.redHighlight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter 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 - } + hoverEnabled: 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 } } + LetterboxMessage { + id: letterboxMessage + } function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml var data = optionalData || userModelData, length = data.length; @@ -427,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/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index da1b2868a7..cb99dc6005 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -95,7 +95,7 @@ Item { readonly property color tableRowDarkEven: "#1c1c1c" // Equivalent to "#a6181818" over #404040 background readonly property color tableBackgroundLight: tableRowLightEven readonly property color tableBackgroundDark: tableRowDarkEven - readonly property color tableScrollHandleLight: tableRowLightOdd + readonly property color tableScrollHandleLight: "#8F8F8F" readonly property color tableScrollHandleDark: "#707070" readonly property color tableScrollBackgroundLight: tableRowLightEven readonly property color tableScrollBackgroundDark: "#323232" diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bce602fb69..0fe4069b29 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6145,7 +6145,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/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 1ade21930d..81eee6b3a7 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1339,7 +1339,10 @@ void Avatar::addToScene(AvatarSharedPointer myHandle) { render::ScenePointer scene = qApp->getMain3DScene(); if (scene) { render::PendingChanges pendingChanges; - if (DependencyManager::get()->shouldRenderAvatars() && !DependencyManager::get()->isIgnoringNode(getSessionUUID())) { + auto nodelist = DependencyManager::get(); + if (DependencyManager::get()->shouldRenderAvatars() + && !nodelist->isIgnoringNode(getSessionUUID()) + && !nodelist->isRadiusIgnoringNode(getSessionUUID())) { addToScene(myHandle, scene, pendingChanges); } scene->enqueuePendingChanges(pendingChanges); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index e3ccc10a65..1f5726acba 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -79,6 +79,7 @@ AvatarManager::AvatarManager(QObject* parent) : packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "processKillAvatar"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "processAvatarIdentityPacket"); + packetReceiver.registerListener(PacketType::ExitingSpaceBubble, this, "processExitingSpaceBubble"); // when we hear that the user has ignored an avatar by session UUID // immediately remove that avatar instead of waiting for the absence of packets from avatar mixer @@ -257,6 +258,9 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) { emit DependencyManager::get()->enteredIgnoreRadius(); } + if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble || removalReason == YourAvatarEnteredTheirBubble) { + DependencyManager::get()->radiusIgnoreNodeBySessionID(avatar->getSessionUUID(), true); + } _avatarFades.push_back(removedAvatar); } 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/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 95f31283d0..ff0028322c 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -127,12 +127,18 @@ void Stats::updateStats(bool force) { auto displayPlugin = qApp->getActiveDisplayPlugin(); auto stats = displayPlugin->getHardwareStats(); STAT_UPDATE(appdropped, stats["app_dropped_frame_count"].toInt()); + STAT_UPDATE(longrenders, stats["long_render_count"].toInt()); + STAT_UPDATE(longsubmits, stats["long_submit_count"].toInt()); + STAT_UPDATE(longframes, stats["long_frame_count"].toInt()); STAT_UPDATE(renderrate, displayPlugin->renderRate()); STAT_UPDATE(presentrate, displayPlugin->presentRate()); STAT_UPDATE(presentnewrate, displayPlugin->newFramePresentRate()); STAT_UPDATE(presentdroprate, displayPlugin->droppedFrameRate()); STAT_UPDATE(stutterrate, displayPlugin->stutterRate()); } else { + STAT_UPDATE(appdropped, -1); + STAT_UPDATE(longrenders, -1); + STAT_UPDATE(longsubmits, -1); STAT_UPDATE(presentrate, -1); STAT_UPDATE(presentnewrate, -1); STAT_UPDATE(presentdroprate, -1); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index f0bd1f8ab7..3fe851494c 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -40,6 +40,9 @@ class Stats : public QQuickItem { STATS_PROPERTY(float, stutterrate, 0) STATS_PROPERTY(int, appdropped, 0) + STATS_PROPERTY(int, longsubmits, 0) + STATS_PROPERTY(int, longrenders, 0) + STATS_PROPERTY(int, longframes, 0) STATS_PROPERTY(float, presentnewrate, 0) STATS_PROPERTY(float, presentdroprate, 0) @@ -137,6 +140,9 @@ public slots: void forceUpdateStats() { updateStats(true); } signals: + void longsubmitsChanged(); + void longrendersChanged(); + void longframesChanged(); void appdroppedChanged(); void framerateChanged(); void expandedChanged(); diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index c708176da7..f4e94b9a35 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -161,6 +161,13 @@ void AvatarHashMap::processKillAvatar(QSharedPointer message, S removeAvatar(sessionUUID, reason); } +void AvatarHashMap::processExitingSpaceBubble(QSharedPointer message, SharedNodePointer sendingNode) { + // read the node id + QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + auto nodeList = DependencyManager::get(); + nodeList->radiusIgnoreNodeBySessionID(sessionUUID, false); +} + void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason) { QWriteLocker locker(&_hashLock); diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 02aef6ac8a..eae4026bfc 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -57,6 +57,7 @@ private slots: void processAvatarDataPacket(QSharedPointer message, SharedNodePointer sendingNode); void processAvatarIdentityPacket(QSharedPointer message, SharedNodePointer sendingNode); void processKillAvatar(QSharedPointer message, SharedNodePointer sendingNode); + void processExitingSpaceBubble(QSharedPointer message, SharedNodePointer sendingNode); protected: AvatarHashMap(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index c4846f6af8..a6f7d3caf4 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -165,6 +165,7 @@ public: if (newPlugin) { bool hasVsync = true; + QThread::setPriority(newPlugin->getPresentPriority()); bool wantVsync = newPlugin->wantVsync(); _context->makeCurrent(); #if defined(Q_OS_WIN) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index a6de3f7baa..ee0bcf91a9 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -80,6 +81,7 @@ protected: void updateCompositeFramebuffer(); + virtual QThread::Priority getPresentPriority() { return QThread::HighPriority; } virtual void compositeLayers(); virtual void compositeScene(); virtual void compositeOverlay(); diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index bd3203150e..d890431a45 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -238,6 +238,10 @@ void NodeList::reset() { _numNoReplyDomainCheckIns = 0; + // lock and clear our set of radius ignored IDs + _radiusIgnoredSetLock.lockForWrite(); + _radiusIgnoredNodeIDs.clear(); + _radiusIgnoredSetLock.unlock(); // lock and clear our set of ignored IDs _ignoredSetLock.lockForWrite(); _ignoredNodeIDs.clear(); @@ -781,6 +785,22 @@ void NodeList::sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationN sendPacket(std::move(ignorePacket), *destinationNode); } +void NodeList::radiusIgnoreNodeBySessionID(const QUuid& nodeID, bool radiusIgnoreEnabled) { + if (radiusIgnoreEnabled) { + QReadLocker radiusIgnoredSetLocker{ &_radiusIgnoredSetLock }; // read lock for insert + // add this nodeID to our set of ignored IDs + _radiusIgnoredNodeIDs.insert(nodeID); + } else { + QWriteLocker radiusIgnoredSetLocker{ &_radiusIgnoredSetLock }; // write lock for unsafe_erase + _radiusIgnoredNodeIDs.unsafe_erase(nodeID); + } +} + +bool NodeList::isRadiusIgnoringNode(const QUuid& nodeID) const { + QReadLocker radiusIgnoredSetLocker{ &_radiusIgnoredSetLock }; // read lock for reading + return _radiusIgnoredNodeIDs.find(nodeID) != _radiusIgnoredNodeIDs.cend(); +} + void NodeList::ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled) { // enumerate the nodes to send a reliable ignore packet to each that can leverage it if (!nodeID.isNull() && _sessionUUID != nodeID) { @@ -1020,12 +1040,12 @@ void NodeList::processUsernameFromIDReply(QSharedPointer messag } void NodeList::setRequestsDomainListData(bool isRequesting) { - // Tell the avatar mixer whether I want to receive any additional data to which I might be entitled + // Tell the avatar mixer and audio mixer whether I want to receive any additional data to which I might be entitled if (_requestsDomainListData == isRequesting) { return; } eachMatchingNode([](const SharedNodePointer& node)->bool { - return node->getType() == NodeType::AvatarMixer; + return (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer); }, [this, isRequesting](const SharedNodePointer& destinationNode) { auto packet = NLPacket::create(PacketType::RequestsDomainListData, sizeof(bool), true); // reliable packet->writePrimitive(isRequesting); diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 75958f1847..8e285629dc 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -76,6 +76,8 @@ public: void toggleIgnoreRadius() { ignoreNodesInRadius(!getIgnoreRadiusEnabled()); } void enableIgnoreRadius() { ignoreNodesInRadius(true); } void disableIgnoreRadius() { ignoreNodesInRadius(false); } + void radiusIgnoreNodeBySessionID(const QUuid& nodeID, bool radiusIgnoreEnabled); + bool isRadiusIgnoringNode(const QUuid& other) const; void ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled); bool isIgnoringNode(const QUuid& nodeID) const; void personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled); @@ -159,6 +161,8 @@ private: QTimer _keepAlivePingTimer; bool _requestsDomainListData; + mutable QReadWriteLock _radiusIgnoredSetLock; + tbb::concurrent_unordered_set _radiusIgnoredNodeIDs; mutable QReadWriteLock _ignoredSetLock; tbb::concurrent_unordered_set _ignoredNodeIDs; mutable QReadWriteLock _personalMutedSetLock; diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index e01f85d03d..95cb7d58df 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -105,7 +105,8 @@ public: UsernameFromIDReply, ViewFrustum, RequestsDomainListData, - LAST_PACKET_TYPE = RequestsDomainListData + ExitingSpaceBubble, + LAST_PACKET_TYPE = ExitingSpaceBubble }; }; diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 4182d5244c..341285a742 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -37,7 +37,7 @@ public slots: * @param {nodeID} nodeID The node or session ID of the user you want to ignore. * @param {bool} enable True for ignored; false for un-ignored. */ - void ignore(const QUuid& nodeID, bool ignoreEnabled); + void ignore(const QUuid& nodeID, bool ignoreEnabled = true); /**jsdoc * Gets a bool containing whether you have ignored the given Avatar UUID. @@ -52,7 +52,7 @@ public slots: * @param {nodeID} nodeID The node or session ID of the user you want to mute. * @param {bool} enable True for enabled; false for disabled. */ - void personalMute(const QUuid& nodeID, bool muteEnabled); + void personalMute(const QUuid& nodeID, bool muteEnabled = true); /**jsdoc * Requests a bool containing whether you have personally muted the given Avatar UUID. diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index 060823a748..b076170ae5 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -31,6 +31,10 @@ OculusDisplayPlugin::OculusDisplayPlugin() { bool OculusDisplayPlugin::internalActivate() { bool result = Parent::internalActivate(); + _longSubmits = 0; + _longRenders = 0; + _longFrames = 0; + currentDebugMode = ovrPerfHud_Off; if (result && _session) { ovr_SetInt(_session, OVR_PERF_HUD_MODE, currentDebugMode); @@ -112,35 +116,43 @@ void OculusDisplayPlugin::uncustomizeContext() { Parent::uncustomizeContext(); } +static const uint64_t FRAME_BUDGET = (11 * USECS_PER_MSEC); +static const uint64_t FRAME_OVER_BUDGET = (15 * USECS_PER_MSEC); + void OculusDisplayPlugin::hmdPresent() { + static uint64_t lastSubmitEnd = 0; + if (!_customized) { return; } PROFILE_RANGE_EX(render, __FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) - int curIndex; - ovr_GetTextureSwapChainCurrentIndex(_session, _textureSwapChain, &curIndex); - GLuint curTexId; - ovr_GetTextureSwapChainBufferGL(_session, _textureSwapChain, curIndex, &curTexId); + { + PROFILE_RANGE_EX(render, "Oculus Blit", 0xff00ff00, (uint64_t)_currentFrame->frameIndex) + int curIndex; + ovr_GetTextureSwapChainCurrentIndex(_session, _textureSwapChain, &curIndex); + GLuint curTexId; + ovr_GetTextureSwapChainBufferGL(_session, _textureSwapChain, curIndex, &curTexId); - // Manually bind the texture to the FBO - // FIXME we should have a way of wrapping raw GL ids in GPU objects without - // taking ownership of the object - auto fbo = getGLBackend()->getFramebufferID(_outputFramebuffer); - glNamedFramebufferTexture(fbo, GL_COLOR_ATTACHMENT0, curTexId, 0); - render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.setFramebuffer(_outputFramebuffer); - batch.setViewportTransform(ivec4(uvec2(), _outputFramebuffer->getSize())); - batch.setStateScissorRect(ivec4(uvec2(), _outputFramebuffer->getSize())); - batch.resetViewTransform(); - batch.setProjectionTransform(mat4()); - batch.setPipeline(_presentPipeline); - batch.setResourceTexture(0, _compositeFramebuffer->getRenderBuffer(0)); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); - glNamedFramebufferTexture(fbo, GL_COLOR_ATTACHMENT0, 0, 0); + // Manually bind the texture to the FBO + // FIXME we should have a way of wrapping raw GL ids in GPU objects without + // taking ownership of the object + auto fbo = getGLBackend()->getFramebufferID(_outputFramebuffer); + glNamedFramebufferTexture(fbo, GL_COLOR_ATTACHMENT0, curTexId, 0); + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.setFramebuffer(_outputFramebuffer); + batch.setViewportTransform(ivec4(uvec2(), _outputFramebuffer->getSize())); + batch.setStateScissorRect(ivec4(uvec2(), _outputFramebuffer->getSize())); + batch.resetViewTransform(); + batch.setProjectionTransform(mat4()); + batch.setPipeline(_presentPipeline); + batch.setResourceTexture(0, _compositeFramebuffer->getRenderBuffer(0)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + glNamedFramebufferTexture(fbo, GL_COLOR_ATTACHMENT0, 0, 0); + } { auto result = ovr_CommitTextureSwapChain(_session, _textureSwapChain); @@ -148,8 +160,33 @@ void OculusDisplayPlugin::hmdPresent() { _sceneLayer.SensorSampleTime = _currentPresentFrameInfo.sensorSampleTime; _sceneLayer.RenderPose[ovrEyeType::ovrEye_Left] = ovrPoseFromGlm(_currentPresentFrameInfo.renderPose); _sceneLayer.RenderPose[ovrEyeType::ovrEye_Right] = ovrPoseFromGlm(_currentPresentFrameInfo.renderPose); + + auto submitStart = usecTimestampNow(); + uint64_t nonSubmitInterval = 0; + if (lastSubmitEnd != 0) { + nonSubmitInterval = submitStart - lastSubmitEnd; + if (nonSubmitInterval > FRAME_BUDGET) { + ++_longRenders; + } + } ovrLayerHeader* layers = &_sceneLayer.Header; - result = ovr_SubmitFrame(_session, _currentFrame->frameIndex, &_viewScaleDesc, &layers, 1); + { + PROFILE_RANGE_EX(render, "Oculus Submit", 0xff00ff00, (uint64_t)_currentFrame->frameIndex) + result = ovr_SubmitFrame(_session, _currentFrame->frameIndex, &_viewScaleDesc, &layers, 1); + } + lastSubmitEnd = usecTimestampNow(); + if (nonSubmitInterval != 0) { + auto submitInterval = lastSubmitEnd - submitStart; + if (nonSubmitInterval < FRAME_BUDGET && submitInterval > FRAME_BUDGET) { + ++_longSubmits; + } + if ((nonSubmitInterval + submitInterval) > FRAME_OVER_BUDGET) { + ++_longFrames; + } + } + + + if (!OVR_SUCCESS(result)) { logWarning("Failed to present"); } @@ -168,6 +205,7 @@ void OculusDisplayPlugin::hmdPresent() { _appDroppedFrames.store(appDroppedFrames); _compositorDroppedFrames.store(compositorDroppedFrames); } + _presentRate.increment(); } @@ -176,6 +214,9 @@ QJsonObject OculusDisplayPlugin::getHardwareStats() const { QJsonObject hardwareStats; hardwareStats["app_dropped_frame_count"] = _appDroppedFrames.load(); hardwareStats["compositor_dropped_frame_count"] = _compositorDroppedFrames.load(); + hardwareStats["long_render_count"] = _longRenders.load(); + hardwareStats["long_submit_count"] = _longSubmits.load(); + hardwareStats["long_frame_count"] = _longFrames.load(); return hardwareStats; } diff --git a/plugins/oculus/src/OculusDisplayPlugin.h b/plugins/oculus/src/OculusDisplayPlugin.h index e44596d6e9..6fc50b829f 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.h +++ b/plugins/oculus/src/OculusDisplayPlugin.h @@ -24,6 +24,8 @@ public: virtual QJsonObject getHardwareStats() const; protected: + QThread::Priority getPresentPriority() override { return QThread::TimeCriticalPriority; } + bool internalActivate() override; void hmdPresent() override; bool isHmdMounted() const override; @@ -39,5 +41,8 @@ private: std::atomic_int _compositorDroppedFrames; std::atomic_int _appDroppedFrames; + std::atomic_int _longSubmits; + std::atomic_int _longRenders; + std::atomic_int _longFrames; }; diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 90a77b508d..e9868bd38d 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -18,9 +18,9 @@ var DEFAULT_SCRIPTS = [ "system/mute.js", "system/goto.js", "system/hmd.js", - "system/marketplaces/marketplace.js", + "system/marketplaces/marketplaces.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 0623ddf100..4d478fbdd5 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)) { @@ -474,7 +492,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); + } + }; + +})