diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 502cf15aa2..b3344e3832 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -176,7 +176,7 @@ std::pair AssetServer::getAssetStatus(const A } else if (loaded && meta.failedLastBake) { return { AssetUtils::Error, meta.lastBakeErrors }; } - + return { AssetUtils::Pending, "" }; } @@ -199,7 +199,7 @@ void AssetServer::maybeBake(const AssetUtils::AssetPath& path, const AssetUtils: void AssetServer::createEmptyMetaFile(const AssetUtils::AssetHash& hash) { QString metaFilePath = "atp:/" + hash + "/meta.json"; QFile metaFile { metaFilePath }; - + if (!metaFile.exists()) { qDebug() << "Creating metafile for " << hash; if (metaFile.open(QFile::WriteOnly)) { @@ -285,7 +285,7 @@ void updateConsumedCores() { auto coreCount = std::thread::hardware_concurrency(); if (isInterfaceRunning) { coreCount = coreCount > MIN_CORES_FOR_MULTICORE ? CPU_AFFINITY_COUNT_HIGH : CPU_AFFINITY_COUNT_LOW; - } + } qCDebug(asset_server) << "Setting max consumed cores to " << coreCount; setMaxCores(coreCount); } @@ -931,6 +931,9 @@ void AssetServer::sendStatsPacket() { connectionStats["5. Period (us)"] = stats.packetSendPeriod; connectionStats["6. Up (Mb/s)"] = stats.sentBytes * megabitsPerSecPerByte; connectionStats["7. Down (Mb/s)"] = stats.receivedBytes * megabitsPerSecPerByte; + connectionStats["last_heard_time_msecs"] = date.toUTC().toMSecsSinceEpoch(); + connectionStats["last_heard_ago_msecs"] = date.msecsTo(QDateTime::currentDateTime()); + nodeStats["Connection Stats"] = connectionStats; using Events = udt::ConnectionStats::Stats::Event; @@ -1147,7 +1150,7 @@ bool AssetServer::deleteMappings(const AssetUtils::AssetPathList& paths) { hashesToCheckForDeletion << it->second; qCDebug(asset_server) << "Deleted a mapping:" << path << "=>" << it->second; - + _fileMappings.erase(it); } else { qCDebug(asset_server) << "Unable to delete a mapping that was not found:" << path; diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index e2f28c221e..470ab3f233 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -668,6 +668,12 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { downstreamStats["min_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMin); downstreamStats["max_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMax); downstreamStats["avg_gap_30s"] = formatUsecTime(streamStats._timeGapWindowAverage); + downstreamStats["min_gap_usecs"] = static_cast(streamStats._timeGapMin); + downstreamStats["max_gap_usecs"] = static_cast(streamStats._timeGapMax); + downstreamStats["avg_gap_usecs"] = static_cast(streamStats._timeGapAverage); + downstreamStats["min_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowMin); + downstreamStats["max_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowMax); + downstreamStats["avg_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowAverage); result["downstream"] = downstreamStats; @@ -695,6 +701,13 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { upstreamStats["max_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMax); upstreamStats["avg_gap_30s"] = formatUsecTime(streamStats._timeGapWindowAverage); + upstreamStats["min_gap_usecs"] = static_cast(streamStats._timeGapMin); + upstreamStats["max_gap_usecs"] = static_cast(streamStats._timeGapMax); + upstreamStats["avg_gap_usecs"] = static_cast(streamStats._timeGapAverage); + upstreamStats["min_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowMin); + upstreamStats["max_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowMax); + upstreamStats["avg_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowAverage); + result["upstream"] = upstreamStats; } else { result["upstream"] = "mic unknown"; @@ -725,6 +738,12 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { upstreamStats["max_gap_30s"] = formatUsecTime(streamStats._timeGapWindowMax); upstreamStats["avg_gap_30s"] = formatUsecTime(streamStats._timeGapWindowAverage); + upstreamStats["min_gap_usecs"] = static_cast(streamStats._timeGapMin); + upstreamStats["max_gap_usecs"] = static_cast(streamStats._timeGapMax); + upstreamStats["avg_gap_usecs"] = static_cast(streamStats._timeGapAverage); + upstreamStats["min_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowMin); + upstreamStats["max_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowMax); + upstreamStats["avg_gap_30s_usecs"] = static_cast(streamStats._timeGapWindowAverage); injectorArray.push_back(upstreamStats); } } diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 80e0060299..63520262cd 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -89,6 +89,7 @@ int OctreeServer::_shortProcessWait = 0; int OctreeServer::_noProcessWait = 0; static const QString PERSIST_FILE_DOWNLOAD_PATH = "/models.json.gz"; +static const double NANOSECONDS_PER_SECOND = 1000000.0;; void OctreeServer::resetSendingStats() { @@ -1197,7 +1198,7 @@ void OctreeServer::domainSettingsRequestComplete() { } else { beginRunning(); } -} +} void OctreeServer::beginRunning() { auto nodeList = DependencyManager::get(); @@ -1344,6 +1345,10 @@ QString OctreeServer::getUptime() { return formattedUptime; } +double OctreeServer::getUptimeSeconds() { + return (usecTimestampNow() - _startedUSecs) / NANOSECONDS_PER_SECOND; +} + QString OctreeServer::getFileLoadTime() { QString result; if (isInitialLoadComplete()) { @@ -1386,6 +1391,10 @@ QString OctreeServer::getFileLoadTime() { return result; } +double OctreeServer::getFileLoadTimeSeconds() { + return getLoadElapsedTime() / NANOSECONDS_PER_SECOND; +} + QString OctreeServer::getConfiguration() { QString result; for (int i = 1; i < _argc; i++) { @@ -1421,6 +1430,8 @@ void OctreeServer::sendStatsPacket() { statsArray1["4. persistFileLoadTime"] = getFileLoadTime(); statsArray1["5. clients"] = getCurrentClientCount(); statsArray1["6. threads"] = threadsStats; + statsArray1["uptime_seconds"] = getUptimeSeconds(); + statsArray1["persistFileLoadTime_seconds"] = getFileLoadTimeSeconds(); // Octree Stats QJsonObject octreeStats; diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index 07b1e334b1..3ae4dddee9 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -158,7 +158,9 @@ protected: void initHTTPManager(int port); void resetSendingStats(); QString getUptime(); + double getUptimeSeconds(); QString getFileLoadTime(); + double getFileLoadTimeSeconds(); QString getConfiguration(); QString getStatusLink(); diff --git a/cmake/installer/installer.ico b/cmake/installer/installer.ico index 5c09071822..b337544f88 100644 Binary files a/cmake/installer/installer.ico and b/cmake/installer/installer.ico differ diff --git a/cmake/ports/aristo/portfile.cmake b/cmake/ports/aristo/portfile.cmake index 532e1304f4..94efe6f7ea 100644 --- a/cmake/ports/aristo/portfile.cmake +++ b/cmake/ports/aristo/portfile.cmake @@ -2,10 +2,12 @@ include(vcpkg_common_functions) set(ARISTO_VERSION 0.8.1) set(MASTER_COPY_SOURCE_PATH ${CURRENT_BUILDTREES_DIR}/src) +file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_BUILD_ASSETS.txt" EXTERNAL_BUILD_ASSETS) + if (WIN32) vcpkg_download_distfile( ARISTO_SOURCE_ARCHIVE - URLS https://athena-public.s3.amazonaws.com/seth/aristo-0.8.1-windows.zip + URLS "${EXTERNAL_BUILD_ASSETS}/seth/aristo-0.8.1-windows.zip" SHA512 05179c63b72a1c9f5be8a7a2b7389025da683400dbf819e5a6199dd6473c56774d2885182dc5a11cb6324058d228a4ead832222e8b3e1bebaa4c61982e85f0a8 FILENAME aristo-0.8.1-windows.zip ) diff --git a/cmake/ports/sranipal/CONTROL b/cmake/ports/sranipal/CONTROL index 3f878b1c4d..b7d510595e 100644 --- a/cmake/ports/sranipal/CONTROL +++ b/cmake/ports/sranipal/CONTROL @@ -1,3 +1,3 @@ Source: sranipal Version: 1.1.0.1 -Description: SRanipal +Description: super reality animation pal! diff --git a/cmake/ports/sranipal/portfile.cmake b/cmake/ports/sranipal/portfile.cmake index da4646be1a..2e6acea361 100644 --- a/cmake/ports/sranipal/portfile.cmake +++ b/cmake/ports/sranipal/portfile.cmake @@ -2,10 +2,12 @@ include(vcpkg_common_functions) set(SRANIPAL_VERSION 1.1.0.1) set(MASTER_COPY_SOURCE_PATH ${CURRENT_BUILDTREES_DIR}/src) +file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_BUILD_ASSETS.txt" EXTERNAL_BUILD_ASSETS) + if (WIN32) vcpkg_download_distfile( SRANIPAL_SOURCE_ARCHIVE - URLS https://athena-public.s3.amazonaws.com/seth/sranipal-1.1.0.1-windows.zip + URLS "${EXTERNAL_BUILD_ASSETS}/seth/sranipal-1.1.0.1-windows.zip" SHA512 b09ce012abe4e3c71e8e69626bdd7823ff6576601a821ab365275f2764406a3e5f7b65fcf2eb1d0962eff31eb5958a148b00901f67c229dc6ace56eb5e6c9e1b FILENAME sranipal-1.1.0.1-windows.zip ) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index cdf92918c6..284dd344e7 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -57,6 +57,29 @@ } ] }, + { + "label": "Monitoring", + "name": "monitoring", + "restart": false, + "settings": [ + { + "name": "enable_prometheus_exporter", + "label": "Enable Prometheus Exporter", + "help": "Enable a Prometheus exporter to make it possible to gather the stats that are available at Nodes tab with a Prometheus server. This makes it possible to keep track of long-term domain statistics for graphing, troubleshooting, and performance monitoring.", + "default": false, + "type": "checkbox", + "advanced": true + }, + { + "name": "prometheus_exporter_port", + "label": "Prometheus TCP Port", + "help": "This is the port where the Prometheus exporter accepts connections. It listens both on IPv4 and IPv6 and can be accessed remotely, so you should make sure to restrict access with a firewall as needed.", + "default": "9703", + "type": "int", + "advanced": true + } + ] + }, { "label": "Paths", "html_id": "paths", diff --git a/domain-server/resources/prometheus_exporter/index.html b/domain-server/resources/prometheus_exporter/index.html new file mode 100644 index 0000000000..5a23c78858 --- /dev/null +++ b/domain-server/resources/prometheus_exporter/index.html @@ -0,0 +1,14 @@ + + + Vircadia Prometheus exporter + + + +

Vircadia Prometheus exporter

+ +

This is the Prometheus exporter, used to export stats about the domain server for graphing and analysis.

+

+ Metrics +

+ + diff --git a/domain-server/resources/web/favicon.ico b/domain-server/resources/web/favicon.ico index becc1b8e8b..cbcf364902 100644 Binary files a/domain-server/resources/web/favicon.ico and b/domain-server/resources/web/favicon.ico differ diff --git a/domain-server/resources/web/js/shared.js b/domain-server/resources/web/js/shared.js index f4053ebddc..3c7dd2705c 100644 --- a/domain-server/resources/web/js/shared.js +++ b/domain-server/resources/web/js/shared.js @@ -52,6 +52,7 @@ var URLs = { // STABLE METAVERSE_URL: https://metaverse.highfidelity.com // STAGING METAVERSE_URL: https://staging.highfidelity.com METAVERSE_URL: 'https://metaverse.highfidelity.com', + CDN_URL: 'https://cdn.highfidelity.com', PLACE_URL: 'https://hifi.place', }; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 9fea49d2da..fab259617d 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -67,16 +67,13 @@ Q_LOGGING_CATEGORY(domain_server_ice, "hifi.domain_server.ice") const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token"; const QString DomainServer::REPLACEMENT_FILE_EXTENSION = ".replace"; +const int MIN_PORT = 1; +const int MAX_PORT = 65535; + int const DomainServer::EXIT_CODE_REBOOT = 234923; -#if USE_STABLE_GLOBAL_SERVICES -const QString ICE_SERVER_DEFAULT_HOSTNAME = "ice.highfidelity.com"; -#else -const QString ICE_SERVER_DEFAULT_HOSTNAME = "dev-ice.highfidelity.com"; -#endif - -QString DomainServer::_iceServerAddr { ICE_SERVER_DEFAULT_HOSTNAME }; +QString DomainServer::_iceServerAddr { NetworkingConstants::ICE_SERVER_DEFAULT_HOSTNAME }; int DomainServer::_iceServerPort { ICE_SERVER_DEFAULT_PORT }; bool DomainServer::_overrideDomainID { false }; QUuid DomainServer::_overridingDomainID; @@ -229,7 +226,6 @@ DomainServer::DomainServer(int argc, char* argv[]) : this, &DomainServer::updateDownstreamNodes); connect(&_settingsManager, &DomainServerSettingsManager::settingsUpdated, this, &DomainServer::updateUpstreamNodes); - setupGroupCacheRefresh(); optionallySetupOAuth(); @@ -330,6 +326,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : _nodePingMonitorTimer = new QTimer{ this }; connect(_nodePingMonitorTimer, &QTimer::timeout, this, &DomainServer::nodePingMonitor); _nodePingMonitorTimer->start(NODE_PING_MONITOR_INTERVAL_MSECS); + + initializeExporter(); } void DomainServer::parseCommandLine(int argc, char* argv[]) { @@ -424,6 +422,11 @@ DomainServer::~DomainServer() { _contentManager->terminate(); } + if (_httpExporterManager) { + _httpExporterManager->close(); + delete _httpExporterManager; + } + DependencyManager::destroy(); // cleanup the AssetClient thread @@ -1977,7 +1980,6 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url const QString URI_API_BACKUPS_ID = "/api/backups/"; const QString URI_API_BACKUPS_DOWNLOAD_ID = "/api/backups/download/"; const QString URI_API_BACKUPS_RECOVER = "/api/backups/recover/"; - const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; QPointer connectionPtr { connection }; @@ -3037,6 +3039,27 @@ void DomainServer::updateUpstreamNodes() { updateReplicationNodes(Upstream); } +void DomainServer::initializeExporter() +{ + static const QString ENABLE_EXPORTER = "monitoring.enable_prometheus_exporter"; + static const QString EXPORTER_PORT = "monitoring.prometheus_exporter_port"; + + bool isExporterEnabled = _settingsManager.valueOrDefaultValueForKeyPath(ENABLE_EXPORTER).toBool(); + int exporterPort = _settingsManager.valueOrDefaultValueForKeyPath(EXPORTER_PORT).toInt(); + + if (exporterPort < MIN_PORT || exporterPort > MAX_PORT) { + qCWarning(domain_server) << "Prometheus exporter port " << exporterPort << " is out of range."; + isExporterEnabled = false; + } + + qCDebug(domain_server) << "Setting up Prometheus exporter"; + + if (isExporterEnabled && !_httpExporterManager) { + qCInfo(domain_server) << "Starting Prometheus exporter on port " << exporterPort; + _httpExporterManager = new HTTPManager(QHostAddress::Any, (quint16)exporterPort, QString("%1/resources/prometheus_exporter/").arg(QCoreApplication::applicationDirPath()), &_exporter); + } +} + void DomainServer::updateReplicatedNodes() { // Make sure we have downstream nodes in our list static const QString REPLICATED_USERS_KEY = "users"; diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 95b4b784cb..7c0fa5fb15 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -36,6 +36,7 @@ #include "DomainContentBackupManager.h" #include "PendingAssignedNodeData.h" +#include "DomainServerExporter.h" #include @@ -115,7 +116,7 @@ private slots: void sendHeartbeatToIceServer(); void nodePingMonitor(); - void handleConnectedNode(SharedNodePointer newNode, quint64 requestReceiveTime); + void handleConnectedNode(SharedNodePointer newNode, quint64 requestReceiveTime); void handleTempDomainSuccess(QNetworkReply* requestReply); void handleTempDomainError(QNetworkReply* requestReply); @@ -138,6 +139,7 @@ private slots: void updateReplicatedNodes(); void updateDownstreamNodes(); void updateUpstreamNodes(); + void initializeExporter(); void tokenGrantFinished(); void profileRequestFinished(); @@ -234,8 +236,10 @@ private: std::vector _replicatedUsernames; DomainGatekeeper _gatekeeper; + DomainServerExporter _exporter; HTTPManager _httpManager; + HTTPManager* _httpExporterManager { nullptr }; std::unique_ptr _httpsManager; QHash _allAssignments; diff --git a/domain-server/src/DomainServerExporter.cpp b/domain-server/src/DomainServerExporter.cpp new file mode 100644 index 0000000000..a7affaf4dd --- /dev/null +++ b/domain-server/src/DomainServerExporter.cpp @@ -0,0 +1,471 @@ +// +// DomainServerExporter.cpp +// domain-server/src +// +// Created by Dale Glass on 3 Apr 2020. +// Copyright 2020 Dale Glass +// +// Prometheus exporter +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// TODO: +// +// Look into the data provided by OctreeServer::handleHTTPRequest in assignment-client/src/octree/OctreeServer.cpp +// Turns out the octree server (entity server) can optionally deliver additional statistics via another HTTP server +// that is disabled by default. This functionality can be enabled by setting statusPort to a port number. +// +// Look into what appears in Audio Mixer -> z_listeners -> jitter -> injectors, so far it's been an empty list. + +#include +#include +#include +#include +#include +#include + +#include "DomainServerExporter.h" +#include "DependencyManager.h" +#include "LimitedNodeList.h" +#include "HTTPConnection.h" +#include "DomainServerNodeData.h" + +Q_LOGGING_CATEGORY(domain_server_exporter, "hifi.domain_server.prometheus_exporter") + +static const QMap TYPE_MAP { + { "asset_server_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_cw_p" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_down_mb_s" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_est_max_p_s" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_last_heard_ago_msecs" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_last_heard_time_msecs" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_period_us" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_rtt_ms" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_connection_stats_up_mb_s" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_downstream_stats_duplicates" , DomainServerExporter::MetricType::Counter }, + { "asset_server_downstream_stats_recvd_p_s" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_downstream_stats_recvd_packets" , DomainServerExporter::MetricType::Counter }, + { "asset_server_downstream_stats_sent_ack" , DomainServerExporter::MetricType::Counter }, + { "asset_server_io_stats_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_io_stats_inbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_io_stats_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_io_stats_outbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_upstream_stats_procd_ack" , DomainServerExporter::MetricType::Counter }, + { "asset_server_upstream_stats_recvd_ack" , DomainServerExporter::MetricType::Counter }, + { "asset_server_upstream_stats_retransmitted" , DomainServerExporter::MetricType::Counter }, + { "asset_server_upstream_stats_sent_p_s" , DomainServerExporter::MetricType::Gauge }, + { "asset_server_upstream_stats_sent_packets" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_listeners_per_frame" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_listeners_silent_per_frame" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_streams_per_frame" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_check_time" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_check_time_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_events" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_events_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_frame" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_frame_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_mix" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_mix_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_packets" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_packets_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_sleep" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_sleep_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_tic" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_avg_timing_stats_us_per_tic_trailing" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_io_stats_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_io_stats_inbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_io_stats_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_io_stats_outbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_available" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_available_avg_10s" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_avg_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_avg_gap_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_desired" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_lost_percent" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_lost_percent_30s" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_max_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_max_gap_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_min_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_min_gap_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_downstream_not_mixed" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_downstream_overflows" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_downstream_starves" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_downstream_unplayed" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_injectors" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_upstream_available" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_available_avg_10s" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_avg_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_avg_gap_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_desired_calc" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_lost_percent" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_lost_percent_30s" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_max_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_max_gap_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_mic_desired" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_min_gap_30s_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_min_gap_usecs" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_listeners_jitter_upstream_not_mixed" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_upstream_overflows" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_upstream_silents_dropped" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_upstream_starves" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_jitter_upstream_unplayed" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_listeners_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_active_streams" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_active_to_inactive" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_active_to_skippped" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_avg_mixes_per_block" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_hrtf_renders" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_hrtf_resets" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_hrtf_updates" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_inactive_streams" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_inactive_to_active" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_inactive_to_skippped" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_percent_hrtf_mixes" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_percent_manual_echo_mixes" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_percent_manual_stereo_mixes" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_mix_stats_skipped_streams" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_skippped_to_active" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_skippped_to_inactive" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_mix_stats_total_mixes" , DomainServerExporter::MetricType::Counter }, + { "audio_mixer_silent_packets_per_frame" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_threads" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_throttling_ratio" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_trailing_mix_ratio" , DomainServerExporter::MetricType::Gauge }, + { "audio_mixer_use_dynamic_jitter_buffers" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_av_data_receive_rate" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_avg_other_av_skips_per_second" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_avg_other_av_starves_per_second" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_delta_full_vs_avatar_data_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_inbound_av_data_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_num_avs_sent_last_frame" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_outbound_av_data_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_outbound_av_traits_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_recent_other_av_in_view" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_recent_other_av_out_of_view" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_avatars_total_num_out_of_order_sends" , DomainServerExporter::MetricType::Counter }, + { "avatar_mixer_average_listeners_last_second" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_broadcast_loop_rate" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_io_stats_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_io_stats_inbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_io_stats_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_io_stats_outbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_parallel_tasks_broadcast_avatar_data_functor" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_parallel_tasks_broadcast_avatar_data_innner" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_parallel_tasks_broadcast_avatar_data_lock_wait" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_parallel_tasks_broadcast_avatar_data_node_transform" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_parallel_tasks_broadcast_avatar_data_total" , DomainServerExporter::MetricType::Counter }, + { "avatar_mixer_parallel_tasks_display_name_management_total" , DomainServerExporter::MetricType::Counter }, + { "avatar_mixer_parallel_tasks_process_queued_avatar_data_packets_lock_wait" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_parallel_tasks_process_queued_avatar_data_packets_total" , DomainServerExporter::MetricType::Counter }, + { "avatar_mixer_single_core_tasks_incoming_packets_handle_avatar_identity_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_incoming_packets_handle_avatar_query_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_incoming_packets_handle_kill_avatar_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_incoming_packets_handle_node_ignore_request_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_incoming_packets_handle_radius_ignore_request_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_incoming_packets_handle_requests_domain_list_data_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_process_events" , DomainServerExporter::MetricType::Counter }, + { "avatar_mixer_single_core_tasks_queue_incoming_packet" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_single_core_tasks_send_stats" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_received_1_nodes_processed" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_1_nodes_broadcasted_to" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_2_average_others_included" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_3_average_over_budget_avatars" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_4_average_data_bytes" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_5_average_traits_bytes" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_6_average_identity_bytes" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_sent_7_average_hero_avatars" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_timing_1_process_incoming_packets" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_timing_2_ignore_calculation" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_timing_3_to_byte_array" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_timing_4_avatar_data_packing" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_timing_5_packet_sending" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_slaves_aggregate_per_frame_timing_6_job_elapsed_time" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_threads" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_throttling_ratio" , DomainServerExporter::MetricType::Gauge }, + { "avatar_mixer_trailing_mix_ratio" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_io_stats_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_io_stats_inbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_io_stats_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_io_stats_outbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_nodes_inbound_kbit_s" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_nodes_outbound_kbit_s" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_nodes_reliable_packet_s" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_nodes_unreliable_packet_s" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_octree_stats_element_count" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_octree_stats_internal_element_count" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_octree_stats_leaf_element_count" , DomainServerExporter::MetricType::Gauge }, + { "entity_script_server_script_engine_stats_number_running_scripts" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_data_packet_queue" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_data_total_elements" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_data_total_packets" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_timing_avg_lock_wait_time_per_element" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_timing_avg_lock_wait_time_per_packet" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_timing_avg_process_time_per_element" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_timing_avg_process_time_per_packet" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_inbound_timing_avg_transit_time_per_packet" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_clients" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_persist_file_load_time_seconds" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_threads_handle_pacekt_send" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_threads_packet_distributor" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_threads_processing" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_threads_write_datagram" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_misc_uptime_seconds" , DomainServerExporter::MetricType::Counter }, + { "entity_server_entity_server_octree_element_count" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_octree_internal_element_count" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_octree_leaf_element_count" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_data_total_bytes" , DomainServerExporter::MetricType::Counter }, + { "entity_server_entity_server_outbound_data_total_bytes_bit_masks" , DomainServerExporter::MetricType::Counter }, + { "entity_server_entity_server_outbound_data_total_bytes_octal_codes" , DomainServerExporter::MetricType::Counter }, + { "entity_server_entity_server_outbound_data_total_bytes_wasted" , DomainServerExporter::MetricType::Counter }, + { "entity_server_entity_server_outbound_data_total_packets" , DomainServerExporter::MetricType::Counter }, + { "entity_server_entity_server_outbound_timing_avg_compress_and_write_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_timing_avg_encode_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_timing_avg_inside_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_timing_avg_loop_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_timing_avg_send_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_timing_avg_tree_traverse_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_entity_server_outbound_timing_node_wait_time" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_io_stats_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_io_stats_inbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_io_stats_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "entity_server_io_stats_outbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_assignment_stats_num_queued_check_ins" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_io_stats_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_io_stats_inbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_io_stats_outbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_io_stats_outbound_pps" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_messages_inbound_kbps" , DomainServerExporter::MetricType::Gauge }, + { "messages_mixer_messages_outbound_kbps" , DomainServerExporter::MetricType::Gauge } +}; + + +// Things we're not going to convert for various reasons, such as containing text, +// or having a value followed by an unit ("5.2 seconds"). +// +// Things like text like usernames have no place in the Prometheus model, so they can be skipped. +// +// For numeric values with an unit, instead of trying to parse it, the stats will just need to +// have a second copy of the metric added, with the value expressed as a number, with the original +// being blacklisted here. + +static const QSet BLACKLIST = { + "asset_server_connection_stats_last_heard", // Timestamp as a string + "asset_server_username", // Username + "audio_mixer_listeners_jitter_downstream_avg_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_avg_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_max_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_max_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_min_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_downstream_min_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_injectors", // Array, empty. TODO: check if this ever contains anything. + "audio_mixer_listeners_jitter_upstream", // Only exists in the absence of a connection + "audio_mixer_listeners_jitter_upstream_avg_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_avg_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_max_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_max_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_min_gap", // Number as string with unit name, alternative added + "audio_mixer_listeners_jitter_upstream_min_gap_30s", // Number as string with unit name, alternative added + "audio_mixer_listeners_username", // Username + "avatar_mixer_avatars_display_name", // Username + "avatar_mixer_avatars_username", // Username + "entity_script_server_nodes_node_type", // Username + "entity_script_server_nodes_username", // Username + "entity_server_entity_server_misc_configuration", // Text + "entity_server_entity_server_misc_detailed_stats_url", // URL + "entity_server_entity_server_misc_persist_file_load_time", // Number as string with unit name, alternative added + "entity_server_entity_server_misc_uptime", // Number as string with unit name, alternative added + "messages_mixer_messages_username" // Username +}; + +DomainServerExporter::DomainServerExporter() { +} + +bool DomainServerExporter::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) { + const QString URI_METRICS = "/metrics"; + const QString EXPORTER_MIME_TYPE = "text/plain"; + + qCDebug(domain_server_exporter) << "Request on URL " << url; + + if (url.path() == URI_METRICS) { + auto nodeList = DependencyManager::get(); + QString output = ""; + QTextStream outStream(&output); + + nodeList->eachNode([this, &outStream](const SharedNodePointer& node) { generateMetricsForNode(outStream, node); }); + + connection->respond(HTTPConnection::StatusCode200, output.toUtf8(), qPrintable(EXPORTER_MIME_TYPE)); + return true; + } + + return false; +} + +QString DomainServerExporter::escapeName(const QString& name) { + QRegularExpression invalidCharacters("[^A-Za-z0-9_]"); + + QString result = name; + + // If a key is named something like: "6. threads", turn it into just "threads" + result.replace(QRegularExpression("^\\d+\\. "), ""); + result.replace(QRegularExpression("^\\d+_"), ""); + + // If a key is named something like "z_listeners", turn it into just "listeners" + result.replace(QRegularExpression("^z_"), ""); + + // If a key is named something like "lost%", change it to "lost_percent_". + // redundant underscores will be removed below. + result.replace(QRegularExpression("%"), "_percent_"); + + // change mixedCaseNames to mixed_case_names + result.replace(QRegularExpression("([a-z])([A-Z])"), "\\1_\\2"); + + // Replace all invalid characters with a _ + result.replace(invalidCharacters, "_"); + + // Remove any "_" characters at the beginning or end + result.replace(QRegularExpression("^_+"), ""); + result.replace(QRegularExpression("_+$"), ""); + + // Replace any duplicated _ characters with a single one + result.replace(QRegularExpression("_+"), "_"); + + result = result.toLower(); + + return result; +} + +void DomainServerExporter::generateMetricsForNode(QTextStream& stream, const SharedNodePointer& node) { + QJsonObject statsObject = static_cast(node->getLinkedData())->getStatsJSONObject(); + QString nodeType = NodeType::getNodeTypeName(static_cast(node->getType())); + + stream << "\n\n\n"; + stream << "###############################################################\n"; + stream << "# " << nodeType << "\n"; + stream << "###############################################################\n"; + + generateMetricsFromJson(stream, nodeType, escapeName(nodeType), QHash(), statsObject); +} + +void DomainServerExporter::generateMetricsFromJson(QTextStream& stream, + QString originalPath, + QString path, + QHash labels, + const QJsonObject& root) { + for (auto iter = root.constBegin(); iter != root.constEnd(); ++iter) { + auto escapedKey = escapeName(iter.key()); + auto metricValue = iter.value(); + auto metricName = path + "_" + escapedKey; + auto origMetricName = originalPath + " -> " + iter.key(); + + if (metricValue.isObject()) { + QUuid possible_uuid = QUuid::fromString(iter.key()); + + if (possible_uuid.isNull()) { + generateMetricsFromJson(stream, originalPath + " -> " + iter.key(), path + "_" + escapedKey, labels, + iter.value().toObject()); + } else { + labels.insert("uuid", possible_uuid.toString(QUuid::WithoutBraces)); + generateMetricsFromJson(stream, originalPath, path, labels, iter.value().toObject()); + } + + continue; + } + + if (BLACKLIST.contains(metricName)) { + continue; + } + + bool conversionOk = false; + double converted = 0; + + if (metricValue.isString()) { + // Prometheus only deals with numeric values. See if this string contains a valid one + + QString tmp = metricValue.toString(); + converted = tmp.toDouble(&conversionOk); + + if (!conversionOk) { + qCWarning(domain_server_exporter) << "Failed to convert value of " << origMetricName << " (" << metricName + << ") to double: " << tmp << "'"; + continue; + } + } + + stream << QString("\n# HELP %1 %2 -> %3\n").arg(metricName).arg(originalPath).arg(iter.key()); + + if (TYPE_MAP.contains(metricName)) { + stream << "# TYPE " << metricName << " "; + switch (TYPE_MAP[metricName]) { + case DomainServerExporter::MetricType::Untyped: + stream << "untyped"; + break; + case DomainServerExporter::MetricType::Counter: + stream << "counter"; + break; + case DomainServerExporter::MetricType::Gauge: + stream << "gauge"; + break; + case DomainServerExporter::MetricType::Histogram: + stream << "histogram"; + break; + case DomainServerExporter::MetricType::Summary: + stream << "summary"; + break; + } + stream << "\n"; + } else { + qCWarning(domain_server_exporter) + << "Type for metric " << origMetricName << " (" << metricName << ") not known."; + } + + stream << path << "_" << escapedKey; + if (!labels.isEmpty()) { + stream << "{"; + + bool isFirst = true; + QHashIterator iter(labels); + + while (iter.hasNext()) { + iter.next(); + + if (!isFirst) { + stream << ","; + } + + QString escapedValue = iter.value(); + escapedValue.replace("\\", "\\\\"); + escapedValue.replace("\"", "\\\""); + escapedValue.replace("\n", "\\\n"); + + stream << iter.key() << "=\"" << escapedValue << "\""; + + isFirst = false; + } + stream << "}"; + } + + stream << " "; + + if (metricValue.isBool()) { + stream << (iter.value().toBool() ? "1" : "0"); + } else if (metricValue.isDouble()) { + stream << metricValue.toDouble(); + } else if (metricValue.isString()) { + // Converted above + stream << converted; + } else { + qCWarning(domain_server_exporter) + << "Can't convert metric " << origMetricName << "(" << metricName << ") with value " << metricValue; + } + + stream << "\n"; + } +} diff --git a/domain-server/src/DomainServerExporter.h b/domain-server/src/DomainServerExporter.h new file mode 100644 index 0000000000..500532621a --- /dev/null +++ b/domain-server/src/DomainServerExporter.h @@ -0,0 +1,55 @@ +// +// DomainServerExporter.h +// domain-server/src +// +// Created by Dale Glass on 3 Apr 2020. +// Copyright 2020 Dale Glass +// +// Prometheus exporter +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef DOMAINSERVEREXPORTER_H +#define DOMAINSERVEREXPORTER_H + +#include +#include "HTTPManager.h" +#include "Node.h" +#include +#include +#include +#include + + + +/** + * @brief Prometheus exporter for domain stats + * + * This class exportors the statistics that can be seen on the domain's page in + * a format that can be parsed by Prometheus. This is useful for troubleshooting, + * monitoring performance, and making pretty graphs. + */ +class DomainServerExporter : public HTTPRequestHandler +{ +public: + typedef enum { + Untyped, /* Works the same as Gauge, with the difference of signalling that the actual type is unknown */ + Counter, /* Value only goes up. Eg, number of packets received */ + Gauge, /* Current numerical value that can go up or down. Current temperature, memory usage, etc */ + Histogram, /* Samples sorted in buckets gathered over time */ + Summary + } MetricType; + + DomainServerExporter(); + ~DomainServerExporter() = default; + bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override; + +private: + QString escapeName(const QString &name); + void generateMetricsForNode(QTextStream& stream, const SharedNodePointer& node); + void generateMetricsFromJson(QTextStream& stream, QString originalPath, QString path, QHash labels, const QJsonObject& obj); +}; + +#endif // DOMAINSERVEREXPORTER_H diff --git a/interface/resources/images/Loading-Inner-H.png b/interface/resources/images/Loading-Inner-H.png index 93369d61c8..06359f0c35 100644 Binary files a/interface/resources/images/Loading-Inner-H.png and b/interface/resources/images/Loading-Inner-H.png differ diff --git a/interface/resources/images/about-projectathena.png b/interface/resources/images/about-projectathena.png deleted file mode 100644 index ae2d2bcd06..0000000000 Binary files a/interface/resources/images/about-projectathena.png and /dev/null differ diff --git a/interface/resources/images/about-vircadia.png b/interface/resources/images/about-vircadia.png index 14ae73b226..38ece8a154 100644 Binary files a/interface/resources/images/about-vircadia.png and b/interface/resources/images/about-vircadia.png differ diff --git a/interface/resources/images/hifi-logo-blackish.svg b/interface/resources/images/hifi-logo-blackish.svg deleted file mode 100644 index 60bfb3d418..0000000000 --- a/interface/resources/images/hifi-logo-blackish.svg +++ /dev/null @@ -1,123 +0,0 @@ - - - -image/svg+xml \ No newline at end of file diff --git a/interface/resources/images/hifi-logo.svg b/interface/resources/images/hifi-logo.svg deleted file mode 100644 index e5d66d8f18..0000000000 --- a/interface/resources/images/hifi-logo.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/images/project-athena-banner-color2.svg b/interface/resources/images/project-athena-banner-color2.svg deleted file mode 100644 index b41a980fe0..0000000000 --- a/interface/resources/images/project-athena-banner-color2.svg +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - image/svg+xml - - Artboard 1 - - - - - - - - - - - - - - - - - - - Artboard 1 - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/images/vircadia-banner.svg b/interface/resources/images/vircadia-banner.svg new file mode 100644 index 0000000000..b53dd79040 --- /dev/null +++ b/interface/resources/images/vircadia-banner.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/images/vircadia-logo.svg b/interface/resources/images/vircadia-logo.svg index b53dd79040..10645c4120 100644 --- a/interface/resources/images/vircadia-logo.svg +++ b/interface/resources/images/vircadia-logo.svg @@ -1,23 +1,60 @@ - - - - - - - - - - - - - - - - - - - - - - - + +image/svg+xml + + + + + + + + + + + \ No newline at end of file diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index 375d68ad26..a27bf0c3bb 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -3,10 +3,10 @@ // // Created by David Rowe on 3 Jun 2015 // Copyright 2015 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. +// // Distributed under the Apache License, Version 2.0. -// // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// import Hifi 1.0 import QtQuick 2.4 @@ -87,7 +87,7 @@ FocusScope { anchors.centerIn: parent sourceSize.width: 500 sourceSize.height: 91 - source: "../images/vircadia-logo.svg" + source: "../images/vircadia-banner.svg" horizontalAlignment: Image.AlignHCenter } } diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 4f0ea2e607..571b7e074c 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -3,6 +3,7 @@ // // Created by Clement on 7/18/16 // Copyright 2015 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -486,6 +487,7 @@ Item { } } } + Item { id: signUpContainer width: loginContainer.width @@ -494,7 +496,7 @@ Item { anchors { left: loginContainer.left top: loginContainer.bottom - topMargin: 0.15 * parent.height + topMargin: 0.05 * parent.height } TextMetrics { id: signUpTextMetrics @@ -542,37 +544,54 @@ Item { "errorString": "" }); } } - } - TextMetrics { - id: dismissButtonTextMetrics - font: loginErrorMessage.font - text: dismissButton.text - } - HifiControlsUit.Button { - id: dismissButton - width: dismissButtonTextMetrics.width - height: d.minHeightButton - anchors { - bottom: parent.bottom - right: parent.right - margins: 3 * hifi.dimensions.contentSpacing.y - } - color: hifi.buttons.noneBorderlessWhite - text: qsTr("No thanks, take me in-world! >") - fontCapitalization: Font.MixedCase - fontFamily: linkAccountBody.fontFamily - fontSize: linkAccountBody.fontSize - fontBold: linkAccountBody.fontBold - visible: loginDialog.getLoginDialogPoppedUp() && !linkAccountBody.linkSteam && !linkAccountBody.linkOculus; - onClicked: { - if (linkAccountBody.loginDialogPoppedUp) { - var data = { - "action": "user dismissed login screen" - }; - UserActivityLogger.logAction("encourageLoginDialog", data); - loginDialog.dismissLoginDialog(); + + Text { + id: signUpTextSecond + text: qsTr("or") + anchors { + left: signUpShortcutText.right + leftMargin: hifi.dimensions.contentSpacing.x + } + lineHeight: 1 + color: "white" + font.family: linkAccountBody.fontFamily + font.pixelSize: linkAccountBody.textFieldFontSize + font.bold: linkAccountBody.fontBold + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + visible: loginDialog.getLoginDialogPoppedUp() && !linkAccountBody.linkSteam && !linkAccountBody.linkOculus; + } + + TextMetrics { + id: dismissButtonTextMetrics + font: loginErrorMessage.font + text: dismissButton.text + } + HifiControlsUit.Button { + id: dismissButton + width: loginButton.width + height: d.minHeightButton + anchors { + top: signUpText.bottom + topMargin: hifi.dimensions.contentSpacing.y + left: loginButton.left + } + text: qsTr("Use without account, log in anonymously") + fontCapitalization: Font.MixedCase + fontFamily: linkAccountBody.fontFamily + fontSize: linkAccountBody.fontSize + fontBold: linkAccountBody.fontBold + visible: loginDialog.getLoginDialogPoppedUp() && !linkAccountBody.linkSteam && !linkAccountBody.linkOculus; + onClicked: { + if (linkAccountBody.loginDialogPoppedUp) { + var data = { + "action": "user dismissed login screen" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + loginDialog.dismissLoginDialog(); + } + root.tryDestroy(); } - root.tryDestroy(); } } } diff --git a/interface/resources/qml/OverlayLoginDialog.qml b/interface/resources/qml/OverlayLoginDialog.qml index 466585f045..5fb72f2689 100644 --- a/interface/resources/qml/OverlayLoginDialog.qml +++ b/interface/resources/qml/OverlayLoginDialog.qml @@ -83,7 +83,7 @@ FocusScope { anchors.centerIn: parent sourceSize.width: 500 sourceSize.height: 91 - source: "../images/vircadia-logo.svg" + source: "../images/vircadia-banner.svg" horizontalAlignment: Image.AlignHCenter } } diff --git a/interface/resources/qml/UpdateDialog.qml b/interface/resources/qml/UpdateDialog.qml index 9c22d0b65b..c3a7a45c69 100644 --- a/interface/resources/qml/UpdateDialog.qml +++ b/interface/resources/qml/UpdateDialog.qml @@ -47,7 +47,7 @@ ScrollingWindow { Image { id: logo - source: "../images/hifi-logo.svg" + source: "../images/vircadia-logo.svg" width: updateDialog.logoSize height: updateDialog.logoSize anchors { diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index 533fd1197c..f794fea66a 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -131,7 +131,7 @@ FocusScope { anchors.centerIn: parent sourceSize.width: 400 sourceSize.height: 73 - source: "../../images/vircadia-logo.svg" + source: "../../images/vircadia-banner.svg" horizontalAlignment: Image.AlignHCenter } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index edb862b023..128ef61c75 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -128,7 +128,7 @@ ShadowRectangle { } } - // FIXME: Link to a Project Athena version of the video. + // FIXME: Link to a Vircadias version of the video. /* RalewayButton { id: video diff --git a/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml b/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml index cbaa5c7b08..e7a9e0cef2 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml @@ -25,7 +25,7 @@ Rectangle { Image { width: 400; height: 73 fillMode: Image.PreserveAspectFit - source: "../../../images/vircadia-logo.svg" + source: "../../../images/vircadia-banner.svg" } Item { height: 30; width: 1 } Column { @@ -53,7 +53,7 @@ Rectangle { textFormat: Text.StyledText linkColor: "#00B4EF" color: "white" - text: "Vircadia Github." + text: "Vircadia Github." size: 20 onLinkActivated: { HiFiAbout.openUrl("https:/github.com/kasenvr/project-athena"); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 85c2d4abe0..05ddcd5bd3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -347,7 +347,6 @@ static const QString STANDARD_TO_ACTION_MAPPING_NAME = "Standard to Action"; static const QString NO_MOVEMENT_MAPPING_NAME = "Standard to Action (No Movement)"; static const QString NO_MOVEMENT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_nomovement.json"; -static const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; static const int INTERVAL_TO_CHECK_HMD_WORN_STATUS = 500; // milliseconds static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop"; static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin"; @@ -7620,7 +7619,7 @@ bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { QUrl scriptURL { scriptFilenameOrURL }; - if (scriptURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + if (scriptURL.host().endsWith(NetworkingConstants::MARKETPLACE_CDN_HOSTNAME)) { int startIndex = shortName.lastIndexOf('/') + 1; int endIndex = shortName.lastIndexOf('?'); shortName = shortName.mid(startIndex, endIndex - startIndex); @@ -7743,7 +7742,7 @@ bool Application::askToReplaceDomainContent(const QString& url) { const int MAX_CHARACTERS_PER_LINE = 90; if (DependencyManager::get()->getThisNodeCanReplaceContent()) { QUrl originURL { url }; - if (originURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + if (originURL.host().endsWith(NetworkingConstants::MARKETPLACE_CDN_HOSTNAME)) { // Create a confirmation dialog when this call is made static const QString infoText = simpleWordWrap("Your domain's content will be replaced with a new content set. " "If you want to save what you have now, create a backup before proceeding. For more information about backing up " diff --git a/interface/src/Application.h b/interface/src/Application.h index 198f5ef7cf..684ff6bdaa 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -223,9 +223,7 @@ public: bool getPreferStylusOverLaser() { return _preferStylusOverLaserSetting.get(); } void setPreferStylusOverLaser(bool value); - // FIXME: Remove setting completely or make available through JavaScript API? - //bool getPreferAvatarFingerOverStylus() { return _preferAvatarFingerOverStylusSetting.get(); } - bool getPreferAvatarFingerOverStylus() { return false; } + bool getPreferAvatarFingerOverStylus() { return _preferAvatarFingerOverStylusSetting.get(); } void setPreferAvatarFingerOverStylus(bool value); bool getMiniTabletEnabled() { return _miniTabletEnabledSetting.get(); } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index a3ef39f1e9..a1bb670837 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -799,19 +799,19 @@ Menu::Menu() { // Help > Vircadia Docs action = addActionToQMenuAndActionHash(helpMenu, "Online Documentation"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://docs.vircadia.dev/")); + QDesktopServices::openUrl(NetworkingConstants::HELP_DOCS_URL); }); // Help > Vircadia Forum /* action = addActionToQMenuAndActionHash(helpMenu, "Online Forums"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://forums.highfidelity.com/")); + QDesktopServices::openUrl(NetworkingConstants::HELP_FORUM_URL)); }); */ // Help > Scripting Reference action = addActionToQMenuAndActionHash(helpMenu, "Online Script Reference"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://apidocs.vircadia.dev/")); + QDesktopServices::openUrl(NetworkingConstants::HELP_SCRIPTING_REFERENCE_URL); }); addActionToQMenuAndActionHash(helpMenu, "Controls Reference", 0, qApp, SLOT(showHelp())); @@ -821,13 +821,13 @@ Menu::Menu() { // Help > Release Notes action = addActionToQMenuAndActionHash(helpMenu, "Release Notes"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://docs.vircadia.dev/release-notes.html")); + QDesktopServices::openUrl(NetworkingConstants::HELP_RELEASE_NOTES_URL); }); // Help > Report a Bug! action = addActionToQMenuAndActionHash(helpMenu, "Report a Bug!"); connect(action, &QAction::triggered, qApp, [] { - QDesktopServices::openUrl(QUrl("https://github.com/kasenvr/project-athena/issues")); + QDesktopServices::openUrl(NetworkingConstants::HELP_BUG_REPORT_URL); }); } diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 4c1e55fa1c..3e0d69f78b 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -95,7 +95,7 @@ public: static bool isValidNewProjectName(const QString& projectPath, const QString& projectName); static QString getDefaultProjectsPath() { - return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/Project Athena Projects"; + return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/Vircadia Projects"; } signals: diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 3f6d5ed408..ae476b8142 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -379,7 +379,7 @@ int main(int argc, const char* argv[]) { PROFILE_SYNC_END(startup, "app full ctor", ""); #if defined(Q_OS_LINUX) - app.setWindowIcon(QIcon(PathUtils::resourcesPath() + "images/hifi-logo.svg")); + app.setWindowIcon(QIcon(PathUtils::resourcesPath() + "images/vircadia-logo.svg")); #endif QTimer exitTimer; diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 4ce30063e2..9f8a8ec013 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -223,13 +223,11 @@ void setupPreferences() { preferences->addPreference(preference); } - /* - // FIXME: Remove setting completely or make available through JavaScript API? { auto getter = []()->bool { return qApp->getPreferAvatarFingerOverStylus(); }; auto setter = [](bool value) { qApp->setPreferAvatarFingerOverStylus(value); }; preferences->addPreference(new CheckPreference(UI_CATEGORY, "Prefer Avatar Finger Over Stylus", getter, setter)); - }*/ + } // Snapshots static const QString SNAPSHOTS { "Snapshots" }; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index d9ad82fb51..4e8c88560b 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -610,6 +610,14 @@ QString defaultAudioDeviceName(QAudio::Mode mode) { << " [" << deviceName << "] [" << "]"; #endif +#endif + +#ifdef Q_OS_LINUX + if ( mode == QAudio::AudioInput ) { + deviceName = QAudioDeviceInfo::defaultInputDevice().deviceName(); + } else { + deviceName = QAudioDeviceInfo::defaultOutputDevice().deviceName(); + } #endif return deviceName; } diff --git a/libraries/auto-updater/src/AutoUpdater.cpp b/libraries/auto-updater/src/AutoUpdater.cpp index 300a22983a..d8afac59b2 100644 --- a/libraries/auto-updater/src/AutoUpdater.cpp +++ b/libraries/auto-updater/src/AutoUpdater.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include AutoUpdater::AutoUpdater() : @@ -36,18 +37,15 @@ void AutoUpdater::checkForUpdate() { this->getLatestVersionData(); } -const QUrl BUILDS_XML_URL("https://highfidelity.com/builds.xml"); -const QUrl MASTER_BUILDS_XML_URL("https://highfidelity.com/dev-builds.xml"); - void AutoUpdater::getLatestVersionData() { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QUrl buildsURL; if (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable) { - buildsURL = BUILDS_XML_URL; + buildsURL = NetworkingConstants::BUILDS_XML_URL; } else if (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Master) { - buildsURL = MASTER_BUILDS_XML_URL; + buildsURL = NetworkingConstants::MASTER_BUILDS_XML_URL; } QNetworkRequest latestVersionRequest(buildsURL); diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index 1eb1a9fa1a..604a4f9c73 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -312,7 +312,10 @@ void UserInputMapper::update(float deltaTime) { Input::NamedVector UserInputMapper::getAvailableInputs(uint16 deviceID) const { Locker locker(_lock); auto iterator = _registeredDevices.find(deviceID); - return iterator->second->getAvailableInputs(); + if (iterator != _registeredDevices.end()) { + return iterator->second->getAvailableInputs(); + } + return Input::NamedVector(); } QVector UserInputMapper::getAllActions() const { @@ -366,7 +369,7 @@ bool UserInputMapper::triggerHapticPulse(float strength, float duration, control Locker locker(_lock); bool toReturn = false; for (const auto& device : _registeredDevices) { - toReturn = toReturn || device.second->triggerHapticPulse(strength, duration, hand); + toReturn = device.second->triggerHapticPulse(strength, duration, hand) || toReturn; } return toReturn; } @@ -1237,16 +1240,42 @@ void UserInputMapper::disableMapping(const Mapping::Pointer& mapping) { } void UserInputMapper::setActionState(Action action, float value, bool valid) { + Locker locker(_lock); _actionStates[toInt(action)] = value; _actionStatesValid[toInt(action)] = valid; } void UserInputMapper::deltaActionState(Action action, float delta, bool valid) { + Locker locker(_lock); _actionStates[toInt(action)] += delta; bool wasValid = _actionStatesValid[toInt(action)]; _actionStatesValid[toInt(action)] = wasValid & valid; } +float UserInputMapper::getActionState(Action action) const { + Locker locker(_lock); + + int index = toInt(action); + if (index >= 0 && index < _actionStates.size()) { + return _actionStates[index]; + } + + qCDebug(controllers) << "UserInputMapper::getActionState invalid action:" << index; + return 0.0f; +} + +bool UserInputMapper::getActionStateValid(Action action) const { + Locker locker(_lock); + + int index = toInt(action); + if (index >= 0 && index < _actionStatesValid.size()) { + return _actionStatesValid[index]; + } + + qCDebug(controllers) << "UserInputMapper::getActionStateValid invalid action:" << index; + return false; +} + } diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index cd44f3226c..79fcf6e64c 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -81,8 +81,8 @@ namespace controller { QVector getAllActions() const; QString getActionName(Action action) const; QString getStandardPoseName(uint16_t pose); - float getActionState(Action action) const { return _actionStates[toInt(action)]; } - bool getActionStateValid(Action action) const { return _actionStatesValid[toInt(action)]; } + float getActionState(Action action) const; + bool getActionStateValid(Action action) const; Pose getPoseState(Action action) const; int findAction(const QString& actionName) const; QVector getActionNames() const; diff --git a/libraries/embedded-webserver/src/HTTPManager.h b/libraries/embedded-webserver/src/HTTPManager.h index 597f6921cc..d25cde413e 100644 --- a/libraries/embedded-webserver/src/HTTPManager.h +++ b/libraries/embedded-webserver/src/HTTPManager.h @@ -34,7 +34,7 @@ class HTTPManager : public QTcpServer, public HTTPRequestHandler { public: /// Initializes the manager. HTTPManager(const QHostAddress& listenAddress, quint16 port, const QString& documentRoot, HTTPRequestHandler* requestHandler = nullptr); - + bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override; private slots: diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index fedda7a42e..1ce19033e5 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1446,14 +1446,17 @@ void EntityTree::addCertifiedEntityOnServer(EntityItemPointer entity) { entityList << entityItemID; // adds to list within hash because entityList is a reference. qCDebug(entities) << "Certificate ID" << certID << "belongs to" << entityItemID << "total" << entityList.size() << "entities."; } - // Delete an already-existing entity from the tree if it has the same + // Handle an already-existing entity from the tree if it has the same // CertificateID as the entity we're trying to add. if (!existingEntityItemID.isNull()) { qCDebug(entities) << "Certificate ID" << certID << "already exists on entity with ID" - << existingEntityItemID << ". Deleting existing entity."; - withWriteLock([&] { - deleteEntity(existingEntityItemID, true); - }); + << existingEntityItemID << ". No action will be taken to remove it."; + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. + // withWriteLock([&] { + // deleteEntity(existingEntityItemID, true); + // }); } } @@ -1527,10 +1530,13 @@ void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove continue; } qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString() - << "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << entityID; - withWriteLock([&] { - deleteEntity(entityID, true); - }); + << "doesn't match the current Domain ID" << thisDomainID << ". No action will be taken to remove it: " << entityID; + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. + // withWriteLock([&] { + // deleteEntity(entityID, true); + // }); } { QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock); @@ -1555,10 +1561,13 @@ void EntityTree::startChallengeOwnershipTimer(const EntityItemID& entityItemID) } }); connect(_challengeOwnershipTimeoutTimer, &QTimer::timeout, this, [=]() { - qCDebug(entities) << "Ownership challenge timed out, deleting entity" << entityItemID; - withWriteLock([&] { - deleteEntity(entityItemID, true); - }); + qCDebug(entities) << "Ownership challenge timed out for entity " << entityItemID << ". No action will be taken to remove it."; + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. + // withWriteLock([&] { + // deleteEntity(entityItemID, true); + // }); if (_challengeOwnershipTimeoutTimer) { _challengeOwnershipTimeoutTimer->stop(); _challengeOwnershipTimeoutTimer->deleteLater(); @@ -1650,10 +1659,13 @@ void EntityTree::sendChallengeOwnershipPacket(const QString& certID, const QStri QByteArray text = computeNonce(entityItemID, ownerKey); if (text == "") { - qCDebug(entities) << "CRITICAL ERROR: Couldn't compute nonce. Deleting entity..."; - withWriteLock([&] { - deleteEntity(entityItemID, true); - }); + qCDebug(entities) << "CRITICAL ERROR: Couldn't compute nonce. No action will be taken to remove this entity."; + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. + // withWriteLock([&] { + // deleteEntity(entityItemID, true); + // }); } else { qCDebug(entities) << "Challenging ownership of Cert ID" << certID; // 2. Send the nonce to the rezzing avatar's node @@ -1724,15 +1736,21 @@ void EntityTree::validatePop(const QString& certID, const EntityItemID& entityIt if (networkReply->error() == QNetworkReply::NoError) { if (!jsonObject["invalid_reason"].toString().isEmpty()) { - qCDebug(entities) << "invalid_reason not empty, deleting entity" << entityItemID; - withWriteLock([&] { - deleteEntity(entityItemID, true); - }); + qCDebug(entities) << "invalid_reason not empty, no action will be taken to delete entity" << entityItemID; + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. + // withWriteLock([&] { + // deleteEntity(entityItemID, true); + // }); } else if (jsonObject["transfer_status"].toArray().first().toString() == "failed") { - qCDebug(entities) << "'transfer_status' is 'failed', deleting entity" << entityItemID; - withWriteLock([&] { - deleteEntity(entityItemID, true); - }); + qCDebug(entities) << "'transfer_status' is 'failed', no action will be taken to delete entity" << entityItemID; + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. + // withWriteLock([&] { + // deleteEntity(entityItemID, true); + // }); } else { // Second, challenge ownership of the PoP cert // (ignore pending status; a failure will be cleaned up during DDV) @@ -1742,11 +1760,14 @@ void EntityTree::validatePop(const QString& certID, const EntityItemID& entityIt senderNode); } } else { - qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; deleting entity" << entityItemID + qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; no action will be taken to delete entity" << entityItemID << "More info:" << jsonObject; - withWriteLock([&] { - deleteEntity(entityItemID, true); - }); + // FIXME: All certificate checking needs to be moved to its own files, + // then the deletion settings need to have a toggle for domain owners + // and a setting to change the verification service provider. + // withWriteLock([&] { + // deleteEntity(entityItemID, true); + // }); } networkReply->deleteLater(); diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index fa4e3dc29b..d34b5cf090 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -211,6 +211,7 @@ void DomainHandler::setURLAndID(QUrl domainURL, QUuid domainID) { // if it's in the error state, reset and try again. if (_domainURL != domainURL || (_sockAddr.getPort() != domainPort && domainURL.scheme() == URL_SCHEME_HIFI) + || isServerless() // For reloading content in serverless domain. || _isInErrorState) { // re-set the domain info so that auth information is reloaded hardReset("Changing domain URL"); diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 68059fb158..178c56c34a 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -33,7 +33,7 @@ #include "NetworkingConstants.h" #include "MetaverseAPI.h" -const unsigned short DEFAULT_DOMAIN_SERVER_PORT = +const unsigned short DEFAULT_DOMAIN_SERVER_PORT = QProcessEnvironment::systemEnvironment() .contains("HIFI_DOMAIN_SERVER_PORT") ? QProcessEnvironment::systemEnvironment() @@ -41,7 +41,7 @@ const unsigned short DEFAULT_DOMAIN_SERVER_PORT = .toUShort() : 40102; -const unsigned short DEFAULT_DOMAIN_SERVER_DTLS_PORT = +const unsigned short DEFAULT_DOMAIN_SERVER_DTLS_PORT = QProcessEnvironment::systemEnvironment() .contains("HIFI_DOMAIN_SERVER_DTLS_PORT") ? QProcessEnvironment::systemEnvironment() @@ -49,7 +49,7 @@ const unsigned short DEFAULT_DOMAIN_SERVER_DTLS_PORT = .toUShort() : 40103; -const quint16 DOMAIN_SERVER_HTTP_PORT = +const quint16 DOMAIN_SERVER_HTTP_PORT = QProcessEnvironment::systemEnvironment() .contains("HIFI_DOMAIN_SERVER_HTTP_PORT") ? QProcessEnvironment::systemEnvironment() @@ -57,7 +57,7 @@ const quint16 DOMAIN_SERVER_HTTP_PORT = .toUInt() : 40100; -const quint16 DOMAIN_SERVER_HTTPS_PORT = +const quint16 DOMAIN_SERVER_HTTPS_PORT = QProcessEnvironment::systemEnvironment() .contains("HIFI_DOMAIN_SERVER_HTTPS_PORT") ? QProcessEnvironment::systemEnvironment() @@ -65,6 +65,15 @@ const quint16 DOMAIN_SERVER_HTTPS_PORT = .toUInt() : 40101; +const quint16 DOMAIN_SERVER_EXPORTER_PORT = + QProcessEnvironment::systemEnvironment() + .contains("VIRCADIA_DOMAIN_SERVER_EXPORTER_PORT") + ? QProcessEnvironment::systemEnvironment() + .value("VIRCADIA_DOMAIN_SERVER_EXPORTER_PORT") + .toUInt() + : 9703; + + const int MAX_SILENT_DOMAIN_SERVER_CHECK_INS = 5; class DomainHandler : public QObject { diff --git a/libraries/networking/src/MetaverseAPI.cpp b/libraries/networking/src/MetaverseAPI.cpp index 0fb0bcecad..73316ecda3 100644 --- a/libraries/networking/src/MetaverseAPI.cpp +++ b/libraries/networking/src/MetaverseAPI.cpp @@ -3,7 +3,7 @@ // libraries/networking/src // // Created by Kalila (kasenvr) on 2019-12-16. -// Copyright 2019 Project Athena +// Copyright 2019 Vircadia // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/libraries/networking/src/MetaverseAPI.h b/libraries/networking/src/MetaverseAPI.h index 423f465229..026f8d8b70 100644 --- a/libraries/networking/src/MetaverseAPI.h +++ b/libraries/networking/src/MetaverseAPI.h @@ -3,7 +3,7 @@ // libraries/networking/src // // Created by Kalila (kasenvr) on 2019-12-16. -// Copyright 2019 Project Athena +// Copyright 2019 Vircadia // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index 3bd84bc977..1d28205310 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -27,6 +27,28 @@ namespace NetworkingConstants { const QUrl METAVERSE_SERVER_URL_STABLE { "https://metaverse.highfidelity.com" }; const QUrl METAVERSE_SERVER_URL_STAGING { "https://staging-metaverse.vircadia.com" }; + + // Web Engine requests to this parent domain have an account authorization header added + const QString AUTH_HOSTNAME_BASE = "highfidelity.com"; + + const QUrl BUILDS_XML_URL("https://highfidelity.com/builds.xml"); + const QUrl MASTER_BUILDS_XML_URL("https://highfidelity.com/dev-builds.xml"); + + +#if USE_STABLE_GLOBAL_SERVICES + const QString ICE_SERVER_DEFAULT_HOSTNAME = "ice.highfidelity.com"; +#else + const QString ICE_SERVER_DEFAULT_HOSTNAME = "dev-ice.highfidelity.com"; +#endif + + const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; + + const QUrl HELP_DOCS_URL { "https://docs.vircadia.dev" }; + const QUrl HELP_FORUM_URL { "https://forums.vircadia.dev" }; + const QUrl HELP_SCRIPTING_REFERENCE_URL{ "https://apidocs.vircadia.dev/" }; + const QUrl HELP_RELEASE_NOTES_URL{ "https://docs.vircadia.dev/release-notes.html" }; + const QUrl HELP_BUG_REPORT_URL{ "https://github.com/kasenvr/project-athena/issues" }; + } const QString HIFI_URL_SCHEME_ABOUT = "about"; diff --git a/libraries/ui/src/ui/types/RequestFilters.cpp b/libraries/ui/src/ui/types/RequestFilters.cpp index 943dd02c29..9287559289 100644 --- a/libraries/ui/src/ui/types/RequestFilters.cpp +++ b/libraries/ui/src/ui/types/RequestFilters.cpp @@ -29,7 +29,7 @@ namespace { auto metaverseServerURL = MetaverseAPI::getCurrentMetaverseServerURL(); static const QStringList HF_HOSTS = { "highfidelity.com", "highfidelity.io", - metaverseServerURL.toString(), "metaverse.highfidelity.io" + metaverseServerURL.toString(), }; const auto& scheme = url.scheme(); const auto& host = url.host(); diff --git a/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp b/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp index 5c5b975676..0989d28d23 100644 --- a/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp +++ b/plugins/hifiLeapMotion/src/LeapMotionPlugin.cpp @@ -281,6 +281,7 @@ void LeapMotionPlugin::InputDevice::update(float deltaTime, const controller::In glm::vec3 pos; glm::quat rot; + glm::quat prevRot; if (_isLeapOnHMD) { auto jointPosition = joints[i].position; const glm::vec3 HMD_EYE_TO_LEAP_OFFSET = glm::vec3(0.0f, 0.0f, -0.09f); // Eyes to surface of Leap Motion. @@ -291,17 +292,24 @@ void LeapMotionPlugin::InputDevice::update(float deltaTime, const controller::In glm::quat jointOrientation = joints[i].orientation; jointOrientation = glm::quat(jointOrientation.w, -jointOrientation.x, -jointOrientation.z, -jointOrientation.y); rot = controllerToAvatarRotation * hmdSensorOrientation * jointOrientation; + + glm::quat prevJointOrientation = prevJoints[i].orientation; + prevJointOrientation = + glm::quat(prevJointOrientation.w, -prevJointOrientation.x, -prevJointOrientation.z, -prevJointOrientation.y); + prevRot = controllerToAvatarRotation * hmdSensorOrientation * prevJointOrientation; + } else { pos = controllerToAvatarRotation * (joints[i].position - leapMotionOffset); const glm::quat ZERO_HAND_ORIENTATION = glm::quat(glm::vec3(PI_OVER_TWO, PI, 0.0f)); rot = controllerToAvatarRotation * joints[i].orientation * ZERO_HAND_ORIENTATION; + prevRot = controllerToAvatarRotation * prevJoints[i].orientation * ZERO_HAND_ORIENTATION; } glm::vec3 linearVelocity, angularVelocity; if (i < prevJoints.size()) { linearVelocity = (pos - (prevJoints[i].position * METERS_PER_CENTIMETER)) / deltaTime; // m/s // quat log imaginary part points along the axis of rotation, with length of one half the angle of rotation. - glm::quat d = glm::log(rot * glm::inverse(prevJoints[i].orientation)); + glm::quat d = glm::log(rot * glm::inverse(prevRot)); angularVelocity = glm::vec3(d.x, d.y, d.z) / (0.5f * deltaTime); // radians/s } diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt index dcb2e39e1b..e80b2215bf 100644 --- a/plugins/openvr/CMakeLists.txt +++ b/plugins/openvr/CMakeLists.txt @@ -15,5 +15,7 @@ if (WIN32 AND (NOT USE_GLES)) include_hifi_library_headers(octree) target_openvr() + target_sranipal() + target_aristo() target_link_libraries(${TARGET_NAME} Winmm.lib) endif() diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 8aa7311de4..2c9eb296ab 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -13,6 +13,21 @@ #include #include +#ifdef _WIN32 +#pragma warning( push ) +#pragma warning( disable : 4091 ) +#pragma warning( disable : 4334 ) +#endif + +#include +#include +#include +#include + +#ifdef _WIN32 +#pragma warning( pop ) +#endif + #include #include #include @@ -37,6 +52,8 @@ #include #include +#include "OpenVrDisplayPlugin.h" + extern PoseData _nextSimPoseData; vr::IVRSystem* acquireOpenVrSystem(); @@ -62,6 +79,32 @@ static const int SECOND_FOOT = 1; static const int HIP = 2; static const int CHEST = 3; +enum ViveHandJointIndex { + HAND = 0, + THUMB_1, + THUMB_2, + THUMB_3, + THUMB_4, + INDEX_1, + INDEX_2, + INDEX_3, + INDEX_4, + MIDDLE_1, + MIDDLE_2, + MIDDLE_3, + MIDDLE_4, + RING_1, + RING_2, + RING_3, + RING_4, + PINKY_1, + PINKY_2, + PINKY_3, + PINKY_4, + + Size +}; + const char* ViveControllerManager::NAME { "OpenVR" }; const std::map TRACKING_RESULT_TO_STRING = { @@ -130,6 +173,51 @@ static glm::mat4 calculateResetMat() { return glm::mat4(); } +class ViveProEyeReadThread : public QThread { +public: + ViveProEyeReadThread() { + setObjectName("OpenVR ViveProEye Read Thread"); + } + void run() override { + while (!quit) { + ViveSR::anipal::Eye::EyeData eyeData; + int result = ViveSR::anipal::Eye::GetEyeData(&eyeData); + { + QMutexLocker locker(&eyeDataMutex); + eyeDataBuffer.getEyeDataResult = result; + if (result == ViveSR::Error::WORK) { + uint64_t leftValids = eyeData.verbose_data.left.eye_data_validata_bit_mask; + uint64_t rightValids = eyeData.verbose_data.right.eye_data_validata_bit_mask; + + eyeDataBuffer.leftDirectionValid = + (leftValids & (uint64_t)ViveSR::anipal::Eye::SINGLE_EYE_DATA_GAZE_DIRECTION_VALIDITY) > (uint64_t)0; + eyeDataBuffer.rightDirectionValid = + (rightValids & (uint64_t)ViveSR::anipal::Eye::SINGLE_EYE_DATA_GAZE_DIRECTION_VALIDITY) > (uint64_t)0; + eyeDataBuffer.leftOpennessValid = + (leftValids & (uint64_t)ViveSR::anipal::Eye::SINGLE_EYE_DATA_EYE_OPENNESS_VALIDITY) > (uint64_t)0; + eyeDataBuffer.rightOpennessValid = + (rightValids & (uint64_t)ViveSR::anipal::Eye::SINGLE_EYE_DATA_EYE_OPENNESS_VALIDITY) > (uint64_t)0; + + float *leftGaze = eyeData.verbose_data.left.gaze_direction_normalized.elem_; + float *rightGaze = eyeData.verbose_data.right.gaze_direction_normalized.elem_; + eyeDataBuffer.leftEyeGaze = glm::vec3(leftGaze[0], leftGaze[1], leftGaze[2]); + eyeDataBuffer.rightEyeGaze = glm::vec3(rightGaze[0], rightGaze[1], rightGaze[2]); + + eyeDataBuffer.leftEyeOpenness = eyeData.verbose_data.left.eye_openness; + eyeDataBuffer.rightEyeOpenness = eyeData.verbose_data.right.eye_openness; + } + } + } + } + + bool quit { false }; + + // mutex and buffer for moving data from this thread to the other one + QMutex eyeDataMutex; + EyeDataBuffer eyeDataBuffer; +}; + + static QString outOfRangeDataStrategyToString(ViveControllerManager::OutOfRangeDataStrategy strategy) { switch (strategy) { default: @@ -211,6 +299,81 @@ QString ViveControllerManager::configurationLayout() { return OPENVR_LAYOUT; } +bool isDeviceIndexActive(vr::IVRSystem*& system, uint32_t deviceIndex) { + if (!system) { + return false; + } + if (deviceIndex != vr::k_unTrackedDeviceIndexInvalid && + system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_Controller && + system->IsTrackedDeviceConnected(deviceIndex)) { + vr::EDeviceActivityLevel activityLevel = system->GetTrackedDeviceActivityLevel(deviceIndex); + if (activityLevel == vr::k_EDeviceActivityLevel_UserInteraction) { + return true; + } + } + return false; +} + +bool isHandControllerActive(vr::IVRSystem*& system, vr::ETrackedControllerRole deviceRole) { + if (!system) { + return false; + } + auto deviceIndex = system->GetTrackedDeviceIndexForControllerRole(deviceRole); + return isDeviceIndexActive(system, deviceIndex); +} + +bool areBothHandControllersActive(vr::IVRSystem*& system) { + return + isHandControllerActive(system, vr::TrackedControllerRole_LeftHand) && + isHandControllerActive(system, vr::TrackedControllerRole_RightHand); +} + + +void ViveControllerManager::enableGestureDetection() { + if (_viveCameraHandTracker) { + return; + } + if (!ViveSR::anipal::Eye::IsViveProEye()) { + return; + } + +// #define HAND_TRACKER_USE_EXTERNAL_TRANSFORM 1 + +#ifdef HAND_TRACKER_USE_EXTERNAL_TRANSFORM + UseExternalTransform(true); // camera hand tracker results are in HMD frame +#else + UseExternalTransform(false); // camera hand tracker results are in sensor frame +#endif + GestureOption options; // defaults are GestureBackendAuto and GestureModeSkeleton + GestureFailure gestureFailure = StartGestureDetection(&options); + switch (gestureFailure) { + case GestureFailureNone: + qDebug() << "StartGestureDetection success"; + _viveCameraHandTracker = true; + break; + case GestureFailureOpenCL: + qDebug() << "StartGestureDetection (Only on Windows) OpenCL is not supported on the machine"; + break; + case GestureFailureCamera: + qDebug() << "StartGestureDetection Start camera failed"; + break; + case GestureFailureInternal: + qDebug() << "StartGestureDetection Internal errors"; + break; + case GestureFailureCPUOnPC: + qDebug() << "StartGestureDetection CPU backend is not supported on Windows"; + break; + } +} + +void ViveControllerManager::disableGestureDetection() { + if (!_viveCameraHandTracker) { + return; + } + StopGestureDetection(); + _viveCameraHandTracker = false; +} + bool ViveControllerManager::activate() { InputPlugin::activate(); @@ -230,6 +393,28 @@ bool ViveControllerManager::activate() { auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; + + if (ViveSR::anipal::Eye::IsViveProEye()) { + qDebug() << "Vive Pro eye-tracking detected"; + + int error = ViveSR::anipal::Initial(ViveSR::anipal::Eye::ANIPAL_TYPE_EYE, NULL); + if (error == ViveSR::Error::WORK) { + _viveProEye = true; + qDebug() << "Successfully initialize Eye engine."; + } else if (error == ViveSR::Error::RUNTIME_NOT_FOUND) { + _viveProEye = false; + qDebug() << "please follows SRanipal SDK guide to install SR_Runtime first"; + } else { + _viveProEye = false; + qDebug() << "Failed to initialize Eye engine. please refer to ViveSR error code:" << error; + } + + if (_viveProEye) { + _viveProEyeReadThread = std::make_shared(); + _viveProEyeReadThread->start(QThread::HighPriority); + } + } + return true; } @@ -251,6 +436,13 @@ void ViveControllerManager::deactivate() { userInputMapper->removeDevice(_inputDevice->_deviceID); _registeredWithInputMapper = false; + if (_viveProEyeReadThread) { + _viveProEyeReadThread->quit = true; + _viveProEyeReadThread->wait(); + _viveProEyeReadThread = nullptr; + ViveSR::anipal::Release(ViveSR::anipal::Eye::ANIPAL_TYPE_EYE); + } + saveSettings(); } @@ -262,6 +454,317 @@ bool ViveControllerManager::isHeadControllerMounted() const { return activityLevel == vr::k_EDeviceActivityLevel_UserInteraction; } +void ViveControllerManager::invalidateEyeInputs() { + _inputDevice->_poseStateMap[controller::LEFT_EYE].valid = false; + _inputDevice->_poseStateMap[controller::RIGHT_EYE].valid = false; + _inputDevice->_axisStateMap[controller::EYEBLINK_L].valid = false; + _inputDevice->_axisStateMap[controller::EYEBLINK_R].valid = false; +} + + +void ViveControllerManager::updateEyeTracker(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + if (!isHeadControllerMounted()) { + invalidateEyeInputs(); + return; + } + + EyeDataBuffer eyeDataBuffer; + { + // GetEyeData takes around 4ms to finish, so we run it on a thread. + QMutexLocker locker(&_viveProEyeReadThread->eyeDataMutex); + memcpy(&eyeDataBuffer, &_viveProEyeReadThread->eyeDataBuffer, sizeof(eyeDataBuffer)); + } + + if (eyeDataBuffer.getEyeDataResult != ViveSR::Error::WORK) { + invalidateEyeInputs(); + return; + } + + // only update from buffer values if the new data is "valid" + if (!eyeDataBuffer.leftDirectionValid) { + eyeDataBuffer.leftEyeGaze = _prevEyeData.leftEyeGaze; + eyeDataBuffer.leftDirectionValid = _prevEyeData.leftDirectionValid; + } + if (!eyeDataBuffer.rightDirectionValid) { + eyeDataBuffer.rightEyeGaze = _prevEyeData.rightEyeGaze; + eyeDataBuffer.rightDirectionValid = _prevEyeData.rightDirectionValid; + } + if (!eyeDataBuffer.leftOpennessValid) { + eyeDataBuffer.leftEyeOpenness = _prevEyeData.leftEyeOpenness; + eyeDataBuffer.leftOpennessValid = _prevEyeData.leftOpennessValid; + } + if (!eyeDataBuffer.rightOpennessValid) { + eyeDataBuffer.rightEyeOpenness = _prevEyeData.rightEyeOpenness; + eyeDataBuffer.rightOpennessValid = _prevEyeData.rightOpennessValid; + } + _prevEyeData = eyeDataBuffer; + + // transform data into what the controller system expects. + + // in the data from sranipal, left=+x, up=+y, forward=+z + mat4 localLeftEyeMat = glm::lookAt(vec3(0.0f, 0.0f, 0.0f), + glm::vec3(eyeDataBuffer.leftEyeGaze[0], + eyeDataBuffer.leftEyeGaze[1], + -eyeDataBuffer.leftEyeGaze[2]), + vec3(0.0f, 1.0f, 0.0f)); + quat localLeftEyeRot = glm::quat_cast(localLeftEyeMat); + quat avatarLeftEyeRot = _inputDevice->_poseStateMap[controller::HEAD].rotation * localLeftEyeRot; + + mat4 localRightEyeMat = glm::lookAt(vec3(0.0f, 0.0f, 0.0f), + glm::vec3(eyeDataBuffer.rightEyeGaze[0], + eyeDataBuffer.rightEyeGaze[1], + -eyeDataBuffer.rightEyeGaze[2]), + vec3(0.0f, 1.0f, 0.0f)); + quat localRightEyeRot = glm::quat_cast(localRightEyeMat); + quat avatarRightEyeRot = _inputDevice->_poseStateMap[controller::HEAD].rotation * localRightEyeRot; + + // TODO -- figure out translations for eyes + if (eyeDataBuffer.leftDirectionValid) { + _inputDevice->_poseStateMap[controller::LEFT_EYE] = controller::Pose(glm::vec3(), avatarLeftEyeRot); + _inputDevice->_poseStateMap[controller::LEFT_EYE].valid = true; + } else { + _inputDevice->_poseStateMap[controller::LEFT_EYE].valid = false; + } + if (eyeDataBuffer.rightDirectionValid) { + _inputDevice->_poseStateMap[controller::RIGHT_EYE] = controller::Pose(glm::vec3(), avatarRightEyeRot); + _inputDevice->_poseStateMap[controller::RIGHT_EYE].valid = true; + } else { + _inputDevice->_poseStateMap[controller::RIGHT_EYE].valid = false; + } + + quint64 now = usecTimestampNow(); + + // in hifi, 0 is open 1 is closed. in SRanipal 1 is open, 0 is closed. + if (eyeDataBuffer.leftOpennessValid) { + _inputDevice->_axisStateMap[controller::EYEBLINK_L] = + controller::AxisValue(1.0f - eyeDataBuffer.leftEyeOpenness, now); + } else { + _inputDevice->_poseStateMap[controller::EYEBLINK_L].valid = false; + } + if (eyeDataBuffer.rightOpennessValid) { + _inputDevice->_axisStateMap[controller::EYEBLINK_R] = + controller::AxisValue(1.0f - eyeDataBuffer.rightEyeOpenness, now); + } else { + _inputDevice->_poseStateMap[controller::EYEBLINK_R].valid = false; + } +} + +glm::vec3 ViveControllerManager::getRollingAverageHandPoint(int handIndex, int pointIndex) const { +#if 0 + return _handPoints[0][handIndex][pointIndex]; +#else + glm::vec3 result; + for (int s = 0; s < NUMBER_OF_HAND_TRACKER_SMOOTHING_FRAMES; s++) { + result += _handPoints[s][handIndex][pointIndex]; + } + return result / NUMBER_OF_HAND_TRACKER_SMOOTHING_FRAMES; +#endif +} + + +controller::Pose ViveControllerManager::trackedHandDataToPose(int hand, const glm::vec3& palmFacing, + int nearHandPositionIndex, int farHandPositionIndex) { + glm::vec3 nearPoint = getRollingAverageHandPoint(hand, nearHandPositionIndex); + + glm::quat poseRot; + if (nearHandPositionIndex != farHandPositionIndex) { + glm::vec3 farPoint = getRollingAverageHandPoint(hand, farHandPositionIndex); + + glm::vec3 pointingDir = farPoint - nearPoint; // y axis + glm::vec3 otherAxis = glm::cross(pointingDir, palmFacing); + + glm::mat4 rotMat; + rotMat = glm::mat4(glm::vec4(otherAxis, 0.0f), + glm::vec4(pointingDir, 0.0f), + glm::vec4(palmFacing * (hand == 0 ? 1.0f : -1.0f), 0.0f), + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + poseRot = glm::normalize(glmExtractRotation(rotMat)); + } + + if (!isNaN(poseRot)) { + controller::Pose pose(nearPoint, poseRot); + return pose; + } else { + controller::Pose pose; + pose.valid = false; + return pose; + } +} + + +void ViveControllerManager::trackFinger(int hand, int jointIndex1, int jointIndex2, int jointIndex3, int jointIndex4, + controller::StandardPoseChannel joint1, controller::StandardPoseChannel joint2, + controller::StandardPoseChannel joint3, controller::StandardPoseChannel joint4) { + + glm::vec3 point1 = getRollingAverageHandPoint(hand, jointIndex1); + glm::vec3 point2 = getRollingAverageHandPoint(hand, jointIndex2); + glm::vec3 point3 = getRollingAverageHandPoint(hand, jointIndex3); + glm::vec3 point4 = getRollingAverageHandPoint(hand, jointIndex4); + + glm::vec3 wristPos = getRollingAverageHandPoint(hand, ViveHandJointIndex::HAND); + glm::vec3 thumb2 = getRollingAverageHandPoint(hand, ViveHandJointIndex::THUMB_2); + glm::vec3 pinkie1 = getRollingAverageHandPoint(hand, ViveHandJointIndex::PINKY_1); + + // 1st + glm::vec3 palmFacing = glm::normalize(glm::cross(pinkie1 - wristPos, thumb2 - wristPos)); + glm::vec3 handForward = glm::normalize(point1 - wristPos); + glm::vec3 x = glm::normalize(glm::cross(palmFacing, handForward)); + glm::vec3 y = glm::normalize(point2 - point1); + glm::vec3 z = (hand == 0) ? glm::cross(y, x) : glm::cross(x, y); + glm::mat4 rotMat1 = glm::mat4(glm::vec4(x, 0.0f), + glm::vec4(y, 0.0f), + glm::vec4(z, 0.0f), + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + glm::quat rot1 = glm::normalize(glmExtractRotation(rotMat1)); + if (!isNaN(rot1)) { + _inputDevice->_poseStateMap[joint1] = controller::Pose(point1, rot1); + } + + + // 2nd + glm::vec3 x2 = x; // glm::normalize(glm::cross(point3 - point2, point2 - point1)); + glm::vec3 y2 = glm::normalize(point3 - point2); + glm::vec3 z2 = (hand == 0) ? glm::cross(y2, x2) : glm::cross(x2, y2); + + glm::mat4 rotMat2 = glm::mat4(glm::vec4(x2, 0.0f), + glm::vec4(y2, 0.0f), + glm::vec4(z2, 0.0f), + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + glm::quat rot2 = glm::normalize(glmExtractRotation(rotMat2)); + if (!isNaN(rot2)) { + _inputDevice->_poseStateMap[joint2] = controller::Pose(point2, rot2); + } + + + // 3rd + glm::vec3 x3 = x; // glm::normalize(glm::cross(point4 - point3, point3 - point1)); + glm::vec3 y3 = glm::normalize(point4 - point3); + glm::vec3 z3 = (hand == 0) ? glm::cross(y3, x3) : glm::cross(x3, y3); + + glm::mat4 rotMat3 = glm::mat4(glm::vec4(x3, 0.0f), + glm::vec4(y3, 0.0f), + glm::vec4(z3, 0.0f), + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + glm::quat rot3 = glm::normalize(glmExtractRotation(rotMat3)); + if (!isNaN(rot3)) { + _inputDevice->_poseStateMap[joint3] = controller::Pose(point3, rot3); + } + + + // 4th + glm::quat rot4 = rot3; + if (!isNaN(rot4)) { + _inputDevice->_poseStateMap[joint4] = controller::Pose(point4, rot4); + } +} + + +void ViveControllerManager::updateCameraHandTracker(float deltaTime, + const controller::InputCalibrationData& inputCalibrationData) { + + if (areBothHandControllersActive(_system)) { + // if both hand-controllers are in use, don't do camera hand tracking + disableGestureDetection(); + } else { + enableGestureDetection(); + } + + if (!_viveCameraHandTracker) { + return; + } + + const GestureResult* results = NULL; + int handTrackerFrameIndex { -1 }; + int resultsHandCount = GetGestureResult(&results, &handTrackerFrameIndex); + + // FIXME: Why the commented-out condition? + if (handTrackerFrameIndex >= 0 /* && handTrackerFrameIndex != _lastHandTrackerFrameIndex */) { +#ifdef HAND_TRACKER_USE_EXTERNAL_TRANSFORM + glm::mat4 trackedHandToAvatar = + glm::inverse(inputCalibrationData.avatarMat) * + inputCalibrationData.sensorToWorldMat * + inputCalibrationData.hmdSensorMat; + // glm::mat4 trackedHandToAvatar = _inputDevice->_poseStateMap[controller::HEAD].getMatrix() * Matrices::Y_180; +#else + DisplayPluginPointer displayPlugin = _container->getActiveDisplayPlugin(); + std::shared_ptr openVRDisplayPlugin = + std::dynamic_pointer_cast(displayPlugin); + glm::mat4 sensorResetMatrix; + if (openVRDisplayPlugin) { + sensorResetMatrix = openVRDisplayPlugin->getSensorResetMatrix(); + } + + glm::mat4 trackedHandToAvatar = + glm::inverse(inputCalibrationData.avatarMat) * + inputCalibrationData.sensorToWorldMat * + sensorResetMatrix; +#endif + + // roll all the old points in the rolling average + memmove(&(_handPoints[1]), + &(_handPoints[0]), + sizeof(_handPoints[0]) * (NUMBER_OF_HAND_TRACKER_SMOOTHING_FRAMES - 1)); + + for (int handIndex = 0; handIndex < resultsHandCount; handIndex++) { + bool isLeftHand = results[handIndex].isLeft; + + vr::ETrackedControllerRole controllerRole = + isLeftHand ? vr::TrackedControllerRole_LeftHand : vr::TrackedControllerRole_RightHand; + if (isHandControllerActive(_system, controllerRole)) { + continue; // if the controller for this hand is tracked, ignore camera hand tracking + } + + int hand = isLeftHand ? 0 : 1; + for (int pointIndex = 0; pointIndex < NUMBER_OF_HAND_POINTS; pointIndex++) { + glm::vec3 pos(results[handIndex].points[3 * pointIndex], + results[handIndex].points[3 * pointIndex + 1], + -results[handIndex].points[3 * pointIndex + 2]); + _handPoints[0][hand][pointIndex] = transformPoint(trackedHandToAvatar, pos); + } + + glm::vec3 wristPos = getRollingAverageHandPoint(hand, ViveHandJointIndex::HAND); + glm::vec3 thumb2 = getRollingAverageHandPoint(hand, ViveHandJointIndex::THUMB_2); + glm::vec3 pinkie1 = getRollingAverageHandPoint(hand, ViveHandJointIndex::PINKY_1); + glm::vec3 palmFacing = glm::cross(pinkie1 - wristPos, thumb2 - wristPos); // z axis + + _inputDevice->_poseStateMap[isLeftHand ? controller::LEFT_HAND : controller::RIGHT_HAND] = + trackedHandDataToPose(hand, palmFacing, ViveHandJointIndex::HAND, ViveHandJointIndex::MIDDLE_1); + trackFinger(hand, ViveHandJointIndex::THUMB_1, ViveHandJointIndex::THUMB_2, ViveHandJointIndex::THUMB_3, + ViveHandJointIndex::THUMB_4, + isLeftHand ? controller::LEFT_HAND_THUMB1 : controller::RIGHT_HAND_THUMB1, + isLeftHand ? controller::LEFT_HAND_THUMB2 : controller::RIGHT_HAND_THUMB2, + isLeftHand ? controller::LEFT_HAND_THUMB3 : controller::RIGHT_HAND_THUMB3, + isLeftHand ? controller::LEFT_HAND_THUMB4 : controller::RIGHT_HAND_THUMB4); + trackFinger(hand, ViveHandJointIndex::INDEX_1, ViveHandJointIndex::INDEX_2, ViveHandJointIndex::INDEX_3, + ViveHandJointIndex::INDEX_4, + isLeftHand ? controller::LEFT_HAND_INDEX1 : controller::RIGHT_HAND_INDEX1, + isLeftHand ? controller::LEFT_HAND_INDEX2 : controller::RIGHT_HAND_INDEX2, + isLeftHand ? controller::LEFT_HAND_INDEX3 : controller::RIGHT_HAND_INDEX3, + isLeftHand ? controller::LEFT_HAND_INDEX4 : controller::RIGHT_HAND_INDEX4); + trackFinger(hand, ViveHandJointIndex::MIDDLE_1, ViveHandJointIndex::MIDDLE_2, ViveHandJointIndex::MIDDLE_3, + ViveHandJointIndex::MIDDLE_4, + isLeftHand ? controller::LEFT_HAND_MIDDLE1 : controller::RIGHT_HAND_MIDDLE1, + isLeftHand ? controller::LEFT_HAND_MIDDLE2 : controller::RIGHT_HAND_MIDDLE2, + isLeftHand ? controller::LEFT_HAND_MIDDLE3 : controller::RIGHT_HAND_MIDDLE3, + isLeftHand ? controller::LEFT_HAND_MIDDLE4 : controller::RIGHT_HAND_MIDDLE4); + trackFinger(hand, ViveHandJointIndex::RING_1, ViveHandJointIndex::RING_2, ViveHandJointIndex::RING_3, + ViveHandJointIndex::RING_4, + isLeftHand ? controller::LEFT_HAND_RING1 : controller::RIGHT_HAND_RING1, + isLeftHand ? controller::LEFT_HAND_RING2 : controller::RIGHT_HAND_RING2, + isLeftHand ? controller::LEFT_HAND_RING3 : controller::RIGHT_HAND_RING3, + isLeftHand ? controller::LEFT_HAND_RING4 : controller::RIGHT_HAND_RING4); + trackFinger(hand, ViveHandJointIndex::PINKY_1, ViveHandJointIndex::PINKY_2, ViveHandJointIndex::PINKY_3, + ViveHandJointIndex::PINKY_4, + isLeftHand ? controller::LEFT_HAND_PINKY1 : controller::RIGHT_HAND_PINKY1, + isLeftHand ? controller::LEFT_HAND_PINKY2 : controller::RIGHT_HAND_PINKY2, + isLeftHand ? controller::LEFT_HAND_PINKY3 : controller::RIGHT_HAND_PINKY3, + isLeftHand ? controller::LEFT_HAND_PINKY4 : controller::RIGHT_HAND_PINKY4); + } + } + _lastHandTrackerFrameIndex = handTrackerFrameIndex; +} + + void ViveControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { if (!_system) { @@ -297,6 +800,12 @@ void ViveControllerManager::pluginUpdate(float deltaTime, const controller::Inpu userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; } + + if (_viveProEye) { + updateEyeTracker(deltaTime, inputCalibrationData); + } + + updateCameraHandTracker(deltaTime, inputCalibrationData); } void ViveControllerManager::loadSettings() { @@ -830,9 +1339,7 @@ void ViveControllerManager::InputDevice::handleHmd(uint32_t deviceIndex, const c void ViveControllerManager::InputDevice::handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand) { - if (_system->IsTrackedDeviceConnected(deviceIndex) && - _system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_Controller && - _nextSimPoseData.vrPoses[deviceIndex].bPoseIsValid) { + if (isDeviceIndexActive(_system, deviceIndex) && _nextSimPoseData.vrPoses[deviceIndex].bPoseIsValid) { // process pose const mat4& mat = _nextSimPoseData.poses[deviceIndex]; @@ -1401,9 +1908,52 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI makePair(LEFT_GRIP, "LeftGrip"), makePair(RIGHT_GRIP, "RightGrip"), - // 3d location of controller + // 3d location of left controller and fingers makePair(LEFT_HAND, "LeftHand"), + makePair(LEFT_HAND_THUMB1, "LeftHandThumb1"), + makePair(LEFT_HAND_THUMB2, "LeftHandThumb2"), + makePair(LEFT_HAND_THUMB3, "LeftHandThumb3"), + makePair(LEFT_HAND_THUMB4, "LeftHandThumb4"), + makePair(LEFT_HAND_INDEX1, "LeftHandIndex1"), + makePair(LEFT_HAND_INDEX2, "LeftHandIndex2"), + makePair(LEFT_HAND_INDEX3, "LeftHandIndex3"), + makePair(LEFT_HAND_INDEX4, "LeftHandIndex4"), + makePair(LEFT_HAND_MIDDLE1, "LeftHandMiddle1"), + makePair(LEFT_HAND_MIDDLE2, "LeftHandMiddle2"), + makePair(LEFT_HAND_MIDDLE3, "LeftHandMiddle3"), + makePair(LEFT_HAND_MIDDLE4, "LeftHandMiddle4"), + makePair(LEFT_HAND_RING1, "LeftHandRing1"), + makePair(LEFT_HAND_RING2, "LeftHandRing2"), + makePair(LEFT_HAND_RING3, "LeftHandRing3"), + makePair(LEFT_HAND_RING4, "LeftHandRing4"), + makePair(LEFT_HAND_PINKY1, "LeftHandPinky1"), + makePair(LEFT_HAND_PINKY2, "LeftHandPinky2"), + makePair(LEFT_HAND_PINKY3, "LeftHandPinky3"), + makePair(LEFT_HAND_PINKY4, "LeftHandPinky4"), + + // 3d location of right controller and fingers makePair(RIGHT_HAND, "RightHand"), + makePair(RIGHT_HAND_THUMB1, "RightHandThumb1"), + makePair(RIGHT_HAND_THUMB2, "RightHandThumb2"), + makePair(RIGHT_HAND_THUMB3, "RightHandThumb3"), + makePair(RIGHT_HAND_THUMB4, "RightHandThumb4"), + makePair(RIGHT_HAND_INDEX1, "RightHandIndex1"), + makePair(RIGHT_HAND_INDEX2, "RightHandIndex2"), + makePair(RIGHT_HAND_INDEX3, "RightHandIndex3"), + makePair(RIGHT_HAND_INDEX4, "RightHandIndex4"), + makePair(RIGHT_HAND_MIDDLE1, "RightHandMiddle1"), + makePair(RIGHT_HAND_MIDDLE2, "RightHandMiddle2"), + makePair(RIGHT_HAND_MIDDLE3, "RightHandMiddle3"), + makePair(RIGHT_HAND_MIDDLE4, "RightHandMiddle4"), + makePair(RIGHT_HAND_RING1, "RightHandRing1"), + makePair(RIGHT_HAND_RING2, "RightHandRing2"), + makePair(RIGHT_HAND_RING3, "RightHandRing3"), + makePair(RIGHT_HAND_RING4, "RightHandRing4"), + makePair(RIGHT_HAND_PINKY1, "RightHandPinky1"), + makePair(RIGHT_HAND_PINKY2, "RightHandPinky2"), + makePair(RIGHT_HAND_PINKY3, "RightHandPinky3"), + makePair(RIGHT_HAND_PINKY4, "RightHandPinky4"), + makePair(LEFT_FOOT, "LeftFoot"), makePair(RIGHT_FOOT, "RightFoot"), makePair(HIPS, "Hips"), @@ -1411,6 +1961,10 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI makePair(HEAD, "Head"), makePair(LEFT_ARM, "LeftArm"), makePair(RIGHT_ARM, "RightArm"), + makePair(LEFT_EYE, "LeftEye"), + makePair(RIGHT_EYE, "RightEye"), + makePair(EYEBLINK_L, "EyeBlink_L"), + makePair(EYEBLINK_R, "EyeBlink_R"), // 16 tracked poses makePair(TRACKED_OBJECT_00, "TrackedObject00"), diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index dbd248dc53..714be87842 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -31,6 +31,23 @@ namespace vr { class IVRSystem; } +class ViveProEyeReadThread; + +class EyeDataBuffer { +public: + int getEyeDataResult { 0 }; + bool leftDirectionValid { false }; + bool rightDirectionValid { false }; + bool leftOpennessValid { false }; + bool rightOpennessValid { false }; + glm::vec3 leftEyeGaze; + glm::vec3 rightEyeGaze; + float leftEyeOpenness { 0.0f }; + float rightEyeOpenness { 0.0f }; +}; + + + class ViveControllerManager : public InputPlugin { Q_OBJECT public: @@ -49,12 +66,18 @@ public: bool isHeadController() const override { return true; } bool isHeadControllerMounted() const; + void enableGestureDetection(); + void disableGestureDetection(); + bool activate() override; void deactivate() override; QString getDeviceName() { return QString::fromStdString(_inputDevice->_headsetName); } void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } + void invalidateEyeInputs(); + void updateEyeTracker(float deltaTime, const controller::InputCalibrationData& inputCalibrationData); + void updateCameraHandTracker(float deltaTime, const controller::InputCalibrationData& inputCalibrationData); void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; virtual void saveSettings() const override; @@ -229,6 +252,23 @@ private: vr::IVRSystem* _system { nullptr }; std::shared_ptr _inputDevice { std::make_shared(_system) }; + bool _viveProEye { false }; + std::shared_ptr _viveProEyeReadThread; + EyeDataBuffer _prevEyeData; + + bool _viveCameraHandTracker { false }; + int _lastHandTrackerFrameIndex { -1 }; + + const static int NUMBER_OF_HAND_TRACKER_SMOOTHING_FRAMES { 6 }; + const static int NUMBER_OF_HAND_POINTS { 21 }; + glm::vec3 _handPoints[NUMBER_OF_HAND_TRACKER_SMOOTHING_FRAMES][2][NUMBER_OF_HAND_POINTS]; // 2 for number of hands + glm::vec3 getRollingAverageHandPoint(int handIndex, int pointIndex) const; + controller::Pose trackedHandDataToPose(int hand, const glm::vec3& palmFacing, + int nearHandPositionIndex, int farHandPositionIndex); + void trackFinger(int hand, int jointIndex1, int jointIndex2, int jointIndex3, int jointIndex4, + controller::StandardPoseChannel joint1, controller::StandardPoseChannel joint2, + controller::StandardPoseChannel joint3, controller::StandardPoseChannel joint4); + static const char* NAME; }; diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 5e7e120bf3..830785d89b 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -34,7 +34,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/miniTablet.js", "system/audioMuteOverlay.js", "system/inspect.js", - "system/keyboardShortcuts/keyboardShortcuts.js" + "system/keyboardShortcuts/keyboardShortcuts.js", + "system/hand-track-walk.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index f0d3ec0c03..5af86d3bbd 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -35,7 +35,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); var DEBUG = false; var SHOW_GRAB_SPHERE = false; - if (typeof Test !== "undefined") { PROFILE = true; } @@ -54,6 +53,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.pointerManager = new PointerManager(); this.grabSphereOverlays = [null, null]; this.targetIDs = {}; + this.debugPanelID = null; + this.debugLines = []; // a module can occupy one or more "activity" slots while it's running. If all the required slots for a module are // not set to false (not in use), a module cannot start. When a module is using a slot, that module's name @@ -97,11 +98,15 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); }; this.runningPluginNames = {}; + this.leftTriggerValue = 0; this.leftTriggerClicked = 0; + this.leftTrackerClicked = false; // is leftTriggerClicked == 1 because a hand tracker set it? + this.leftSecondaryValue = 0; + this.rightTriggerValue = 0; this.rightTriggerClicked = 0; - this.leftSecondaryValue = 0; + this.rightTrackerClicked = false; // is rightTriggerClicked == 1 because a hand tracker set it? this.rightSecondaryValue = 0; this.leftTriggerPress = function (value) { @@ -162,6 +167,38 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } }; + this.checkForHandTrackingClick = function() { + + var pinchOnBelowDistance = 0.016; + var pinchOffAboveDistance = 0.035; + + var leftIndexPose = Controller.getPoseValue(Controller.Standard.LeftHandIndex4); + var leftThumbPose = Controller.getPoseValue(Controller.Standard.LeftHandThumb4); + var leftThumbToIndexDistance = Vec3.distance(leftIndexPose.translation, leftThumbPose.translation); + if (leftIndexPose.valid && leftThumbPose.valid && leftThumbToIndexDistance < pinchOnBelowDistance) { + _this.leftTriggerClicked = 1; + _this.leftTriggerValue = 1; + _this.leftTrackerClicked = true; + } else if (_this.leftTrackerClicked && leftThumbToIndexDistance > pinchOffAboveDistance) { + _this.leftTriggerClicked = 0; + _this.leftTriggerValue = 0; + _this.leftTrackerClicked = false; + } + + var rightIndexPose = Controller.getPoseValue(Controller.Standard.RightHandIndex4); + var rightThumbPose = Controller.getPoseValue(Controller.Standard.RightHandThumb4); + var rightThumbToIndexDistance = Vec3.distance(rightIndexPose.translation, rightThumbPose.translation); + if (rightIndexPose.valid && rightThumbPose.valid && rightThumbToIndexDistance < pinchOnBelowDistance) { + _this.rightTriggerClicked = 1; + _this.rightTriggerValue = 1; + _this.rightTrackerClicked = true; + } else if (_this.rightTrackerClicked && rightThumbToIndexDistance > pinchOffAboveDistance) { + _this.rightTriggerClicked = 0; + _this.rightTriggerValue = 0; + _this.rightTrackerClicked = false; + } + }; + this.update = function () { try { _this.updateInternal(); @@ -171,6 +208,18 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Script.setTimeout(_this.update, BASIC_TIMER_INTERVAL_MS); }; + this.addDebugLine = function(line) { + if (this.debugLines.length > 8) { + this.debugLines.shift(); + } + this.debugLines.push(line); + var debugPanelText = ""; + this.debugLines.forEach(function(debugLine) { + debugPanelText += debugLine + "\n"; + }); + Entities.editEntity(this.debugPanelID, { text: debugPanelText }); + }; + this.updateInternal = function () { if (PROFILE) { Script.beginProfileRange("dispatch.pre"); @@ -274,6 +323,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } var nearbyEntityIDs = Entities.findEntities(controllerPosition, findRadius); + for (var j = 0; j < nearbyEntityIDs.length; j++) { var entityID = nearbyEntityIDs[j]; var props = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES); @@ -369,6 +419,9 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } } + // check for hand-tracking "click" + _this.checkForHandTrackingClick(); + // bundle up all the data about the current situation var controllerData = { triggerValues: [_this.leftTriggerValue, _this.rightTriggerValue], @@ -406,7 +459,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); _this.markSlots(candidatePlugin, orderedPluginName); _this.pointerManager.makePointerVisible(candidatePlugin.parameters.handLaser); if (DEBUG) { - print("controllerDispatcher running " + orderedPluginName); + _this.addDebugLine("running " + orderedPluginName); } } if (PROFILE) { @@ -438,8 +491,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); if (DEBUG) { if (JSON.stringify(_this.targetIDs[runningPluginName]) != JSON.stringify(runningness.targets)) { - print("controllerDispatcher targetIDs[" + runningPluginName + "] = " + - JSON.stringify(runningness.targets)); + _this.addDebugLine("targetIDs[" + runningPluginName + "] = " + + JSON.stringify(runningness.targets)); } } @@ -450,12 +503,12 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); delete _this.runningPluginNames[runningPluginName]; delete _this.targetIDs[runningPluginName]; if (DEBUG) { - print("controllerDispatcher deleted targetIDs[" + runningPluginName + "]"); + _this.addDebugLine("deleted targetIDs[" + runningPluginName + "]"); } _this.markSlots(plugin, false); _this.pointerManager.makePointerInvisible(plugin.parameters.handLaser); if (DEBUG) { - print("controllerDispatcher stopping " + runningPluginName); + _this.addDebugLine("stopping " + runningPluginName); } } _this.pointerManager.lockPointerEnd(plugin.parameters.handLaser, runningness.laserLockInfo); @@ -599,7 +652,33 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Overlays.mousePressOnOverlay.disconnect(mousePress); Entities.mousePressOnEntity.disconnect(mousePress); Messages.messageReceived.disconnect(controllerDispatcher.handleMessage); + if (_this.debugPanelID) { + Entities.deleteEntity(_this.debugPanelID); + _this.debugPanelID = null; + } }; + + if (DEBUG) { + this.debugPanelID = Entities.addEntity({ + name: "controllerDispatcher debug panel", + type: "Text", + dimensions: { x: 1.0, y: 0.3, z: 0.01 }, + parentID: MyAvatar.sessionUUID, + // parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX"), + parentJointIndex: -1, + localPosition: { x: -0.25, y: 0.8, z: -1.2 }, + textColor: { red: 255, green: 255, blue: 255}, + backgroundColor: { red: 0, green: 0, blue: 0}, + text: "", + lineHeight: 0.03, + leftMargin: 0.015, + topMargin: 0.01, + backgroundAlpha: 0.7, + textAlpha: 1.0, + unlit: true, + ignorePickIntersection: true + }, "local"); + } } function mouseReleaseOnOverlay(overlayID, event) { @@ -629,6 +708,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.messageReceived.connect(controllerDispatcher.handleMessage); - Script.scriptEnding.connect(controllerDispatcher.cleanup); + Script.scriptEnding.connect(function () { + controllerDispatcher.cleanup(); + }); Script.setTimeout(controllerDispatcher.update, BASIC_TIMER_INTERVAL_MS); }()); diff --git a/scripts/system/controllers/controllerModules/disableOtherModule.js b/scripts/system/controllers/controllerModules/disableOtherModule.js index 7636c56f65..d49776f06a 100644 --- a/scripts/system/controllers/controllerModules/disableOtherModule.js +++ b/scripts/system/controllers/controllerModules/disableOtherModule.js @@ -17,7 +17,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.hand = hand; this.disableModules = false; this.parameters = makeDispatcherModuleParameters( - 90, + 95, this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"], diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index 1eaed44ce2..8f18be9c27 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -14,7 +14,7 @@ TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic, getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, Picks, makeLaserLockInfo, makeLaserParams, AddressManager, getEntityParents, Selection, DISPATCHER_HOVERING_LIST, - worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES, Uuid, Picks + worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES, Uuid, Picks, handsAreTracked, Messages */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -374,6 +374,9 @@ Script.include("/~/system/libraries/controllers.js"); this.isReady = function (controllerData) { if (HMD.active) { + if (handsAreTracked()) { + return makeRunningValues(false, [], []); + } if (this.notPointingAtEntity(controllerData)) { return makeRunningValues(false, [], []); } diff --git a/scripts/system/controllers/controllerModules/farGrabEntity.js b/scripts/system/controllers/controllerModules/farGrabEntity.js index ecafa3cb26..c486d46c33 100644 --- a/scripts/system/controllers/controllerModules/farGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farGrabEntity.js @@ -12,7 +12,7 @@ HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, projectOntoEntityXYPlane, ContextOverlay, HMD, Picks, makeLaserLockInfo, makeLaserParams, AddressManager, getEntityParents, Selection, DISPATCHER_HOVERING_LIST, unhighlightTargetEntity, Messages, findGrabbableGroupParent, - worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES + worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES, handsAreTracked */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -63,7 +63,6 @@ Script.include("/~/system/libraries/controllers.js"); this.endedGrab = 0; this.MIN_HAPTIC_PULSE_INTERVAL = 500; // ms this.disabled = false; - var _this = this; this.initialControllerRotation = Quat.IDENTITY; this.currentControllerRotation = Quat.IDENTITY; this.manipulating = false; @@ -99,7 +98,7 @@ Script.include("/~/system/libraries/controllers.js"); this.getOffhand = function () { return (this.hand === RIGHT_HAND ? LEFT_HAND : RIGHT_HAND); - } + }; // Activation criteria for rotating a fargrabbed entity. If we're changing the mapping, this is where to do it. this.shouldManipulateTarget = function (controllerData) { @@ -406,6 +405,9 @@ Script.include("/~/system/libraries/controllers.js"); this.isReady = function (controllerData) { if (HMD.active) { + if (handsAreTracked()) { + return makeRunningValues(false, [], []); + } if (this.notPointingAtEntity(controllerData)) { return makeRunningValues(false, [], []); } diff --git a/scripts/system/controllers/controllerModules/farTrigger.js b/scripts/system/controllers/controllerModules/farTrigger.js index c9c9d3deee..2a8a4d7246 100644 --- a/scripts/system/controllers/controllerModules/farTrigger.js +++ b/scripts/system/controllers/controllerModules/farTrigger.js @@ -8,7 +8,7 @@ /* global Script, RIGHT_HAND, LEFT_HAND, MyAvatar, makeRunningValues, Entities, enableDispatcherModule, disableDispatcherModule, makeDispatcherModuleParameters, - getGrabbableData, makeLaserParams, DISPATCHER_PROPERTIES + getGrabbableData, makeLaserParams, DISPATCHER_PROPERTIES, RayPick, handsAreTracked */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -63,6 +63,9 @@ Script.include("/~/system/libraries/controllers.js"); this.isReady = function (controllerData) { this.targetEntityID = null; + if (handsAreTracked()) { + return makeRunningValues(false, [], []); + } if (controllerData.triggerClicks[this.hand] === 0) { return makeRunningValues(false, [], []); } diff --git a/scripts/system/controllers/controllerModules/nearGrabEntity.js b/scripts/system/controllers/controllerModules/nearGrabEntity.js index 763c1a1ce0..45d518bb39 100644 --- a/scripts/system/controllers/controllerModules/nearGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearGrabEntity.js @@ -151,7 +151,7 @@ Script.include("/~/system/libraries/controllers.js"); this.run = function (controllerData, deltaTime) { if (this.grabbing) { - if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE && + if (!controllerData.triggerClicks[this.hand] && controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { this.endNearGrabEntity(); return makeRunningValues(false, [], []); diff --git a/scripts/system/controllers/controllerModules/stylusInput.js b/scripts/system/controllers/controllerModules/stylusInput.js index c4aa9efd50..cbef45050e 100644 --- a/scripts/system/controllers/controllerModules/stylusInput.js +++ b/scripts/system/controllers/controllerModules/stylusInput.js @@ -7,7 +7,7 @@ /* global Script, MyAvatar, Controller, Uuid, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, makeRunningValues, Vec3, makeDispatcherModuleParameters, Overlays, HMD, Settings, getEnabledModuleByName, Pointers, - Picks, PickType + Picks, PickType, Keyboard */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -52,22 +52,21 @@ Script.include("/~/system/libraries/controllers.js"); this.disable = false; this.otherModuleNeedsToRun = function(controllerData) { - var grabOverlayModuleName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"; - var grabOverlayModule = getEnabledModuleByName(grabOverlayModuleName); - var grabEntityModuleName = this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity"; + var grabEntityModuleName = this.hand === RIGHT_HAND ? "RightNearGrabEntity" : "LeftNearGrabEntity"; var grabEntityModule = getEnabledModuleByName(grabEntityModuleName); - var grabOverlayModuleReady = grabOverlayModule ? grabOverlayModule.isReady(controllerData) : makeRunningValues(false, [], []); var grabEntityModuleReady = grabEntityModule ? grabEntityModule.isReady(controllerData) : makeRunningValues(false, [], []); - var farGrabModuleName = this.hand === RIGHT_HAND ? "RightFarActionGrabEntity" : "LeftFarActionGrabEntity"; + + var farGrabModuleName = this.hand === RIGHT_HAND ? "RightFarGrabEntity" : "LeftFarGrabEntity"; var farGrabModule = getEnabledModuleByName(farGrabModuleName); var farGrabModuleReady = farGrabModule ? farGrabModule.isReady(controllerData) : makeRunningValues(false, [], []); + var nearTabletHighlightModuleName = this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight"; var nearTabletHighlightModule = getEnabledModuleByName(nearTabletHighlightModuleName); - var nearTabletHighlightModuleReady = nearTabletHighlightModule - ? nearTabletHighlightModule.isReady(controllerData) : makeRunningValues(false, [], []); - return grabOverlayModuleReady.active || farGrabModuleReady.active || grabEntityModuleReady.active - || nearTabletHighlightModuleReady.active; + var nearTabletHighlightModuleReady = nearTabletHighlightModule ? + nearTabletHighlightModule.isReady(controllerData) : makeRunningValues(false, [], []); + + return farGrabModuleReady.active || grabEntityModuleReady.active; }; this.overlayLaserActive = function(controllerData) { @@ -121,14 +120,15 @@ Script.include("/~/system/libraries/controllers.js"); } // Add the mini tablet. - if (HMD.miniTabletScreenID && Overlays.getProperty(HMD.miniTabletScreenID, "visible")) { + if (HMD.miniTabletScreenID && Overlays.getProperty(HMD.miniTabletScreenID, "visible") && + this.hand != HMD.miniTabletHand) { stylusTarget = getOverlayDistance(controllerPosition, HMD.miniTabletScreenID); if (stylusTarget) { stylusTargets.push(stylusTarget); } } - const WEB_DISPLAY_STYLUS_DISTANCE = (Keyboard.raised && Keyboard.preferMalletsOverLasers) ? 0.2 : 0.5; + var WEB_DISPLAY_STYLUS_DISTANCE = (Keyboard.raised && Keyboard.preferMalletsOverLasers) ? 0.2 : 0.5; var nearStylusTarget = isNearStylusTarget(stylusTargets, WEB_DISPLAY_STYLUS_DISTANCE * sensorScaleFactor); if (nearStylusTarget.length !== 0) { diff --git a/scripts/system/controllers/controllerModules/trackedHandTablet.js b/scripts/system/controllers/controllerModules/trackedHandTablet.js new file mode 100644 index 0000000000..66cf408af8 --- /dev/null +++ b/scripts/system/controllers/controllerModules/trackedHandTablet.js @@ -0,0 +1,136 @@ +"use strict"; + +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, makeRunningValues, enableDispatcherModule, disableDispatcherModule, + makeDispatcherModuleParameters, handsAreTracked, Controller, Vec3, Tablet, HMD, MyAvatar +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + + function TrackedHandTablet() { + this.mappingName = 'hand-track-tablet-' + Math.random(); + this.inputMapping = Controller.newMapping(this.mappingName); + this.leftIndexPos = null; + this.leftThumbPos = null; + this.rightIndexPos = null; + this.rightThumbPos = null; + this.touchOnBelowDistance = 0.016; + this.touchOffAboveDistance = 0.045; + + this.gestureCompleted = false; + this.previousGestureCompleted = false; + + this.parameters = makeDispatcherModuleParameters( + 70, + ["rightHand", "leftHand"], + [], + 100); + + this.checkForGesture = function () { + if (this.leftThumbPos && this.leftIndexPos && this.rightThumbPos && this.rightIndexPos) { + var leftTipDistance = Vec3.distance(this.leftThumbPos, this.leftIndexPos); + var rightTipDistance = Vec3.distance(this.rightThumbPos, this.rightIndexPos); + if (leftTipDistance < this.touchOnBelowDistance && rightTipDistance < this.touchOnBelowDistance) { + this.gestureCompleted = true; + } else if (leftTipDistance > this.touchOffAboveDistance || rightTipDistance > this.touchOffAboveDistance) { + this.gestureCompleted = false; + } // else don't change gestureCompleted + } else { + this.gestureCompleted = false; + } + + if (this.gestureCompleted && !this.previousGestureCompleted) { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + if (HMD.showTablet) { + HMD.closeTablet(false); + } else if (!HMD.showTablet && !tablet.toolbarMode && !MyAvatar.isAway) { + tablet.gotoHomeScreen(); + HMD.openTablet(false); + } + } + + this.previousGestureCompleted = this.gestureCompleted; + }; + + this.leftIndexChanged = function (pose) { + if (pose.valid) { + this.leftIndexPos = pose.translation; + } else { + this.leftIndexPos = null; + } + this.checkForGesture(); + }; + + this.leftThumbChanged = function (pose) { + if (pose.valid) { + this.leftThumbPos = pose.translation; + } else { + this.leftThumbPos = null; + } + this.checkForGesture(); + }; + + this.rightIndexChanged = function (pose) { + if (pose.valid) { + this.rightIndexPos = pose.translation; + } else { + this.rightIndexPos = null; + } + this.checkForGesture(); + }; + + this.rightThumbChanged = function (pose) { + if (pose.valid) { + this.rightThumbPos = pose.translation; + } else { + this.rightThumbPos = null; + } + this.checkForGesture(); + }; + + this.isReady = function (controllerData) { + return makeRunningValues(handsAreTracked() && this.gestureCompleted, [], []); + }; + + this.run = function (controllerData) { + return this.isReady(controllerData); + }; + + this.setup = function () { + var _this = this; + this.inputMapping.from(Controller.Standard.LeftHandIndex4).peek().to(function (pose) { + _this.leftIndexChanged(pose); + }); + this.inputMapping.from(Controller.Standard.LeftHandThumb4).peek().to(function (pose) { + _this.leftThumbChanged(pose); + }); + this.inputMapping.from(Controller.Standard.RightHandIndex4).peek().to(function (pose) { + _this.rightIndexChanged(pose); + }); + this.inputMapping.from(Controller.Standard.RightHandThumb4).peek().to(function (pose) { + _this.rightThumbChanged(pose); + }); + + Controller.enableMapping(this.mappingName); + }; + + this.cleanUp = function () { + this.inputMapping.disable(); + }; + } + + var trackedHandWalk = new TrackedHandTablet(); + trackedHandWalk.setup(); + enableDispatcherModule("TrackedHandTablet", trackedHandWalk); + + function cleanup() { + trackedHandWalk.cleanUp(); + disableDispatcherModule("TrackedHandTablet"); + } + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/system/controllers/controllerModules/trackedHandWalk.js b/scripts/system/controllers/controllerModules/trackedHandWalk.js new file mode 100644 index 0000000000..9ecc53a1fa --- /dev/null +++ b/scripts/system/controllers/controllerModules/trackedHandWalk.js @@ -0,0 +1,172 @@ +"use strict"; + +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, makeRunningValues, enableDispatcherModule, disableDispatcherModule, + makeDispatcherModuleParameters, handsAreTracked, Controller, Vec3 +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + + function TrackedHandWalk() { + this.gestureMappingName = 'hand-track-walk-gesture-' + Math.random(); + this.inputGestureMapping = Controller.newMapping(this.gestureMappingName); + this.mappingName = 'hand-track-walk-' + Math.random(); + this.inputMapping = Controller.newMapping(this.mappingName); + this.leftIndexPos = null; + this.leftThumbPos = null; + this.rightIndexPos = null; + this.rightThumbPos = null; + this.touchOnBelowDistance = 0.016; + this.touchOffAboveDistance = 0.045; + this.walkingForward = false; + this.walkingBackward = false; + + this.mappingEnabled = false; + + this.parameters = makeDispatcherModuleParameters( + 80, + ["rightHand", "leftHand"], + [], + 100); + + this.getControlPoint = function () { + return Vec3.multiply(Vec3.sum(this.leftIndexPos, this.rightIndexPos), 0.5); + }; + + this.updateWalking = function () { + if (this.leftIndexPos && this.rightIndexPos) { + var indexTipDistance = Vec3.distance(this.leftIndexPos, this.rightIndexPos); + if (indexTipDistance < this.touchOnBelowDistance) { + this.walkingForward = true; + this.controlPoint = this.getControlPoint(); + } else if (this.walkingForward && indexTipDistance > this.touchOffAboveDistance) { + this.walkingForward = false; + } // else don't change walkingForward + } + + if (this.leftThumbPos && this.rightThumbPos) { + var thumbTipDistance = Vec3.distance(this.leftThumbPos, this.rightThumbPos); + if (thumbTipDistance < this.touchOnBelowDistance) { + this.walkingBackward = true; + this.controlPoint = this.getControlPoint(); + } else if (this.walkingBackward && thumbTipDistance > this.touchOffAboveDistance) { + this.walkingBackward = false; + } // else don't change this.walkingBackward + } + + if ((this.walkingForward || this.walkingBackward) && !this.mappingEnabled) { + Controller.enableMapping(this.mappingName); + this.mappingEnabled = true; + } else if (!(this.walkingForward || this.walkingBackward) && this.mappingEnabled) { + this.inputMapping.disable(); + this.mappingEnabled = false; + } // else don't change mappingEnabled + }; + + this.leftIndexChanged = function (pose) { + if (pose.valid) { + this.leftIndexPos = pose.translation; + } else { + this.leftIndexPos = null; + } + this.updateWalking(); + }; + + this.leftThumbChanged = function (pose) { + if (pose.valid) { + this.leftThumbPos = pose.translation; + } else { + this.leftThumbPos = null; + } + this.updateWalking(); + }; + + this.rightIndexChanged = function (pose) { + if (pose.valid) { + this.rightIndexPos = pose.translation; + } else { + this.rightIndexPos = null; + } + this.updateWalking(); + }; + + this.rightThumbChanged = function (pose) { + if (pose.valid) { + this.rightThumbPos = pose.translation; + } else { + this.rightThumbPos = null; + } + this.updateWalking(); + }; + + this.isReady = function (controllerData) { + return makeRunningValues(handsAreTracked() && (this.walkingForward || this.walkingBackward), [], []); + }; + + this.run = function (controllerData) { + return this.isReady(controllerData); + }; + + this.setup = function () { + var _this = this; + this.inputGestureMapping.from(Controller.Standard.LeftHandIndex4).peek().to(function (pose) { + _this.leftIndexChanged(pose); + }); + this.inputGestureMapping.from(Controller.Standard.LeftHandThumb4).peek().to(function (pose) { + _this.leftThumbChanged(pose); + }); + this.inputGestureMapping.from(Controller.Standard.RightHandIndex4).peek().to(function (pose) { + _this.rightIndexChanged(pose); + }); + this.inputGestureMapping.from(Controller.Standard.RightHandThumb4).peek().to(function (pose) { + _this.rightThumbChanged(pose); + }); + + this.inputMapping.from(function() { + if (_this.walkingForward) { + // var currentPoint = _this.getControlPoint(); + // return currentPoint.z - _this.controlPoint.z; + return -0.5; + } else if (_this.walkingBackward) { + // var currentPoint = _this.getControlPoint(); + // return currentPoint.z - _this.controlPoint.z; + return 0.5; + } else { + // return Controller.getActionValue(Controller.Standard.TranslateZ); + return 0.0; + } + }).to(Controller.Actions.TranslateZ); + + // this.inputMapping.from(function() { + // if (_this.walkingForward) { + // var currentPoint = _this.getControlPoint(); + // return currentPoint.x - _this.controlPoint.x; + // } else { + // return Controller.getActionValue(Controller.Standard.Yaw); + // } + // }).to(Controller.Actions.Yaw); + + Controller.enableMapping(this.gestureMappingName); + }; + + this.cleanUp = function () { + this.inputGestureMapping.disable(); + this.inputMapping.disable(); + }; + } + + var trackedHandWalk = new TrackedHandWalk(); + trackedHandWalk.setup(); + enableDispatcherModule("TrackedHandWalk", trackedHandWalk); + + function cleanup() { + trackedHandWalk.cleanUp(); + disableDispatcherModule("TrackedHandWalk"); + } + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index c9cb61b5f5..fb422ebdf7 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -34,7 +34,9 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/nearTabletHighlight.js", "controllerModules/nearGrabEntity.js", "controllerModules/farGrabEntity.js", - "controllerModules/pushToTalk.js" + "controllerModules/pushToTalk.js", + "controllerModules/trackedHandWalk.js", + "controllerModules/trackedHandTablet.js" ]; var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index 3d8578715d..ab21c5776c 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -603,20 +603,28 @@ var toolBar = (function () { Script.setTimeout(dimensionsCheckFunction, DIMENSIONS_CHECK_INTERVAL); } // Make sure the model entity is loaded before we try to figure out - // its dimensions. - var MAX_LOADED_CHECKS = 10; + // its dimensions. We need to give ample time to load the entity. + var MAX_LOADED_CHECKS = 100; // 100 * 100ms = 10 seconds. var LOADED_CHECK_INTERVAL = 100; var isLoadedCheckCount = 0; var entityIsLoadedCheck = function() { isLoadedCheckCount++; if (isLoadedCheckCount === MAX_LOADED_CHECKS || Entities.isLoaded(entityID)) { var naturalDimensions = Entities.getEntityProperties(entityID, "naturalDimensions").naturalDimensions; - + + if (isLoadedCheckCount === MAX_LOADED_CHECKS) { + console.log("Model entity failed to load in time: " + (MAX_LOADED_CHECKS * LOADED_CHECK_INTERVAL) + " ... setting dimensions to: " + JSON.stringify(naturalDimensions)) + } + Entities.editEntity(entityID, { visible: true, dimensions: naturalDimensions }) dimensionsCheckCallback(); + // We want to update the selection manager again since the script has moved on without us. + selectionManager.clearSelections(this); + entityListTool.sendUpdate(); + selectionManager.setSelections([entityID], this); return; } Script.setTimeout(entityIsLoadedCheck, LOADED_CHECK_INTERVAL); diff --git a/scripts/system/html/css/miniHandTablet.css b/scripts/system/html/css/miniHandTablet.css new file mode 100644 index 0000000000..ef2c27ff14 --- /dev/null +++ b/scripts/system/html/css/miniHandTablet.css @@ -0,0 +1,56 @@ +/* +miniTablet.css + +Copyright 2019 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: 32px 0px; +} + +.button { + text-align: center; +} + +img { + width: 149px; + height: 149px; +} + +#expand { + width: 149px; + height: 149px; + 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 { +} diff --git a/scripts/system/html/js/miniTablet.js b/scripts/system/html/js/miniTablet.js index c48201cef5..b02c6ae213 100644 --- a/scripts/system/html/js/miniTablet.js +++ b/scripts/system/html/js/miniTablet.js @@ -35,7 +35,9 @@ function setUnhover() { if (!isUnhover) { - gotoButton.classList.add("unhover"); + if (gotoButton) { + gotoButton.classList.add("unhover"); + } expandButton.classList.add("unhover"); isUnhover = true; } @@ -43,7 +45,9 @@ function clearUnhover() { if (isUnhover) { - gotoButton.classList.remove("unhover"); + if (gotoButton) { + gotoButton.classList.remove("unhover"); + } expandButton.classList.remove("unhover"); isUnhover = false; } @@ -62,10 +66,14 @@ switch (message.type) { case MUTE_MESSAGE: - muteImage.src = message.icon; + if (muteImage) { + muteImage.src = message.icon; + } break; case GOTO_MESSAGE: - gotoImage.src = message.icon; + if (gotoImage) { + gotoImage.src = message.icon; + } break; } } @@ -130,9 +138,7 @@ 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(); @@ -140,11 +146,19 @@ document.body.addEventListener("mouseenter", onBodyHover, false); document.body.addEventListener("mouseleave", onBodyUnhover, false); - muteButton.addEventListener("mouseenter", onButtonHover, false); - gotoButton.addEventListener("mouseenter", onButtonHover, false); + if (muteButton) { + muteImage = document.getElementById("mute-img"); + muteButton.addEventListener("mouseenter", onButtonHover, false); + muteButton.addEventListener("click", onMuteButtonClick, true); + } + + if (gotoButton) { + gotoImage = document.getElementById("goto-img"); + gotoButton.addEventListener("mouseenter", onButtonHover, false); + gotoButton.addEventListener("click", onGotoButtonClick, true); + } + expandButton.addEventListener("mouseenter", onButtonHover, false); - muteButton.addEventListener("click", onMuteButtonClick, true); - gotoButton.addEventListener("click", onGotoButtonClick, true); expandButton.addEventListener("click", onExpandButtonClick, true); document.body.onunload = function () { diff --git a/scripts/system/html/miniHandsTablet.html b/scripts/system/html/miniHandsTablet.html new file mode 100644 index 0000000000..1d140797f4 --- /dev/null +++ b/scripts/system/html/miniHandsTablet.html @@ -0,0 +1,26 @@ + + + + + + + + + + +
+
+ +
+
+ + + diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index b7593656a3..9f2142504c 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -164,6 +164,7 @@ WebTablet = function (url, width, dpi, hand, location, visible) { parentID: this.tabletEntityID, parentJointIndex: -1, showKeyboardFocusHighlight: false, + grabbable: false, visible: visible }); diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 3b81e17473..5cfd899da0 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -6,7 +6,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html /* global module, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays, Xform, Mat4, - Selection, Uuid, + Selection, Uuid, Controller, MSECS_PER_SEC:true , LEFT_HAND:true, RIGHT_HAND:true, FORBIDDEN_GRAB_TYPES:true, HAPTIC_PULSE_STRENGTH:true, HAPTIC_PULSE_DURATION:true, ZERO_VEC:true, ONE_VEC:true, DEFAULT_REGISTRATION_POINT:true, INCHES_TO_METERS:true, @@ -56,6 +56,7 @@ TEAR_AWAY_DISTANCE:true, TEAR_AWAY_COUNT:true, TEAR_AWAY_CHECK_TIME:true, + TELEPORT_DEADZONE: true, NEAR_GRAB_DISTANCE: true, distanceBetweenPointAndEntityBoundingBox:true, entityIsEquipped:true, @@ -63,7 +64,8 @@ clearHighlightedEntities:true, unhighlightTargetEntity:true, distanceBetweenEntityLocalPositionAndBoundingBox: true, - worldPositionToRegistrationFrameMatrix: true + worldPositionToRegistrationFrameMatrix: true, + handsAreTracked: true */ MSECS_PER_SEC = 1000.0; @@ -600,6 +602,10 @@ worldPositionToRegistrationFrameMatrix = function(wptrProps, pos) { return offsetMat; }; +handsAreTracked = function () { + return Controller.getPoseValue(Controller.Standard.LeftHandIndex3).valid || + Controller.getPoseValue(Controller.Standard.RightHandIndex3).valid; +}; if (typeof module !== 'undefined') { module.exports = { @@ -624,6 +630,7 @@ if (typeof module !== 'undefined') { TRIGGER_OFF_VALUE: TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE: TRIGGER_ON_VALUE, DISPATCHER_HOVERING_LIST: DISPATCHER_HOVERING_LIST, - worldPositionToRegistrationFrameMatrix: worldPositionToRegistrationFrameMatrix + worldPositionToRegistrationFrameMatrix: worldPositionToRegistrationFrameMatrix, + handsAreTracked: handsAreTracked }; } diff --git a/scripts/system/miniTablet.js b/scripts/system/miniTablet.js index f5b5ecf0a1..1650cb60f4 100644 --- a/scripts/system/miniTablet.js +++ b/scripts/system/miniTablet.js @@ -8,7 +8,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global getTabletWidthFromSettings, TRIGGER_OFF_VALUE */ +/* global getTabletWidthFromSettings, handsAreTracked, TRIGGER_OFF_VALUE, Controller, Script, Camera, Tablet, MyAvatar, + Quat, SoundCache, HMD, Overlays, Vec3, Uuid, Messages */ (function () { @@ -80,7 +81,6 @@ return hand === LEFT_HAND ? RIGHT_HAND : LEFT_HAND; } - UI = function () { if (!(this instanceof UI)) { @@ -114,6 +114,7 @@ uiHand = LEFT_HAND, miniUIOverlay = null, MINI_UI_HTML = Script.resolvePath("./html/miniTablet.html"), + MINI_HAND_UI_HTML = Script.resolvePath("./html/miniHandsTablet.html"), MINI_UI_DIMENSIONS = { x: 0.059, y: 0.0865, z: 0.001 }, MINI_UI_WIDTH_PIXELS = 150, METERS_TO_INCHES = 39.3701, @@ -291,6 +292,7 @@ visible: true }); Overlays.editOverlay(miniUIOverlay, { + url: handsAreTracked() ? MINI_HAND_UI_HTML : MINI_UI_HTML, localPosition: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_UI_LOCAL_POSITION), localRotation: MINI_UI_LOCAL_ROTATION, dimensions: Vec3.multiply(initialScale, MINI_UI_DIMENSIONS), @@ -353,8 +355,8 @@ localRotation, localPosition; - tabletScaleFactor = MyAvatar.sensorToWorldScale - * (1 + scaleFactor * (miniTargetWidth - miniInitialWidth) / miniInitialWidth); + tabletScaleFactor = MyAvatar.sensorToWorldScale * + (1 + scaleFactor * (miniTargetWidth - miniInitialWidth) / miniInitialWidth); dimensions = Vec3.multiply(tabletScaleFactor, MINI_DIMENSIONS); localRotation = Quat.mix(miniExpandLocalRotation, miniTargetLocalRotation, scaleFactor); localPosition = @@ -469,11 +471,11 @@ solid: true, grabbable: true, showKeyboardFocusHighlight: false, - drawInFront: true, + drawInFront: false, visible: false }); miniUIOverlay = Overlays.addOverlay("web3d", { - url: MINI_UI_HTML, + url: handsAreTracked() ? MINI_HAND_UI_HTML : MINI_UI_HTML, parentID: miniOverlay, localPosition: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_UI_LOCAL_POSITION), localRotation: MINI_UI_LOCAL_ROTATION, @@ -482,7 +484,7 @@ alpha: 0, // Hide overlay while its content is being created. grabbable: false, showKeyboardFocusHighlight: false, - drawInFront: true, + drawInFront: false, visible: false }); @@ -642,8 +644,8 @@ // 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; + 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(); @@ -653,8 +655,8 @@ } else { leftTriggerOn = 0; } - isRightTriggerOff = Controller.getValue(Controller.Standard.RT) < TRIGGER_OFF_VALUE - && Controller.getValue(Controller.Standard.RightGrip) < TRIGGER_OFF_VALUE; + 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(); @@ -665,8 +667,8 @@ rightTriggerOn = 0; } - show = (hand === LEFT_HAND ? wasLeftTriggerOff : wasRightTriggerOff) - && ((hand === LEFT_HAND ? wasRightTriggerOff : wasLeftTriggerOff) || ui.isLaserPointingAt()); + 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. @@ -691,10 +693,10 @@ normalDot = Vec3.dot(normalHandVector, miniToCameraDirection); medialAngle = Math.atan2(medialDot, normalDot); lateralAngle = Math.atan2(lateralDot, normalDot); - show = -MAX_MEDIAL_WRIST_CAMERA_ANGLE_RAD <= medialAngle - && medialAngle <= MAX_MEDIAL_FINGER_CAMERA_ANGLE_RAD - && -MAX_LATERAL_THUMB_CAMERA_ANGLE_RAD <= lateralAngle - && lateralAngle <= MAX_LATERAL_PINKY_CAMERA_ANGLE_RAD; + show = -MAX_MEDIAL_WRIST_CAMERA_ANGLE_RAD <= medialAngle && + medialAngle <= MAX_MEDIAL_FINGER_CAMERA_ANGLE_RAD && + -MAX_LATERAL_THUMB_CAMERA_ANGLE_RAD <= lateralAngle && + lateralAngle <= MAX_LATERAL_PINKY_CAMERA_ANGLE_RAD; // Camera looking at mini tablet? cameraToMini = -Vec3.dot(miniToCameraDirection, Quat.getForward(Camera.orientation)); @@ -972,8 +974,8 @@ function setState(state, data) { if (state !== miniState) { - debug("State transition from " + STATE_STRINGS[miniState] + " to " + STATE_STRINGS[state] - + ( data ? " " + JSON.stringify(data) : "")); + 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); } @@ -1061,8 +1063,8 @@ return; } - if (miniState.getState() === miniState.MINI_DISABLED - || (message.grabbedEntity !== HMD.tabletID && message.grabbedEntity !== ui.getMiniTabletID())) { + if (miniState.getState() === miniState.MINI_DISABLED || + (message.grabbedEntity !== HMD.tabletID && message.grabbedEntity !== ui.getMiniTabletID())) { return; } diff --git a/scripts/system/more/app-more.js b/scripts/system/more/app-more.js index b728378e39..8dc0603385 100644 --- a/scripts/system/more/app-more.js +++ b/scripts/system/more/app-more.js @@ -6,7 +6,7 @@ // Created by Keb Helion, February 2020. // Copyright 2020 Vircadia contributors. // -// This script adds a "More Apps" selector to "Project Athena" to allow the user to add optional functionalities to the tablet. +// This script adds a "More Apps" selector to "Vircadia" to allow the user to add optional functionalities to the tablet. // This application has been designed to work directly from the Github repository. // // Distributed under the Apache License, Version 2.0.