diff --git a/BUILD.md b/BUILD.md index 547b79cb08..fc2359b057 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,7 +1,7 @@ ###Dependencies * [cmake](https://cmake.org/download/) ~> 3.3.2 -* [Qt](https://www.qt.io/download-open-source) ~> 5.6.1 +* [Qt](https://www.qt.io/download-open-source) ~> 5.6.2 * [OpenSSL](https://www.openssl.org/community/binaries.html) * IMPORTANT: Use the latest available version of OpenSSL to avoid security vulnerabilities. * [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) @@ -46,8 +46,8 @@ This can either be entered directly into your shell session before you build or The path it needs to be set to will depend on where and how Qt5 was installed. e.g. - export QT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.1/clang_64/lib/cmake/ - export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.6.1-1/lib/cmake + export QT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.2/clang_64/lib/cmake/ + export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.6.2/lib/cmake export QT_CMAKE_PREFIX_PATH=/usr/local/opt/qt5/lib/cmake ####Generating build files @@ -64,7 +64,7 @@ Any variables that need to be set for CMake to find dependencies can be set as E For example, to pass the QT_CMAKE_PREFIX_PATH variable during build file generation: - cmake .. -DQT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.1/lib/cmake + cmake .. -DQT_CMAKE_PREFIX_PATH=/usr/local/qt/5.6.2/lib/cmake ####Finding Dependencies diff --git a/BUILD_OSX.md b/BUILD_OSX.md index 980263cbbc..afd3fa040c 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -16,16 +16,12 @@ For OpenSSL installed via homebrew, set OPENSSL_ROOT_DIR: Note that this uses the version from the homebrew formula at the time of this writing, and the version in the path will likely change. ###Qt -You can use the online installer or the offline installer. +Download and install the [Qt 5.6.2 for macOS](http://download.qt.io/official_releases/qt/5.6/5.6.2/qt-opensource-mac-x64-clang-5.6.2.dmg). -* [Download the online installer](https://www.qt.io/download-open-source/#section-2) - * When it asks you to select components, select the following: - * Qt > Qt 5.6 - -* [Download the offline installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-mac-x64-clang-5.6.1-1.dmg) +Keep the default components checked when going through the installer. Once Qt is installed, you need to manually configure the following: -* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt5.6.1/5.6/clang_64/lib/cmake/` directory. +* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt5.6.2/5.6/clang_64/lib/cmake/` directory. ###Xcode If Xcode is your editor of choice, you can ask CMake to generate Xcode project files instead of Unix Makefiles. diff --git a/BUILD_WIN.md b/BUILD_WIN.md index e37bf27503..841cfba3b3 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -8,23 +8,23 @@ Note: Newer versions of Visual Studio are not yet compatible. ###Step 2. Installing CMake -Download and install the CMake 3.8.0-rc2 "win64-x64 Installer" from the [CMake Website](https://cmake.org/download/). Make sure "Add CMake to system PATH for all users" is checked when going through the installer. +Download and install the [CMake 3.8.0 win64-x64 Installer](https://cmake.org/files/v3.8/cmake-3.8.0-win64-x64.msi). Make sure "Add CMake to system PATH for all users" is checked when going through the installer. ###Step 3. Installing Qt -Download and install the [Qt 5.6.1 Installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe). Please note that the download file is large (850MB) and may take some time. +Download and install the [Qt 5.6.2 for Windows 64-bit (VS 2013)](http://download.qt.io/official_releases/qt/5.6/5.6.2/qt-opensource-windows-x86-msvc2013_64-5.6.2.exe). -Make sure to select all components when going through the installer. +Keep the default components checked when going through the installer. ###Step 4. Setting Qt Environment Variable Go to "Control Panel > System > Advanced System Settings > Environment Variables > New..." (or search “Environment Variables” in Start Search). * Set "Variable name": QT_CMAKE_PREFIX_PATH -* Set "Variable value": `C:\Qt\Qt5.6.1\5.6\msvc2013_64\lib\cmake` +* Set "Variable value": `%QT_DIR%\5.6\msvc2013_64\lib\cmake` ###Step 5. Installing OpenSSL -Download and install the "Win64 OpenSSL v1.0.2k" Installer from [this website](https://slproweb.com/products/Win32OpenSSL.html). +Download and install the [Win64 OpenSSL v1.0.2k Installer](https://slproweb.com/download/Win64OpenSSL-1_0_2k.exe). ###Step 6. Running CMake to Generate Build Files @@ -77,5 +77,5 @@ If not, add the directory where nmake is located to the PATH environment variabl ####Qt is throwing an error -Make sure you have the correct version (5.6.1-1) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly. +Make sure you have the correct version (5.6.2) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly. diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index f2dbe5d1d2..af5f2c904e 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -11,12 +11,14 @@ #include "OctreeServer.h" +#include #include #include #include #include +#include #include #include #include @@ -924,6 +926,57 @@ void OctreeServer::handleJurisdictionRequestPacket(QSharedPointerqueueReceivedPacket(message, senderNode); } +void OctreeServer::handleOctreeFileReplacement(QSharedPointer message) { + if (!_isFinished && !_isShuttingDown) { + // these messages are only allowed to come from the domain server, so make sure that is the case + auto nodeList = DependencyManager::get(); + if (message->getSenderSockAddr() == nodeList->getDomainHandler().getSockAddr()) { + // it's far cleaner to load up the new content upon server startup + // so here we just store a special file at our persist path + // and then force a stop of the server so that it can pick it up when it relaunches + if (!_persistAbsoluteFilePath.isEmpty()) { + + // before we restart the server and make it try and load this data, let's make sure it is valid + auto compressedOctree = message->getMessage(); + QByteArray jsonOctree; + + // assume we have GZipped content + bool wasCompressed = gunzip(compressedOctree, jsonOctree); + if (!wasCompressed) { + // the source was not compressed, assume we were sent regular JSON data + jsonOctree = compressedOctree; + } + + // check the JSON data to verify it is an object + if (QJsonDocument::fromJson(jsonOctree).isObject()) { + if (!wasCompressed) { + // source was not compressed, we compress it before we write it locally + gzip(jsonOctree, compressedOctree); + } + + // write the compressed octree data to a special file + auto replacementFilePath = _persistAbsoluteFilePath.append(OctreePersistThread::REPLACEMENT_FILE_EXTENSION); + QFile replacementFile(replacementFilePath); + if (replacementFile.open(QIODevice::WriteOnly) && replacementFile.write(compressedOctree) != -1) { + // we've now written our replacement file, time to take the server down so it can + // process it when it comes back up + qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server"; + setFinished(true); + } else { + qWarning() << "Could not write replacement octree data to file - refusing to process"; + } + } else { + qDebug() << "Received replacement octree file that is invalid - refusing to process"; + } + } else { + qDebug() << "Cannot perform octree file replacement since current persist file path is not yet known"; + } + } else { + qDebug() << "Received an octree file replacement that was not from our domain server - refusing to process"; + } + } +} + bool OctreeServer::readOptionBool(const QString& optionName, const QJsonObject& settingsSectionObject, bool& result) { result = false; // assume it doesn't exist bool optionAvailable = false; @@ -1148,6 +1201,7 @@ void OctreeServer::domainSettingsRequestComplete() { packetReceiver.registerListener(getMyQueryMessageType(), this, "handleOctreeQueryPacket"); packetReceiver.registerListener(PacketType::OctreeDataNack, this, "handleOctreeDataNackPacket"); packetReceiver.registerListener(PacketType::JurisdictionRequest, this, "handleJurisdictionRequestPacket"); + packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacement"); readConfiguration(); @@ -1173,25 +1227,25 @@ void OctreeServer::domainSettingsRequestComplete() { // If persist filename does not exist, let's see if there is one beside the application binary // If there is, let's copy it over to our target persist directory QDir persistPath { _persistFilePath }; - QString persistAbsoluteFilePath = persistPath.absolutePath(); + _persistAbsoluteFilePath = persistPath.absolutePath(); if (persistPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory - persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); + _persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); } static const QString ENTITY_PERSIST_EXTENSION = ".json.gz"; // force the persist file to end with .json.gz - if (!persistAbsoluteFilePath.endsWith(ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive)) { - persistAbsoluteFilePath += ENTITY_PERSIST_EXTENSION; + if (!_persistAbsoluteFilePath.endsWith(ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive)) { + _persistAbsoluteFilePath += ENTITY_PERSIST_EXTENSION; } else { // make sure the casing of .json.gz is correct - persistAbsoluteFilePath.replace(ENTITY_PERSIST_EXTENSION, ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive); + _persistAbsoluteFilePath.replace(ENTITY_PERSIST_EXTENSION, ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive); } - if (!QFile::exists(persistAbsoluteFilePath)) { + if (!QFile::exists(_persistAbsoluteFilePath)) { qDebug() << "Persist file does not exist, checking for existence of persist file next to application"; static const QString OLD_DEFAULT_PERSIST_FILENAME = "resources/models.json.gz"; @@ -1217,7 +1271,7 @@ void OctreeServer::domainSettingsRequestComplete() { pathToCopyFrom = oldDefaultPersistPath; } - QDir persistFileDirectory { QDir::cleanPath(persistAbsoluteFilePath + "/..") }; + QDir persistFileDirectory { QDir::cleanPath(_persistAbsoluteFilePath + "/..") }; if (!persistFileDirectory.exists()) { qDebug() << "Creating data directory " << persistFileDirectory.absolutePath(); @@ -1225,15 +1279,15 @@ void OctreeServer::domainSettingsRequestComplete() { } if (shouldCopy) { - qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << persistAbsoluteFilePath; + qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << _persistAbsoluteFilePath; - QFile::copy(pathToCopyFrom, persistAbsoluteFilePath); + QFile::copy(pathToCopyFrom, _persistAbsoluteFilePath); } else { qDebug() << "No existing persist file found"; } } - auto persistFileDirectory = QFileInfo(persistAbsoluteFilePath).absolutePath(); + auto persistFileDirectory = QFileInfo(_persistAbsoluteFilePath).absolutePath(); if (_backupDirectoryPath.isEmpty()) { // Use the persist file's directory to store backups _backupDirectoryPath = persistFileDirectory; @@ -1264,7 +1318,7 @@ void OctreeServer::domainSettingsRequestComplete() { qDebug() << "Backups will be stored in: " << _backupDirectoryPath; // now set up PersistThread - _persistThread = new OctreePersistThread(_tree, persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval, + _persistThread = new OctreePersistThread(_tree, _persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval, _wantBackup, _settings, _debugTimestampNow, _persistAsFileType); _persistThread->initialize(true); } diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index 3bb327eb06..8db8d845de 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -136,6 +136,7 @@ private slots: void handleOctreeQueryPacket(QSharedPointer message, SharedNodePointer senderNode); void handleOctreeDataNackPacket(QSharedPointer message, SharedNodePointer senderNode); void handleJurisdictionRequestPacket(QSharedPointer message, SharedNodePointer senderNode); + void handleOctreeFileReplacement(QSharedPointer message); void removeSendThread(); protected: @@ -172,6 +173,7 @@ protected: QString _statusHost; QString _persistFilePath; + QString _persistAbsoluteFilePath; QString _persistAsFileType; QString _backupDirectoryPath; int _packetsPerClientPerInterval; diff --git a/cmake/externals/faceshift/CMakeLists.txt b/cmake/externals/faceshift/CMakeLists.txt deleted file mode 100644 index c4f2055435..0000000000 --- a/cmake/externals/faceshift/CMakeLists.txt +++ /dev/null @@ -1,47 +0,0 @@ -set(EXTERNAL_NAME faceshift) - -include(ExternalProject) -ExternalProject_Add( - ${EXTERNAL_NAME} - URL https://hifi-public.s3.amazonaws.com/dependencies/faceshift.zip - CMAKE_ARGS ${ANDROID_CMAKE_ARGS} -DCMAKE_INSTALL_PREFIX:PATH= - BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build - LOG_DOWNLOAD 1 - LOG_CONFIGURE 1 - LOG_BUILD 1 -) - -# URL_MD5 1bdcb8a0b8d5b1ede434cc41efade41d - -# Hide this external target (for ide users) -set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") - -ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) - -string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) -set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/include CACHE FILEPATH "Path to Faceshift include directory") - -set(LIBRARY_DEBUG_PATH "lib/Debug") -set(LIBRARY_RELEASE_PATH "lib/Release") - -if (WIN32) - set(LIBRARY_PREFIX "") - set(LIBRARY_EXT "lib") - # use selected configuration in release path when building on Windows - set(LIBRARY_RELEASE_PATH "$<$:build/RelWithDebInfo>") - set(LIBRARY_RELEASE_PATH "${LIBRARY_RELEASE_PATH}$<$:build/MinSizeRel>") - set(LIBRARY_RELEASE_PATH "${LIBRARY_RELEASE_PATH}$<$,$>:lib/Release>") -elseif (APPLE) - set(LIBRARY_EXT "a") - set(LIBRARY_PREFIX "lib") - - if (CMAKE_GENERATOR STREQUAL "Unix Makefiles") - set(LIBRARY_DEBUG_PATH "build") - set(LIBRARY_RELEASE_PATH "build") - endif () -endif() - -set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG - ${INSTALL_DIR}/${LIBRARY_DEBUG_PATH}/${LIBRARY_PREFIX}faceshift.${LIBRARY_EXT} CACHE FILEPATH "Faceshift libraries") -set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE -${INSTALL_DIR}/${LIBRARY_RELEASE_PATH}/${LIBRARY_PREFIX}faceshift.${LIBRARY_EXT} CACHE FILEPATH "Faceshift libraries") diff --git a/cmake/macros/TargetFaceshift.cmake b/cmake/macros/TargetFaceshift.cmake deleted file mode 100644 index 99f65d942a..0000000000 --- a/cmake/macros/TargetFaceshift.cmake +++ /dev/null @@ -1,14 +0,0 @@ -# -# Copyright 2015 High Fidelity, Inc. -# Created by Bradley Austin Davis on 2015/10/10 -# -# Distributed under the Apache License, Version 2.0. -# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -# -macro(TARGET_FACESHIFT) - add_dependency_external_projects(faceshift) - find_package(Faceshift REQUIRED) - target_include_directories(${TARGET_NAME} PRIVATE ${FACESHIFT_INCLUDE_DIRS}) - target_link_libraries(${TARGET_NAME} ${FACESHIFT_LIBRARIES}) - add_definitions(-DHAVE_FACESHIFT) -endmacro() \ No newline at end of file diff --git a/cmake/modules/FindFaceshift.cmake b/cmake/modules/FindFaceshift.cmake deleted file mode 100644 index bd77951273..0000000000 --- a/cmake/modules/FindFaceshift.cmake +++ /dev/null @@ -1,26 +0,0 @@ -# -# FindFaceshift.cmake -# -# Try to find the Faceshift networking library -# -# You must provide a FACESHIFT_ROOT_DIR which contains lib and include directories -# -# Once done this will define -# -# FACESHIFT_FOUND - system found Faceshift -# FACESHIFT_INCLUDE_DIRS - the Faceshift include directory -# FACESHIFT_LIBRARIES - Link this to use Faceshift -# -# Created on 8/30/2013 by Andrzej Kapolka -# Copyright 2013 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 -# - -include(SelectLibraryConfigurations) -select_library_configurations(FACESHIFT) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(Faceshift DEFAULT_MSG FACESHIFT_INCLUDE_DIRS FACESHIFT_LIBRARIES) -mark_as_advanced(FACESHIFT_INCLUDE_DIRS FACESHIFT_LIBRARIES FACESHIFT_SEARCH_DIRS) \ No newline at end of file diff --git a/domain-server/resources/web/content/index.shtml b/domain-server/resources/web/content/index.shtml new file mode 100644 index 0000000000..c7eb765878 --- /dev/null +++ b/domain-server/resources/web/content/index.shtml @@ -0,0 +1,44 @@ + + +
+
+
+ +
+
+ +
+
+
+
+

Upload Entities File

+
+
+
+

+ Upload an entities file (e.g.: models.json.gz) to replace the content of this domain.
+ Note: Your domain's content will be replaced by the content you upload, but the backup files of your domain's content will not immediately be changed. +

+

+ If your domain has any content that you would like to re-use at a later date, save a manual backup of your models.json.gz file, which is usually stored at the following paths:
+

C:\Users\[username]\AppData\Roaming\High Fidelity\assignment-client/entities/models.json.gz
+
/Users/[username]/Library/Application Support/High Fidelity/assignment-client/entities/models.json.gz
+
/home/[username]/.local/share/High Fidelity/assignment-client/entities/models.json.gz
+

+
+ +
+
+ +
+
+
+
+
+ + + + + diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js new file mode 100644 index 0000000000..2e6e084164 --- /dev/null +++ b/domain-server/resources/web/content/js/content.js @@ -0,0 +1,45 @@ +$(document).ready(function(){ + + function showSpinnerAlert(title) { + swal({ + title: title, + text: '
', + html: true, + showConfirmButton: false, + allowEscapeKey: false + }); + } + + var frm = $('#upload-form'); + frm.submit(function (ev) { + $.ajax({ + type: frm.attr('method'), + url: frm.attr('action'), + data: new FormData($(this)[0]), + cache: false, + contentType: false, + processData: false, + success: function (data) { + swal({ + title: 'Uploaded', + type: 'success', + text: 'Your Entity Server is restarting to replace its local content with the uploaded file.', + confirmButtonText: 'OK' + }) + }, + error: function (data) { + swal({ + title: '', + type: 'error', + text: 'Your entities file could not be transferred to the Entity Server.
Verify that the file is a .json or .json.gz entities file and try again.', + html: true, + confirmButtonText: 'OK', + }); + } + }); + + ev.preventDefault(); + + showSpinnerAlert("Uploading Entities File"); + }); +}); diff --git a/domain-server/resources/web/header.html b/domain-server/resources/web/header.html index b4eee406f2..965f86b0a1 100644 --- a/domain-server/resources/web/header.html +++ b/domain-server/resources/web/header.html @@ -36,6 +36,7 @@
  • New Assignment
  • +
  • Content
  • Settings
  • diff --git a/domain-server/resources/web/settings/js/sweetalert.min.js b/domain-server/resources/web/js/sweetalert.min.js similarity index 100% rename from domain-server/resources/web/settings/js/sweetalert.min.js rename to domain-server/resources/web/js/sweetalert.min.js diff --git a/domain-server/resources/web/settings/index.shtml b/domain-server/resources/web/settings/index.shtml index 3eb7a53726..1812c52dad 100644 --- a/domain-server/resources/web/settings/index.shtml +++ b/domain-server/resources/web/settings/index.shtml @@ -99,7 +99,7 @@ - + diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 620b11d8ad..782c54419d 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1633,6 +1633,15 @@ QString pathForAssignmentScript(const QUuid& assignmentUUID) { return directory.absoluteFilePath(uuidStringWithoutCurlyBraces(assignmentUUID)); } +QString DomainServer::pathForRedirect(QString path) const { + // make sure the passed path has a leading slash + if (!path.startsWith('/')) { + path.insert(0, '/'); + } + + return "http://" + _hostname + ":" + QString::number(_httpManager.serverPort()) + path; +} + const QString URI_OAUTH = "/oauth"; bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) { const QString JSON_MIME_TYPE = "application/json"; @@ -1640,6 +1649,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url const QString URI_ASSIGNMENT = "/assignment"; const QString URI_NODES = "/nodes"; const QString URI_SETTINGS = "/settings"; + const QString URI_ENTITY_FILE_UPLOAD = "/content/upload"; 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}"; @@ -1869,6 +1879,25 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url // respond with a 200 code for successful upload connection->respond(HTTPConnection::StatusCode200); + return true; + } else if (url.path() == URI_ENTITY_FILE_UPLOAD) { + // this is an entity file upload, ask the HTTPConnection to parse the data + QList formData = connection->parseFormData(); + + Headers redirectHeaders; + + if (formData.size() > 0 && formData[0].second.size() > 0) { + // invoke our method to hand the new octree file off to the octree server + QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", + Qt::QueuedConnection, Q_ARG(QByteArray, formData[0].second)); + + // respond with a 200 for success + connection->respond(HTTPConnection::StatusCode200); + } else { + // respond with a 400 for failure + connection->respond(HTTPConnection::StatusCode400); + } + return true; } } else if (connection->requestOperation() == QNetworkAccessManager::DeleteOperation) { @@ -2159,8 +2188,7 @@ Headers DomainServer::setupCookieHeadersFromProfileReply(QNetworkReply* profileR cookieHeaders.insert("Set-Cookie", cookieString.toUtf8()); // redirect the user back to the homepage so they can present their cookie and be authenticated - QString redirectString = "http://" + _hostname + ":" + QString::number(_httpManager.serverPort()); - cookieHeaders.insert("Location", redirectString.toUtf8()); + cookieHeaders.insert("Location", pathForRedirect().toUtf8()); return cookieHeaders; } @@ -2560,3 +2588,20 @@ void DomainServer::setupGroupCacheRefresh() { _metaverseGroupCacheTimer->start(REFRESH_GROUPS_INTERVAL_MSECS); } } + +void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) { + // enumerate the nodes and find any octree type servers with active sockets + + auto limitedNodeList = DependencyManager::get(); + limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) { + return node->getType() == NodeType::EntityServer && node->getActiveSocket(); + }, [&octreeFile, limitedNodeList](const SharedNodePointer& octreeNode) { + // setup a packet to send to this octree server with the new octree file data + auto octreeFilePacketList = NLPacketList::create(PacketType::OctreeFileReplacement, QByteArray(), true, true); + octreeFilePacketList->write(octreeFile); + + qDebug() << "Sending an octree file replacement of" << octreeFile.size() << "bytes to" << octreeNode; + + limitedNodeList->sendPacketList(std::move(octreeFilePacketList), *octreeNode); + }); +} diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 4c5c42acee..63b82cb37d 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -100,6 +100,8 @@ private slots: void handleSuccessfulICEServerAddressUpdate(QNetworkReply& requestReply); void handleFailedICEServerAddressUpdate(QNetworkReply& requestReply); + void handleOctreeFileReplacement(QByteArray octreeFile); + signals: void iceServerChanged(); void userConnected(); @@ -161,6 +163,8 @@ private: void setupGroupCacheRefresh(); + QString pathForRedirect(QString path = QString()) const; + SubnetList _acSubnetWhitelist; DomainGatekeeper _gatekeeper; diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 4d58d70075..d7e4b1ae7c 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -194,7 +194,7 @@ link_hifi_libraries( recording fbx networking model-networking entities avatars audio audio-client animation script-engine physics render-utils entities-renderer avatars-renderer ui auto-updater - controllers plugins image + controllers plugins image trackers ui-plugins display-plugins input-plugins ${NON_ANDROID_LIBRARIES} ) @@ -202,7 +202,6 @@ link_hifi_libraries( # include the binary directory of render-utils for shader includes target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/libraries/render-utils") -#fixme find a way to express faceshift as a plugin target_bullet() target_opengl() @@ -210,10 +209,6 @@ if (NOT ANDROID) target_glew() endif () -if (WIN32 OR APPLE) - target_faceshift() -endif() - # perform standard include and linking for found externals foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) diff --git a/interface/resources/avatar/avatar-animation.json b/interface/resources/avatar/avatar-animation.json index 975f01855d..9efe3dd29b 100644 --- a/interface/resources/avatar/avatar-animation.json +++ b/interface/resources/avatar/avatar-animation.json @@ -50,6 +50,12 @@ "type": "inverseKinematics", "data": { "targets": [ + { + "jointName": "Hips", + "positionVar": "hipsPosition", + "rotationVar": "hipsRotation", + "typeVar": "hipsType" + }, { "jointName": "RightHand", "positionVar": "rightHandPosition", @@ -75,10 +81,10 @@ "typeVar": "leftFootType" }, { - "jointName": "Neck", - "positionVar": "neckPosition", - "rotationVar": "neckRotation", - "typeVar": "neckType" + "jointName": "Spine2", + "positionVar": "spine2Position", + "rotationVar": "spine2Rotation", + "typeVar": "spine2Type" }, { "jointName": "Head", @@ -91,20 +97,27 @@ "children": [] }, { - "id": "manipulatorOverlay", + "id": "hipsManipulatorOverlay", "type": "overlay", "data": { - "alpha": 1.0, - "boneSet": "spineOnly" + "alpha": 0.0, + "boneSet": "hipsOnly" }, "children": [ { - "id": "spineLean", + "id": "hipsManipulator", "type": "manipulator", "data": { "alpha": 0.0, + "alphaVar": "hipsManipulatorAlpha", "joints": [ - { "type": "absoluteRotation", "jointName": "Spine", "var": "lean" } + { + "jointName": "Hips", + "rotationType": "absolute", + "translationType": "absolute", + "rotationVar": "hipsManipulatorRotation", + "translationVar": "hipsManipulatorPosition" + } ] }, "children": [] diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 53285ea974..62eec9bc3c 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -61,6 +61,11 @@ { "from": "Standard.RightHand", "to": "Actions.RightHand" }, { "from": "Standard.LeftFoot", "to": "Actions.LeftFoot" }, - { "from": "Standard.RightFoot", "to": "Actions.RightFoot" } + { "from": "Standard.RightFoot", "to": "Actions.RightFoot" }, + + { "from": "Standard.Hips", "to": "Actions.Hips" }, + { "from": "Standard.Spine2", "to": "Actions.Spine2" }, + + { "from": "Standard.Head", "to": "Actions.Head" } ] } diff --git a/interface/resources/html/raiseAndLowerKeyboard.js b/interface/resources/html/raiseAndLowerKeyboard.js index 63e016c5d4..27ead23124 100644 --- a/interface/resources/html/raiseAndLowerKeyboard.js +++ b/interface/resources/html/raiseAndLowerKeyboard.js @@ -19,7 +19,7 @@ function shouldRaiseKeyboard() { var nodeName = document.activeElement.nodeName; var nodeType = document.activeElement.type; - if (nodeName === "INPUT" && (nodeType === "text" || nodeType === "number" || nodeType === "password") + if (nodeName === "INPUT" && ["email", "number", "password", "tel", "text", "url"].indexOf(nodeType) !== -1 || document.activeElement.nodeName === "TEXTAREA") { return true; } else { diff --git a/interface/resources/icons/tablet-icons/goto-msg.svg b/interface/resources/icons/tablet-icons/goto-msg.svg new file mode 100644 index 0000000000..9b576ab1bf --- /dev/null +++ b/interface/resources/icons/tablet-icons/goto-msg.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/interface/resources/qml/controls/TabletWebScreen.qml b/interface/resources/qml/controls/TabletWebScreen.qml new file mode 100644 index 0000000000..fec91046d8 --- /dev/null +++ b/interface/resources/qml/controls/TabletWebScreen.qml @@ -0,0 +1,132 @@ +import QtQuick 2.5 +import QtWebEngine 1.1 +import QtWebChannel 1.0 +import "../controls-uit" as HiFiControls +import HFTabletWebEngineProfile 1.0 + +Item { + property alias url: root.url + property alias scriptURL: root.userScriptUrl + property alias eventBridge: eventBridgeWrapper.eventBridge + property alias canGoBack: root.canGoBack; + property var goBack: root.goBack; + property alias urlTag: root.urlTag + property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false + property bool keyboardRaised: false + property bool punctuationMode: false + + // FIXME - Keyboard HMD only: Make Interface either set keyboardRaised property directly in OffscreenQmlSurface + // or provide HMDinfo object to QML in RenderableWebEntityItem and do the following. + /* + onKeyboardRaisedChanged: { + keyboardEnabled = HMDinfo.active; + } + */ + + QtObject { + id: eventBridgeWrapper + WebChannel.id: "eventBridgeWrapper" + property var eventBridge; + } + + property alias viewProfile: root.profile + + WebEngineView { + id: root + objectName: "webEngineView" + x: 0 + y: 0 + width: parent.width + height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height + + profile: HFTabletWebEngineProfile { + id: webviewProfile + storageName: "qmlTabletWebEngine" + } + + property string userScriptUrl: "" + + // creates a global EventBridge object. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // detects when to raise and lower virtual keyboard + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + // User script. + WebEngineScript { + id: userScript + sourceUrl: root.userScriptUrl + injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. + worldId: WebEngineScript.MainWorld + } + + property string urlTag: "noDownload=false"; + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] + + property string newUrl: "" + + webChannel.registeredObjects: [eventBridgeWrapper] + + Component.onCompleted: { + // Ensure the JS from the web-engine makes it to our logging + root.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); + }); + + root.profile.httpUserAgent = "Mozilla/5.0 Chrome (HighFidelityInterface)"; + } + + onFeaturePermissionRequested: { + grantFeaturePermission(securityOrigin, feature, true); + } + + onLoadingChanged: { + keyboardRaised = false; + punctuationMode = false; + keyboard.resetShiftMode(false); + + // Required to support clicking on "hifi://" links + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + var url = loadRequest.url.toString(); + url = (url.indexOf("?") >= 0) ? url + urlTag : url + "?" + urlTag; + if (urlHandler.canHandleUrl(url)) { + if (urlHandler.handleUrl(url)) { + root.stop(); + } + } + } + } + + onNewViewRequested:{ + // desktop is not defined for web-entities or tablet + if (typeof desktop !== "undefined") { + desktop.openBrowserWindow(request, profile); + } else { + tabletRoot.openBrowserWindow(request, profile); + } + } + } + + HiFiControls.Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + +} diff --git a/interface/resources/qml/controls/TabletWebView.qml b/interface/resources/qml/controls/TabletWebView.qml index 41cea625e2..04e784e2ba 100644 --- a/interface/resources/qml/controls/TabletWebView.qml +++ b/interface/resources/qml/controls/TabletWebView.qml @@ -23,8 +23,13 @@ Item { property bool keyboardRaised: false property bool punctuationMode: false property bool isDesktop: false - property bool removingPage: false - property bool loadingPage: false + property string initialPage: "" + property bool startingUp: true + property alias webView: webview + property alias profile: webview.profile + property bool remove: false + property var urlList: [] + property var forwardList: [] property int currentPage: -1 // used as a model for repeater @@ -76,11 +81,21 @@ Item { id: displayUrl color: hifi.colors.baseGray font.pixelSize: 12 + verticalAlignment: Text.AlignLeft anchors { top: nav.bottom horizontalCenter: parent.horizontalCenter; + left: parent.left + leftMargin: 20 } } + + + MouseArea { + anchors.fill: parent + preventStealing: true + propagateComposedEvents: true + } } ListModel { @@ -96,20 +111,24 @@ Item { } function goBack() { - if (webview.canGoBack && !isUrlLoaded(webview.url)) { - if (currentPage > 0) { - removingPage = true; - pagesModel.remove(currentPage); - } + if (webview.canGoBack) { + forwardList.push(webview.url); webview.goBack(); - } else if (currentPage > 0) { - removingPage = true; - pagesModel.remove(currentPage); + } else if (web.urlList.length > 0) { + var url = web.urlList.pop(); + loadUrl(url); + } else if (web.forwardList.length > 0) { + var url = web.forwardList.pop(); + loadUrl(url); + web.forwardList = []; } } - function closeWebEngine() { + if (remove) { + web.destroy(); + return; + } if (parentStackItem) { parentStackItem.pop(); } else { @@ -137,32 +156,42 @@ Item { view.setEnabled(true); } + function loadUrl(url) { + webview.url = url + web.url = webview.url; + web.address = webview.url; + } + + function onInitialPage(url) { + return (url === webview.url); + } + + function urlAppend(url) { - if (removingPage) { - removingPage = false; - return; - } var lurl = decodeURIComponent(url) if (lurl[lurl.length - 1] !== "/") { lurl = lurl + "/" } - if (currentPage === -1 || (pagesModel.get(currentPage).webUrl !== lurl && !timer.running)) { - timer.start(); - pagesModel.append({webUrl: lurl}); - } + web.urlList.push(url); + setBackButtonStatus(); } - onCurrentPageChanged: { - if (currentPage >= 0 && currentPage < pagesModel.count) { - timer.start(); - webview.url = pagesModel.get(currentPage).webUrl; - web.url = webview.url; - web.address = webview.url; + function setBackButtonStatus() { + if (web.urlList.length > 0 || webview.canGoBack) { + back.enabledColor = hifi.colors.darkGray; + back.enabled = true; + } else { + back.enabledColor = hifi.colors.baseGray; + back.enabled = false; } } onUrlChanged: { - gotoPage(url) + loadUrl(url); + if (startingUp) { + web.initialPage = webview.url; + startingUp = false; + } } QtObject { @@ -170,18 +199,7 @@ Item { WebChannel.id: "eventBridgeWrapper" property var eventBridge; } - - Timer { - id: timer - interval: 200 - running: false - repeat: false - onTriggered: timer.stop(); - } - - - - + WebEngineView { id: webview objectName: "webEngineView" @@ -221,6 +239,7 @@ Item { worldId: WebEngineScript.MainWorld } + property string urlTag: "noDownload=false"; userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] property string newUrl: "" @@ -247,9 +266,7 @@ Item { keyboard.resetShiftMode(false); // Required to support clicking on "hifi://" links if (WebEngineView.LoadStartedStatus == loadRequest.status) { - urlAppend(loadRequest.url.toString()); - loadingPage = true; - var url = loadRequest.url.toString(); + var url = loadRequest.url.toString(); if (urlHandler.canHandleUrl(url)) { if (urlHandler.handleUrl(url)) { root.stop(); @@ -260,9 +277,19 @@ Item { if (WebEngineView.LoadFailedStatus == loadRequest.status) { console.log(" Tablet WebEngineView failed to laod url: " + loadRequest.url.toString()); } - } + if (WebEngineView.LoadSucceededStatus == loadRequest.status) { + web.address = webview.url; + if (startingUp) { + web.initialPage = webview.url; + startingUp = false; + } + } + } + onNewViewRequested: { + var currentUrl = webview.url; + urlAppend(currentUrl); request.openIn(webview); } } diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index 52f277520f..04ff731a25 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -113,7 +113,7 @@ Item { if (typeof desktop !== "undefined") { desktop.openBrowserWindow(request, profile); } else { - console.log("onNewViewRequested: desktop not defined"); + tabletRoot.openBrowserWindow(request, profile); } } } diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index b72901fbdf..9617b41150 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -17,7 +17,7 @@ import QtGraphicalEffects 1.0 import "toolbars" import "../styles-uit" -Rectangle { +Item { id: root; property string userName: ""; property string placeName: ""; @@ -31,10 +31,11 @@ Rectangle { property bool drillDownToPlace: false; property bool showPlace: isConcurrency; - property string messageColor: hifi.colors.blueAccent; + property string messageColor: isAnnouncement ? "white" : hifi.colors.blueAccent; property string timePhrase: pastTime(timestamp); property int onlineUsers: 0; property bool isConcurrency: action === 'concurrency'; + property bool isAnnouncement: action === 'announcement'; property bool isStacked: !isConcurrency && drillDownToPlace; property int textPadding: 10; @@ -44,7 +45,7 @@ Rectangle { property int textSizeSmall: 18; property int stackShadowNarrowing: 5; property string defaultThumbnail: Qt.resolvedUrl("../../images/default-domain.gif"); - property int shadowHeight: 20; + property int shadowHeight: 10; HifiConstants { id: hifi } function pastTime(timestamp) { // Answer a descriptive string @@ -69,6 +70,44 @@ Rectangle { } property bool hasGif: imageUrl.indexOf('.gif') === (imageUrl.length - 4); + + function pluralize(count, singular, optionalPlural) { + return (count === 1) ? singular : (optionalPlural || (singular + "s")); + } + + DropShadow { + visible: isStacked; + anchors.fill: shadow1; + source: shadow1; + verticalOffset: 2; + radius: 4; + samples: 9; + color: hifi.colors.baseGrayShadow; + } + Rectangle { + id: shadow1; + visible: isStacked; + width: parent.width - stackShadowNarrowing; + height: shadowHeight; + anchors { + top: parent.bottom; + horizontalCenter: parent.horizontalCenter; + } + } + DropShadow { + anchors.fill: base; + source: base; + verticalOffset: 2; + radius: 4; + samples: 9; + color: hifi.colors.baseGrayShadow; + } + Rectangle { + id: base; + color: "white"; + anchors.fill: parent; + } + AnimatedImage { id: animation; // Always visible, to drive loading, but initially covered up by lobby during load. @@ -80,7 +119,7 @@ Rectangle { id: lobby; visible: !hasGif || (animation.status !== Image.Ready); width: parent.width - (isConcurrency ? 0 : (2 * smallMargin)); - height: parent.height - (isConcurrency ? 0 : smallMargin); + height: parent.height -(isAnnouncement ? smallMargin : messageHeight) - (isConcurrency ? 0 : smallMargin); source: thumbnail || defaultThumbnail; fillMode: Image.PreserveAspectCrop; anchors { @@ -95,41 +134,13 @@ Rectangle { } } } - Rectangle { - id: shadow1; - visible: isStacked; - width: parent.width - stackShadowNarrowing; - height: shadowHeight / 2; - anchors { - top: parent.bottom; - horizontalCenter: parent.horizontalCenter; - } - gradient: Gradient { - GradientStop { position: 0.0; color: "gray" } - GradientStop { position: 1.0; color: "white" } - } - } - Rectangle { - id: shadow2; - visible: isStacked; - width: shadow1.width - stackShadowNarrowing; - height: shadowHeight / 2; - anchors { - top: shadow1.bottom; - horizontalCenter: parent.horizontalCenter; - } - gradient: Gradient { - GradientStop { position: 0.0; color: "gray" } - GradientStop { position: 1.0; color: "white" } - } - } property int dropHorizontalOffset: 0; property int dropVerticalOffset: 1; property int dropRadius: 2; property int dropSamples: 9; property int dropSpread: 0; DropShadow { - visible: true; + visible: showPlace; // Do we have to check for whatever the modern equivalent is for desktop.gradientsSupported? source: place; anchors.fill: place; horizontalOffset: dropHorizontalOffset; @@ -139,12 +150,12 @@ Rectangle { color: hifi.colors.black; spread: dropSpread; } - RalewayLight { + RalewaySemiBold { id: place; visible: showPlace; text: placeName; color: hifi.colors.white; - size: 38; + size: textSize; elide: Text.ElideRight; // requires constrained width anchors { top: parent.top; @@ -154,56 +165,68 @@ Rectangle { } } Rectangle { - id: rectRow - z: 1 - width: message.width + (users.visible ? users.width + bottomRow.spacing : 0) - + (icon.visible ? icon.width + bottomRow.spacing: 0) + bottomRow.spacing; - height: messageHeight + 1; - radius: 25 - - anchors { - bottom: parent.bottom - left: parent.left - leftMargin: textPadding - bottomMargin: textPadding + id: lozenge; + visible: isAnnouncement; + color: hifi.colors.redHighlight; + anchors.fill: infoRow; + radius: lozenge.height / 2.0; + border.width: lozengeHot.containsMouse ? 4 : 0; + border.color: "white"; + } + Row { + id: infoRow; + Image { + id: icon; + source: isAnnouncement ? "../../images/Announce-Blast.svg" : "../../images/snap-icon.svg"; + width: 40; + height: 40; + visible: ((action === 'snapshot') || isAnnouncement) && (messageHeight >= 40); } - - Row { - id: bottomRow - FiraSansRegular { - id: users; - visible: isConcurrency; - text: onlineUsers; - size: textSize; - color: messageColor; - anchors.verticalCenter: message.verticalCenter; - } - Image { - id: icon; - source: "../../images/snap-icon.svg" - width: 40; - height: 40; - visible: action === 'snapshot'; - } - RalewayRegular { - id: message; - text: isConcurrency ? ((onlineUsers === 1) ? "person" : "people") : (drillDownToPlace ? "snapshots" : ("by " + userName)); - size: textSizeSmall; - color: messageColor; - elide: Text.ElideRight; // requires a width to be specified` - anchors { - bottom: parent.bottom; - bottomMargin: parent.spacing; - } - } - spacing: textPadding; - height: messageHeight; + FiraSansRegular { + id: users; + visible: isConcurrency || isAnnouncement; + text: onlineUsers; + size: textSize; + color: messageColor; + anchors.verticalCenter: message.verticalCenter; + } + RalewayRegular { + id: message; + visible: !isAnnouncement; + text: isConcurrency ? pluralize(onlineUsers, "person", "people") : (drillDownToPlace ? "snapshots" : ("by " + userName)); + size: textSizeSmall; + color: messageColor; + elide: Text.ElideRight; // requires a width to be specified` + width: root.width - textPadding + - (icon.visible ? icon.width + parent.spacing : 0) + - (users.visible ? users.width + parent.spacing : 0) + - (actionIcon.width + (2 * smallMargin)); anchors { bottom: parent.bottom; - left: parent.left; - leftMargin: 4 + bottomMargin: parent.spacing; } } + Column { + visible: isAnnouncement; + RalewayRegular { + text: pluralize(onlineUsers, "connection") + " "; // hack padding + size: textSizeSmall; + color: messageColor; + } + RalewayRegular { + text: pluralize(onlineUsers, "is here now", "are here now"); + size: textSizeSmall * 0.7; + color: messageColor; + } + } + spacing: textPadding; + height: messageHeight; + anchors { + bottom: parent.bottom; + left: parent.left; + leftMargin: textPadding; + bottomMargin: isAnnouncement ? textPadding : 0; + } } // These two can be supplied to provide hover behavior. // For example, AddressBarDialog provides functions that set the current list view item @@ -218,39 +241,37 @@ Rectangle { onEntered: hoverThunk(); onExited: unhoverThunk(); } - Rectangle { - id: rectIcon - z: 1 - width: 32 - height: 32 - radius: 15 + StateImage { + id: actionIcon; + visible: !isAnnouncement; + imageURL: "../../images/info-icon-2-state.svg"; + size: 30; + buttonState: messageArea.containsMouse ? 1 : 0; anchors { bottom: parent.bottom; right: parent.right; - bottomMargin: textPadding; - rightMargin: textPadding; - } - - StateImage { - id: actionIcon; - imageURL: "../../images/info-icon-2-state.svg"; - size: 32; - buttonState: messageArea.containsMouse ? 1 : 0; - anchors { - bottom: parent.bottom; - right: parent.right; - //margins: smallMargin; - } + margins: smallMargin; } } - + function go() { + goFunction(drillDownToPlace ? ("/places/" + placeName) : ("/user_stories/" + storyId)); + } MouseArea { id: messageArea; - width: rectIcon.width; - height: rectIcon.height; - anchors.fill: rectIcon + visible: !isAnnouncement; + width: parent.width; + height: messageHeight; + anchors.top: lobby.bottom; acceptedButtons: Qt.LeftButton; - onClicked: goFunction(drillDownToPlace ? ("/places/" + placeName) : ("/user_stories/" + storyId)); + onClicked: go(); + hoverEnabled: true; + } + MouseArea { + id: lozengeHot; + visible: lozenge.visible; + anchors.fill: lozenge; + acceptedButtons: Qt.LeftButton; + onClicked: go(); hoverEnabled: true; } } diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml new file mode 100644 index 0000000000..fd3472b7be --- /dev/null +++ b/interface/resources/qml/hifi/Feed.qml @@ -0,0 +1,247 @@ +// +// Feed.qml +// qml/hifi +// +// Displays a particular type of feed +// +// Created by Howard Stearns on 4/18/2017 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.5 +import QtGraphicalEffects 1.0 +import "toolbars" +import "../styles-uit" + +Column { + id: root; + visible: false; + + property int cardWidth: 212; + property int cardHeight: 152; + property int textPadding: 10; + property int smallMargin: 4; + property int messageHeight: 40; + property int textSize: 24; + property int textSizeSmall: 18; + property int stackShadowNarrowing: 5; + property int stackedCardShadowHeight: 4; + property int labelSize: 20; + + property string metaverseServerUrl: ''; + property string actions: 'snapshot'; + // sendToScript doesn't get wired until after everything gets created. So we have to queue fillDestinations on nextTick. + Component.onCompleted: delay.start(); + property string labelText: actions; + property string filter: ''; + onFilterChanged: filterChoicesByText(); + property var goFunction: null; + property var rpc: null; + + HifiConstants { id: hifi } + ListModel { id: suggestions; } + + function resolveUrl(url) { + return (url.indexOf('/') === 0) ? (metaverseServerUrl + url) : url; + } + function makeModelData(data) { // create a new obj from data + // ListModel elements will only ever have those properties that are defined by the first obj that is added. + // So here we make sure that we have all the properties we need, regardless of whether it is a place data or user story. + var name = data.place_name, + tags = data.tags || [data.action, data.username], + description = data.description || "", + thumbnail_url = data.thumbnail_url || ""; + if (actions === 'concurrency,snapshot') { + // A temporary hack for simulating announcements. We won't use this in production, but if requested, we'll use this data like announcements. + data.details.connections = 4; + data.action = 'announcement'; + } + return { + place_name: name, + username: data.username || "", + path: data.path || "", + created_at: data.created_at || "", + action: data.action || "", + thumbnail_url: resolveUrl(thumbnail_url), + image_url: resolveUrl(data.details && data.details.image_url), + + metaverseId: (data.id || "").toString(), // Some are strings from server while others are numbers. Model objects require uniformity. + + tags: tags, + description: description, + online_users: data.details.connections || data.details.concurrency || 0, + drillDownToPlace: false, + + searchText: [name].concat(tags, description || []).join(' ').toUpperCase() + } + } + property var allStories: []; + property var placeMap: ({}); // Used for making stacks. + property int requestId: 0; + function handleError(url, error, data, cb) { // cb(error) and answer truthy if needed, else falsey + if (!error && (data.status === 'success')) { + return; + } + if (!error) { // Create a message from the data + error = data.status + ': ' + data.error; + } + if (typeof(error) === 'string') { // Make a proper Error object + error = new Error(error); + } + error.message += ' in ' + url; // Include the url. + cb(error); + return true; + } + function getUserStoryPage(pageNumber, cb, cb1) { // cb(error) after all pages of domain data have been added to model + // If supplied, cb1 will be run after the first page IFF it is not the last, for responsiveness. + var options = [ + 'now=' + new Date().toISOString(), + 'include_actions=' + actions, + 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), + 'require_online=true', + 'protocol=' + encodeURIComponent(AddressManager.protocolVersion()), + 'page=' + pageNumber + ]; + var url = metaverseBase + 'user_stories?' + options.join('&'); + var thisRequestId = ++requestId; + rpc('request', url, function (error, data) { + if (thisRequestId !== requestId) { + error = 'stale'; + } + if (handleError(url, error, data, cb)) { + return; // abandon stale requests + } + allStories = allStories.concat(data.user_stories.map(makeModelData)); + if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now + if ((pageNumber === 1) && cb1) { + cb1(); + } + return getUserStoryPage(pageNumber + 1, cb); + } + cb(); + }); + } + property var delay: Timer { // No setTimeout or nextTick in QML. + interval: 0; + onTriggered: fillDestinations(); + } + function fillDestinations() { // Public + function report(label, error) { + console.log(label, actions, error || 'ok', allStories.length, 'filtered to', suggestions.count); + } + var filter = makeFilteredStoryProcessor(), counter = 0; + allStories = []; + suggestions.clear(); + placeMap = {}; + getUserStoryPage(1, function (error) { + allStories.slice(counter).forEach(filter); + report('user stories update', error); + root.visible = !!suggestions.count; + }, function () { // If there's more than a page, put what we have in the model right away, keeping track of how many are processed. + allStories.forEach(function (story) { + counter++; + filter(story); + root.visible = !!suggestions.count; + }); + report('user stories'); + }); + } + function identity(x) { + return x; + } + function makeFilteredStoryProcessor() { // answer a function(storyData) that adds it to suggestions if it matches + var words = filter.toUpperCase().split(/\s+/).filter(identity); + function suggestable(story) { + if (story.action === 'snapshot') { + return true; + } + return story.place_name !== AddressManager.placename; // Not our entry, but do show other entry points to current domain. + } + function matches(story) { + if (!words.length) { + return suggestable(story); + } + return words.every(function (word) { + return story.searchText.indexOf(word) >= 0; + }); + } + function addToSuggestions(place) { + var collapse = ((actions === 'concurrency,snapshot') && (place.action !== 'concurrency')) || (place.action === 'announcement'); + if (collapse) { + var existing = placeMap[place.place_name]; + if (existing) { + existing.drillDownToPlace = true; + return; + } + } + suggestions.append(place); + if (collapse) { + placeMap[place.place_name] = suggestions.get(suggestions.count - 1); + } else if (place.action === 'concurrency') { + suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories). + } + } + return function (story) { + if (matches(story)) { + addToSuggestions(story); + } + }; + } + function filterChoicesByText() { + suggestions.clear(); + placeMap = {}; + allStories.forEach(makeFilteredStoryProcessor()); + root.visible = !!suggestions.count; + } + + RalewayBold { + id: label; + text: labelText; + color: hifi.colors.blueAccent; + size: labelSize; + } + ListView { + id: scroll; + model: suggestions; + orientation: ListView.Horizontal; + highlightMoveDuration: -1; + highlightMoveVelocity: -1; + highlight: Rectangle { color: "transparent"; border.width: 4; border.color: hifiStyleConstants.colors.primaryHighlight; z: 1; } + currentIndex: -1; + + spacing: 12; + width: parent.width; + height: cardHeight + stackedCardShadowHeight; + delegate: Card { + id: card; + width: cardWidth; + height: cardHeight; + goFunction: root.goFunction; + userName: model.username; + placeName: model.place_name; + hifiUrl: model.place_name + model.path; + thumbnail: model.thumbnail_url; + imageUrl: model.image_url; + action: model.action; + timestamp: model.created_at; + onlineUsers: model.online_users; + storyId: model.metaverseId; + drillDownToPlace: model.drillDownToPlace; + + textPadding: root.textPadding; + smallMargin: root.smallMargin; + messageHeight: root.messageHeight; + textSize: root.textSize; + textSizeSmall: root.textSizeSmall; + stackShadowNarrowing: root.stackShadowNarrowing; + shadowHeight: root.stackedCardShadowHeight; + + hoverThunk: function () { scroll.currentIndex = index; } + unhoverThunk: function () { scroll.currentIndex = -1; } + } + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index b78254a4a0..7159b078ee 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -30,18 +30,33 @@ StackView { width: parent !== null ? parent.width : undefined height: parent !== null ? parent.height : undefined property var eventBridge; - property var allStories: []; - property int cardWidth: 460; - property int cardHeight: 320; + property int cardWidth: 212; + property int cardHeight: 152; property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/"; - property var tablet: null; + // This version only implements rpc(method, parameters, callback(error, result)) calls initiated from here, not initiated from .js, nor "notifications". + property var rpcCalls: ({}); + property var rpcCounter: 0; + signal sendToScript(var message); + function rpc(method, parameters, callback) { + rpcCalls[rpcCounter] = callback; + var message = {method: method, params: parameters, id: rpcCounter++, jsonrpc: "2.0"}; + sendToScript(message); + } + function fromScript(message) { + var callback = rpcCalls[message.id]; + if (!callback) { + console.log('No callback for message fromScript', JSON.stringify(message)); + return; + } + delete rpcCalls[message.id]; + callback(message.error, message.result); + } + Component { id: tabletWebView; TabletWebView {} } Component.onCompleted: { - fillDestinations(); updateLocationText(false); - fillDestinations(); addressLine.focus = !HMD.active; root.parentChanged.connect(center); center(); @@ -57,7 +72,7 @@ StackView { } - function resetAfterTeleport() { + function resetAfterTeleport() { //storyCardFrame.shown = root.shown = false; } function goCard(targetString) { @@ -157,7 +172,7 @@ StackView { left: parent.left; } - HifiStyles.RalewayLight { + HifiStyles.RalewayRegular { id: notice; font.pixelSize: hifi.fonts.pixelSize * 0.7; anchors { @@ -190,7 +205,6 @@ StackView { } font.pixelSize: hifi.fonts.pixelSize * 0.75 onTextChanged: { - filterChoicesByText(); updateLocationText(text.length > 0); } onAccepted: { @@ -225,109 +239,80 @@ StackView { } } } + Rectangle { - id: topBar - height: 37 - color: hifiStyleConstants.colors.white - - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.topMargin: 0 - anchors.top: addressBar.bottom - - Row { - id: thing - spacing: 5 * hifi.layout.spacing - - anchors { - top: parent.top; - left: parent.left - leftMargin: 25 - } - - TabletTextButton { - id: allTab; - text: "ALL"; - property string includeActions: 'snapshot,concurrency'; - selected: allTab === selectedTab; - action: tabSelect; - } - - TabletTextButton { - id: placeTab; - text: "PLACES"; - property string includeActions: 'concurrency'; - selected: placeTab === selectedTab; - action: tabSelect; - - } - - TabletTextButton { - id: snapTab; - text: "SNAP"; - property string includeActions: 'snapshot'; - selected: snapTab === selectedTab; - action: tabSelect; + id: bgMain; + anchors { + top: addressBar.bottom; + bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom; + left: parent.left; + right: parent.right; + } + Rectangle { + id: addressShadow; + width: parent.width; + height: 42 - 33; + gradient: Gradient { + GradientStop { position: 0.0; color: "gray" } + GradientStop { position: 1.0; color: "white" } } } - - } - - Rectangle { - id: bgMain - color: hifiStyleConstants.colors.white - anchors.bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom - anchors.bottomMargin: 0 - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - anchors.top: topBar.bottom - anchors.topMargin: 0 - - ListModel { id: suggestions } - - ListView { - id: scroll - - property int stackedCardShadowHeight: 0; - clip: true - spacing: 14 + Rectangle { // Column margins require QtQuick 2.7, which we don't use yet. + id: column; + property real pad: 10; + width: bgMain.width - column.pad; + height: stack.height; + color: "transparent"; anchors { - bottom: parent.bottom - top: parent.top - left: parent.left - right: parent.right - leftMargin: 10 + left: parent.left; + leftMargin: column.pad; + top: addressShadow.bottom; + topMargin: column.pad; } - - model: suggestions - orientation: ListView.Vertical - - delegate: Card { - width: cardWidth; - height: cardHeight; - goFunction: goCard; - userName: model.username; - placeName: model.place_name; - hifiUrl: model.place_name + model.path; - thumbnail: model.thumbnail_url; - imageUrl: model.image_url; - action: model.action; - timestamp: model.created_at; - onlineUsers: model.online_users; - storyId: model.metaverseId; - drillDownToPlace: model.drillDownToPlace; - shadowHeight: scroll.stackedCardShadowHeight; - hoverThunk: function () { scroll.currentIndex = index; } - unhoverThunk: function () { scroll.currentIndex = -1; } + Column { + id: stack; + width: column.width; + spacing: 33 - places.labelSize; + Feed { + id: happeningNow; + width: parent.width; + cardWidth: 312 + (2 * 4); + cardHeight: 163 + (2 * 4); + metaverseServerUrl: addressBarDialog.metaverseServerUrl; + labelText: 'HAPPENING NOW'; + actions: 'announcement'; + filter: addressLine.text; + goFunction: goCard; + rpc: root.rpc; + } + Feed { + id: places; + width: parent.width; + cardWidth: 210; + cardHeight: 110 + messageHeight; + messageHeight: 44; + metaverseServerUrl: addressBarDialog.metaverseServerUrl; + labelText: 'PLACES'; + actions: 'concurrency'; + filter: addressLine.text; + goFunction: goCard; + rpc: root.rpc; + } + Feed { + id: snapshots; + width: parent.width; + cardWidth: 143 + (2 * 4); + cardHeight: 75 + messageHeight + 4; + messageHeight: 32; + textPadding: 6; + metaverseServerUrl: addressBarDialog.metaverseServerUrl; + labelText: 'RECENT SNAPS'; + actions: 'snapshot'; + filter: addressLine.text; + goFunction: goCard; + rpc: root.rpc; + } } - - highlightMoveDuration: -1; - highlightMoveVelocity: -1; - highlight: Rectangle { color: "transparent"; border.width: 4; border.color: hifiStyleConstants.colors.blueHighlight; z: 1; } } } @@ -365,175 +350,13 @@ StackView { } - function getRequest(url, cb) { // cb(error, responseOfCorrectContentType) of url. General for 'get' text/html/json, but without redirects. - // TODO: make available to other .qml. - var request = new XMLHttpRequest(); - // QT bug: apparently doesn't handle onload. Workaround using readyState. - request.onreadystatechange = function () { - var READY_STATE_DONE = 4; - var HTTP_OK = 200; - if (request.readyState >= READY_STATE_DONE) { - var error = (request.status !== HTTP_OK) && request.status.toString() + ':' + request.statusText, - response = !error && request.responseText, - contentType = !error && request.getResponseHeader('content-type'); - if (!error && contentType.indexOf('application/json') === 0) { - try { - response = JSON.parse(response); - } catch (e) { - error = e; - } - } - cb(error, response); - } - }; - request.open("GET", url, true); - request.send(); - } - - function identity(x) { - return x; - } - - function handleError(url, error, data, cb) { // cb(error) and answer truthy if needed, else falsey - if (!error && (data.status === 'success')) { - return; - } - if (!error) { // Create a message from the data - error = data.status + ': ' + data.error; - } - if (typeof(error) === 'string') { // Make a proper Error object - error = new Error(error); - } - error.message += ' in ' + url; // Include the url. - cb(error); - return true; - } - - - function resolveUrl(url) { - return (url.indexOf('/') === 0) ? (addressBarDialog.metaverseServerUrl + url) : url; - } - - function makeModelData(data) { // create a new obj from data - // ListModel elements will only ever have those properties that are defined by the first obj that is added. - // So here we make sure that we have all the properties we need, regardless of whether it is a place data or user story. - var name = data.place_name, - tags = data.tags || [data.action, data.username], - description = data.description || "", - thumbnail_url = data.thumbnail_url || ""; - return { - place_name: name, - username: data.username || "", - path: data.path || "", - created_at: data.created_at || "", - action: data.action || "", - thumbnail_url: resolveUrl(thumbnail_url), - image_url: resolveUrl(data.details.image_url), - - metaverseId: (data.id || "").toString(), // Some are strings from server while others are numbers. Model objects require uniformity. - - tags: tags, - description: description, - online_users: data.details.concurrency || 0, - drillDownToPlace: false, - - searchText: [name].concat(tags, description || []).join(' ').toUpperCase() - } - } - function suggestable(place) { - if (place.action === 'snapshot') { - return true; - } - return (place.place_name !== AddressManager.placename); // Not our entry, but do show other entry points to current domain. - } - property var selectedTab: allTab; - function tabSelect(textButton) { - selectedTab = textButton; - fillDestinations(); - } - property var placeMap: ({}); - function addToSuggestions(place) { - var collapse = allTab.selected && (place.action !== 'concurrency'); - if (collapse) { - var existing = placeMap[place.place_name]; - if (existing) { - existing.drillDownToPlace = true; - return; - } - } - suggestions.append(place); - if (collapse) { - placeMap[place.place_name] = suggestions.get(suggestions.count - 1); - } else if (place.action === 'concurrency') { - suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories). - } - } - property int requestId: 0; - function getUserStoryPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model - var options = [ - 'now=' + new Date().toISOString(), - 'include_actions=' + selectedTab.includeActions, - 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), - 'require_online=true', - 'protocol=' + encodeURIComponent(AddressManager.protocolVersion()), - 'page=' + pageNumber - ]; - var url = metaverseBase + 'user_stories?' + options.join('&'); - var thisRequestId = ++requestId; - getRequest(url, function (error, data) { - if ((thisRequestId !== requestId) || handleError(url, error, data, cb)) { - return; - } - var stories = data.user_stories.map(function (story) { // explicit single-argument function - return makeModelData(story, url); - }); - allStories = allStories.concat(stories); - stories.forEach(makeFilteredPlaceProcessor()); - if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now - return getUserStoryPage(pageNumber + 1, cb); - } - cb(); - }); - } - function makeFilteredPlaceProcessor() { // answer a function(placeData) that adds it to suggestions if it matches - var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity), - data = allStories; - function matches(place) { - if (!words.length) { - return suggestable(place); - } - return words.every(function (word) { - return place.searchText.indexOf(word) >= 0; - }); - } - return function (place) { - if (matches(place)) { - addToSuggestions(place); - } - }; - } - function filterChoicesByText() { - suggestions.clear(); - placeMap = {}; - allStories.forEach(makeFilteredPlaceProcessor()); - } - - function fillDestinations() { - allStories = []; - suggestions.clear(); - placeMap = {}; - getUserStoryPage(1, function (error) { - console.log('user stories query', error || 'ok', allStories.length); - }); - } - function updateLocationText(enteringAddress) { if (enteringAddress) { notice.text = "Go To a place, @user, path, or network address:"; notice.color = hifiStyleConstants.colors.baseGrayHighlight; } else { - notice.text = AddressManager.isConnected ? "Your location:" : "Not Connected"; - notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.baseGrayHighlight : hifiStyleConstants.colors.redHighlight; + notice.text = AddressManager.isConnected ? "YOUR LOCATION" : "NOT CONNECTED"; + notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.blueHighlight : hifiStyleConstants.colors.redHighlight; // Display hostname, which includes ip address, localhost, and other non-placenames. location.text = (AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : ''); } diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 31e6174563..33af7da1ae 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -1,7 +1,9 @@ import QtQuick 2.0 import Hifi 1.0 import QtQuick.Controls 1.4 +import HFTabletWebEngineProfile 1.0 import "../../dialogs" +import "../../controls" Item { id: tabletRoot @@ -11,6 +13,7 @@ Item { property var rootMenu; property var openModal: null; property var openMessage: null; + property var openBrowser: null; property string subMenu: "" signal showDesktop(); property bool shown: true @@ -87,13 +90,18 @@ Item { loader.item.gotoPreviousApp = true; } } + + function loadWebBase() { + loader.source = ""; + loader.source = "TabletWebView.qml"; + } function returnToPreviousApp() { tabletApps.remove(currentApp); var isWebPage = tabletApps.get(currentApp).isWebUrl; if (isWebPage) { - var webUrl = tabletApps.get(currentApp).appWebUrl; - var scriptUrl = tabletApps.get(currentApp).scriptUrl; + var webUrl = tabletApps.get(currentApp).appWebUrl; + var scriptUrl = tabletApps.get(currentApp).scriptUrl; loadSource("TabletWebView.qml"); loadWebUrl(webUrl, scriptUrl); } else { @@ -101,6 +109,16 @@ Item { } } + function openBrowserWindow(request, profile) { + var component = Qt.createComponent("../../controls/TabletWebView.qml"); + var newWindow = component.createObject(tabletRoot); + newWindow.eventBridge = tabletRoot.eventBridge; + newWindow.remove = true; + newWindow.profile = profile; + request.openIn(newWindow.webView); + tabletRoot.openBrowser = newWindow; + } + function loadWebUrl(url, injectedJavaScriptUrl) { tabletApps.clear(); loader.item.url = url; @@ -180,6 +198,11 @@ Item { openModal.destroy(); openModal = null; } + + if (openBrowser) { + openBrowser.destroy(); + openBrowser = null; + } } } diff --git a/interface/resources/qml/hifi/tablet/TabletWebView.qml b/interface/resources/qml/hifi/tablet/TabletWebView.qml index 0f697d634e..ff6be0480f 100644 --- a/interface/resources/qml/hifi/tablet/TabletWebView.qml +++ b/interface/resources/qml/hifi/tablet/TabletWebView.qml @@ -3,7 +3,7 @@ import QtWebEngine 1.2 import "../../controls" as Controls -Controls.WebView { +Controls.TabletWebScreen { } diff --git a/interface/resources/qml/hifi/tablet/WindowRoot.qml b/interface/resources/qml/hifi/tablet/WindowRoot.qml index 5f842df7b7..470fd4a830 100644 --- a/interface/resources/qml/hifi/tablet/WindowRoot.qml +++ b/interface/resources/qml/hifi/tablet/WindowRoot.qml @@ -38,6 +38,11 @@ Windows.ScrollingWindow { loader.source = url; } + function loadWebBase() { + loader.source = ""; + loader.source = "WindowWebView.qml"; + } + function loadWebUrl(url, injectedJavaScriptUrl) { loader.item.url = url; loader.item.scriptURL = injectedJavaScriptUrl; diff --git a/interface/resources/qml/hifi/tablet/WindowWebView.qml b/interface/resources/qml/hifi/tablet/WindowWebView.qml new file mode 100644 index 0000000000..0f697d634e --- /dev/null +++ b/interface/resources/qml/hifi/tablet/WindowWebView.qml @@ -0,0 +1,10 @@ +import QtQuick 2.0 +import QtWebEngine 1.2 + +import "../../controls" as Controls + +Controls.WebView { + +} + + diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 39a4b8ee7c..a8dbb2f4fc 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -128,6 +128,7 @@ #include #include #include +#include #include "AudioClient.h" @@ -136,8 +137,6 @@ #include "avatar/ScriptAvatar.h" #include "CrashHandler.h" #include "devices/DdeFaceTracker.h" -#include "devices/EyeTracker.h" -#include "devices/Faceshift.h" #include "devices/Leapmotion.h" #include "DiscoverabilityManager.h" #include "GLCanvas.h" @@ -480,7 +479,6 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -535,6 +533,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -797,7 +796,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); - connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); + connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &Application::clearDomainAvatars); connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { getOverlays().deleteOverlay(getTabletScreenID()); getOverlays().deleteOverlay(getTabletHomeButtonID()); @@ -1209,10 +1208,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo this->installEventFilter(this); - // initialize our face trackers after loading the menu settings - auto faceshiftTracker = DependencyManager::get(); - faceshiftTracker->init(); - connect(faceshiftTracker.data(), &FaceTracker::muteToggled, this, &Application::faceTrackerMuteToggled); #ifdef HAVE_DDE auto ddeTracker = DependencyManager::get(); ddeTracker->init(); @@ -2052,6 +2047,7 @@ void Application::initializeUi() { rootContext->setContextProperty("Scene", DependencyManager::get().data()); rootContext->setContextProperty("Render", _renderEngine->getConfiguration().get()); rootContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); + rootContext->setContextProperty("Snapshot", DependencyManager::get().data()); rootContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); @@ -3624,20 +3620,13 @@ ivec2 Application::getMouse() const { } FaceTracker* Application::getActiveFaceTracker() { - auto faceshift = DependencyManager::get(); auto dde = DependencyManager::get(); - return (dde->isActive() ? static_cast(dde.data()) : - (faceshift->isActive() ? static_cast(faceshift.data()) : nullptr)); + return dde->isActive() ? static_cast(dde.data()) : nullptr; } FaceTracker* Application::getSelectedFaceTracker() { FaceTracker* faceTracker = nullptr; -#ifdef HAVE_FACESHIFT - if (Menu::getInstance()->isOptionChecked(MenuOption::Faceshift)) { - faceTracker = DependencyManager::get().data(); - } -#endif #ifdef HAVE_DDE if (Menu::getInstance()->isOptionChecked(MenuOption::UseCamera)) { faceTracker = DependencyManager::get().data(); @@ -3647,15 +3636,8 @@ FaceTracker* Application::getSelectedFaceTracker() { } void Application::setActiveFaceTracker() const { -#if defined(HAVE_FACESHIFT) || defined(HAVE_DDE) - bool isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking); -#endif -#ifdef HAVE_FACESHIFT - auto faceshiftTracker = DependencyManager::get(); - faceshiftTracker->setIsMuted(isMuted); - faceshiftTracker->setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::Faceshift) && !isMuted); -#endif #ifdef HAVE_DDE + bool isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking); bool isUsingDDE = Menu::getInstance()->isOptionChecked(MenuOption::UseCamera); Menu::getInstance()->getActionForOption(MenuOption::BinaryEyelidControl)->setVisible(isUsingDDE); Menu::getInstance()->getActionForOption(MenuOption::CoupleEyelids)->setVisible(isUsingDDE); @@ -4373,7 +4355,13 @@ void Application::update(float deltaTime) { controller::InputCalibrationData calibrationData = { myAvatar->getSensorToWorldMatrix(), createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()), - myAvatar->getHMDSensorMatrix() + myAvatar->getHMDSensorMatrix(), + myAvatar->getCenterEyeCalibrationMat(), + myAvatar->getHeadCalibrationMat(), + myAvatar->getSpine2CalibrationMat(), + myAvatar->getHipsCalibrationMat(), + myAvatar->getLeftFootCalibrationMat(), + myAvatar->getRightFootCalibrationMat() }; InputPluginPointer keyboardMousePlugin; @@ -4421,6 +4409,13 @@ void Application::update(float deltaTime) { controller::Pose rightFootPose = userInputMapper->getPoseState(controller::Action::RIGHT_FOOT); myAvatar->setFootControllerPosesInSensorFrame(leftFootPose.transform(avatarToSensorMatrix), rightFootPose.transform(avatarToSensorMatrix)); + controller::Pose hipsPose = userInputMapper->getPoseState(controller::Action::HIPS); + controller::Pose spine2Pose = userInputMapper->getPoseState(controller::Action::SPINE2); + myAvatar->setSpineControllerPosesInSensorFrame(hipsPose.transform(avatarToSensorMatrix), spine2Pose.transform(avatarToSensorMatrix)); + + controller::Pose headPose = userInputMapper->getPoseState(controller::Action::HEAD); + myAvatar->setHeadControllerPoseInSensorFrame(headPose.transform(avatarToSensorMatrix)); + updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... updateDialogs(deltaTime); // update various stats dialogs if present @@ -5131,7 +5126,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se } void Application::resetSensors(bool andReload) { - DependencyManager::get()->reset(); DependencyManager::get()->reset(); DependencyManager::get()->reset(); getActiveDisplayPlugin()->resetSensors(); @@ -5177,7 +5171,6 @@ void Application::clearDomainOctreeDetails() { qCDebug(interfaceapp) << "Clearing domain octree details..."; resetPhysicsReadyInformation(); - getMyAvatar()->setAvatarEntityDataChanged(true); // to recreate worn entities // reset our node to stats and node to jurisdiction maps... since these must be changing... _entityServerJurisdictions.withWriteLock([&] { @@ -5196,14 +5189,18 @@ void Application::clearDomainOctreeDetails() { skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT); _recentlyClearedDomain = true; - - DependencyManager::get()->clearOtherAvatars(); + DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); DependencyManager::get()->clearUnusedResources(); } +void Application::clearDomainAvatars() { + getMyAvatar()->setAvatarEntityDataChanged(true); // to recreate worn entities + DependencyManager::get()->clearOtherAvatars(); +} + void Application::domainChanged(const QString& domainHostname) { updateWindowTitle(); // disable physics until we have enough information about our new location to not cause craziness. @@ -5273,33 +5270,8 @@ void Application::nodeKilled(SharedNodePointer node) { if (node->getType() == NodeType::AudioMixer) { QMetaObject::invokeMethod(DependencyManager::get().data(), "audioMixerKilled"); } else if (node->getType() == NodeType::EntityServer) { - QUuid nodeUUID = node->getUUID(); - // see if this is the first we've heard of this node... - _entityServerJurisdictions.withReadLock([&] { - if (_entityServerJurisdictions.find(nodeUUID) == _entityServerJurisdictions.end()) { - return; - } - - auto rootCode = _entityServerJurisdictions[nodeUUID].getRootOctalCode(); - VoxelPositionSize rootDetails; - voxelDetailsForCode(rootCode.get(), rootDetails); - - qCDebug(interfaceapp, "model server going away...... v[%f, %f, %f, %f]", - (double)rootDetails.x, (double)rootDetails.y, (double)rootDetails.z, (double)rootDetails.s); - - }); - - // If the model server is going away, remove it from our jurisdiction map so we don't send voxels to a dead server - _entityServerJurisdictions.withWriteLock([&] { - _entityServerJurisdictions.erase(_entityServerJurisdictions.find(nodeUUID)); - }); - - // also clean up scene stats for that server - _octreeServerSceneStats.withWriteLock([&] { - if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { - _octreeServerSceneStats.erase(nodeUUID); - } - }); + // we lost an entity server, clear all of the domain octree details + clearDomainOctreeDetails(); } else if (node->getType() == NodeType::AvatarMixer) { // our avatar mixer has gone away - clear the hash of avatars DependencyManager::get()->clearOtherAvatars(); @@ -5504,6 +5476,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("Snapshot", DependencyManager::get().data()); scriptEngine->registerGlobalObject("AudioDevice", AudioDeviceScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get()->getStats().data()); scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get().data()); @@ -6448,7 +6421,7 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa // Get a screenshot and save it QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); // If we're not doing an animated snapshot as well... - if (!includeAnimated || !(SnapshotAnimated::alsoTakeAnimatedSnapshot.get())) { + if (!includeAnimated) { // Tell the dependency manager that the capture of the still snapshot has taken place. emit DependencyManager::get()->stillSnapshotTaken(path, notify); } else { diff --git a/interface/src/Application.h b/interface/src/Application.h index dff1de2860..041f1f8930 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -409,6 +409,7 @@ public slots: private slots: void showDesktop(); void clearDomainOctreeDetails(); + void clearDomainAvatars(); void aboutToQuit(); void resettingDomain(); diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 98bfa9c0c7..36f6d8633e 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -23,6 +23,8 @@ #include "DiscoverabilityManager.h" #include "Menu.h" +#include + const Discoverability::Mode DEFAULT_DISCOVERABILITY_MODE = Discoverability::Friends; DiscoverabilityManager::DiscoverabilityManager() : @@ -37,6 +39,13 @@ const QString API_USER_HEARTBEAT_PATH = "/api/v1/user/heartbeat"; const QString SESSION_ID_KEY = "session_id"; void DiscoverabilityManager::updateLocation() { + // since we store the last location and compare it to + // the current one in this function, we need to do this in + // the object's main thread (or use a mutex) + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "updateLocation"); + return; + } auto accountManager = DependencyManager::get(); auto addressManager = DependencyManager::get(); auto& domainHandler = DependencyManager::get()->getDomainHandler(); @@ -143,7 +152,7 @@ void DiscoverabilityManager::removeLocation() { void DiscoverabilityManager::setDiscoverabilityMode(Discoverability::Mode discoverabilityMode) { if (static_cast(_mode.get()) != discoverabilityMode) { - + // update the setting to the new value _mode.set(static_cast(discoverabilityMode)); updateLocation(); // update right away diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index c99178d8cc..fcd539ca7d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -34,7 +34,6 @@ #include "avatar/AvatarManager.h" #include "AvatarBookmarks.h" #include "devices/DdeFaceTracker.h" -#include "devices/Faceshift.h" #include "MainWindow.h" #include "render/DrawStatus.h" #include "scripting/MenuScriptingInterface.h" @@ -156,6 +155,8 @@ Menu::Menu() { // Audio > Show Level Meter addCheckableActionToQMenuAndActionHash(audioMenu, MenuOption::AudioTools, 0, false); + addCheckableActionToQMenuAndActionHash(audioMenu, MenuOption::AudioNoiseReduction, 0, true, + audioIO.data(), SLOT(toggleAudioNoiseReduction())); // Avatar menu ---------------------------------- MenuWrapper* avatarMenu = addMenu("Avatar"); @@ -196,6 +197,9 @@ Menu::Menu() { 0, // QML Qt::Key_Apostrophe, qApp, SLOT(resetSensors())); + addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::EnableCharacterController, 0, true, + avatar.get(), SLOT(updateMotionBehaviorFromMenu())); + // Avatar > AvatarBookmarks related menus -- Note: the AvatarBookmarks class adds its own submenus here. auto avatarBookmarks = DependencyManager::get(); avatarBookmarks->setupMenus(this, avatarMenu); @@ -446,12 +450,6 @@ Menu::Menu() { qApp, SLOT(setActiveFaceTracker())); faceTrackerGroup->addAction(noFaceTracker); -#ifdef HAVE_FACESHIFT - QAction* faceshiftFaceTracker = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::Faceshift, - 0, false, - qApp, SLOT(setActiveFaceTracker())); - faceTrackerGroup->addAction(faceshiftFaceTracker); -#endif #ifdef HAVE_DDE QAction* ddeFaceTracker = addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::UseCamera, 0, true, @@ -472,11 +470,10 @@ Menu::Menu() { QAction* ddeCalibrate = addActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::CalibrateCamera, 0, DependencyManager::get().data(), SLOT(calibrate())); ddeCalibrate->setVisible(true); // DDE face tracking is on by default -#endif -#if defined(HAVE_FACESHIFT) || defined(HAVE_DDE) faceTrackingMenu->addSeparator(); addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::MuteFaceTracking, - Qt::CTRL | Qt::SHIFT | Qt::Key_F, true); // DDE face tracking is on by default + [](bool mute) { FaceTracker::setIsMuted(mute); }, + Qt::CTRL | Qt::SHIFT | Qt::Key_F, FaceTracker::isMuted()); addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::AutoMuteAudio, 0, false); #endif @@ -532,10 +529,6 @@ Menu::Menu() { avatar.get(), SLOT(updateMotionBehaviorFromMenu()), UNSPECIFIED_POSITION, "Developer"); - addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableCharacterController, 0, true, - avatar.get(), SLOT(updateMotionBehaviorFromMenu()), - UNSPECIFIED_POSITION, "Developer"); - // Developer > Hands >>> MenuWrapper* handOptionsMenu = developerMenu->addMenu("Hands"); addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false, @@ -622,8 +615,6 @@ Menu::Menu() { QString("../../hifi/tablet/TabletAudioPreferences.qml"), "AudioPreferencesDialog"); }); - addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioNoiseReduction, 0, true, - audioIO.data(), SLOT(toggleAudioNoiseReduction())); addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoServerAudio, 0, false, audioIO.data(), SLOT(toggleServerEcho())); addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoLocalAudio, 0, false, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index b6f70f5339..479a78e7c2 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -36,7 +36,7 @@ namespace MenuOption { const QString AssetMigration = "ATP Asset Migration"; const QString AssetServer = "Asset Browser"; const QString Attachments = "Attachments..."; - const QString AudioNoiseReduction = "Audio Noise Reduction"; + const QString AudioNoiseReduction = "Noise Reduction"; const QString AudioScope = "Show Scope"; const QString AudioScopeFiftyFrames = "Fifty"; const QString AudioScopeFiveFrames = "Five"; @@ -96,7 +96,7 @@ namespace MenuOption { const QString DontRenderEntitiesAsScene = "Don't Render Entities as Scene"; const QString EchoLocalAudio = "Echo Local Audio"; const QString EchoServerAudio = "Echo Server Audio"; - const QString EnableCharacterController = "Enable avatar collisions"; + const QString EnableCharacterController = "Collide with world"; const QString EnableInverseKinematics = "Enable Inverse Kinematics"; const QString EntityScriptServerLog = "Entity Script Server Log"; const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation"; @@ -105,7 +105,6 @@ namespace MenuOption { const QString ExpandPaintGLTiming = "Expand /paintGL"; const QString ExpandPhysicsSimulationTiming = "Expand /physics"; const QString ExpandUpdateTiming = "Expand /update"; - const QString Faceshift = "Faceshift"; const QString FirstPerson = "First Person"; const QString FivePointCalibration = "5 Point Calibration"; const QString FixGaze = "Fix Gaze (no saccade)"; diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index 282acf6bf5..16e5776d87 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -13,6 +13,7 @@ #include #include +#include #include "Application.h" #include "Avatar.h" @@ -22,8 +23,6 @@ #include "Menu.h" #include "Util.h" #include "devices/DdeFaceTracker.h" -#include "devices/EyeTracker.h" -#include "devices/Faceshift.h" #include using namespace std; @@ -209,14 +208,14 @@ void Head::simulate(float deltaTime, bool isMine) { // use data to update fake Faceshift blendshape coefficients calculateMouthShapes(deltaTime); - DependencyManager::get()->updateFakeCoefficients(_leftEyeBlink, - _rightEyeBlink, - _browAudioLift, - _audioJawOpen, - _mouth2, - _mouth3, - _mouth4, - _blendshapeCoefficients); + FaceTracker::updateFakeCoefficients(_leftEyeBlink, + _rightEyeBlink, + _browAudioLift, + _audioJawOpen, + _mouth2, + _mouth3, + _mouth4, + _blendshapeCoefficients); applyEyelidOffset(getOrientation()); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 7bc961c654..5e285f21ba 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -41,9 +41,9 @@ #include #include #include +#include #include "Application.h" -#include "devices/Faceshift.h" #include "AvatarManager.h" #include "AvatarActionHold.h" #include "Menu.h" @@ -82,6 +82,18 @@ const float MyAvatar::ZOOM_MIN = 0.5f; const float MyAvatar::ZOOM_MAX = 25.0f; const float MyAvatar::ZOOM_DEFAULT = 1.5f; +// default values, used when avatar is missing joints... (avatar space) +// static const glm::quat DEFAULT_AVATAR_MIDDLE_EYE_ROT { Quaternions::Y_180 }; +static const glm::vec3 DEFAULT_AVATAR_MIDDLE_EYE_POS { 0.0f, 0.6f, 0.0f }; +static const glm::vec3 DEFAULT_AVATAR_HEAD_POS { 0.0f, 0.53f, 0.0f }; +static const glm::vec3 DEFAULT_AVATAR_NECK_POS { 0.0f, 0.445f, 0.025f }; +static const glm::vec3 DEFAULT_AVATAR_SPINE2_POS { 0.0f, 0.32f, 0.02f }; +static const glm::vec3 DEFAULT_AVATAR_HIPS_POS { 0.0f, 0.0f, 0.0f }; +static const glm::vec3 DEFAULT_AVATAR_LEFTFOOT_POS { -0.08f, -0.96f, 0.029f}; +static const glm::quat DEFAULT_AVATAR_LEFTFOOT_ROT { -0.40167322754859924f, 0.9154590368270874f, -0.005437685176730156f, -0.023744143545627594f }; +static const glm::vec3 DEFAULT_AVATAR_RIGHTFOOT_POS { 0.08f, -0.96f, 0.029f }; +static const glm::quat DEFAULT_AVATAR_RIGHTFOOT_ROT { -0.4016716778278351f, 0.9154615998268127f, 0.0053307069465518f, 0.023696165531873703f }; + MyAvatar::MyAvatar(QThread* thread, RigPointer rig) : Avatar(thread, rig), _wasPushing(false), @@ -650,18 +662,13 @@ void MyAvatar::updateFromTrackers(float deltaTime) { } FaceTracker* tracker = qApp->getActiveFaceTracker(); - bool inFacetracker = tracker && !tracker->isMuted(); + bool inFacetracker = tracker && !FaceTracker::isMuted(); if (inHmd) { estimatedPosition = extractTranslation(getHMDSensorMatrix()); estimatedPosition.x *= -1.0f; - _trackedHeadPosition = estimatedPosition; - - const float OCULUS_LEAN_SCALE = 0.05f; - estimatedPosition /= OCULUS_LEAN_SCALE; } else if (inFacetracker) { estimatedPosition = tracker->getHeadTranslation(); - _trackedHeadPosition = estimatedPosition; estimatedRotation = glm::degrees(safeEulerAngles(tracker->getHeadRotation())); } @@ -1378,6 +1385,65 @@ controller::Pose MyAvatar::getRightFootControllerPoseInAvatarFrame() const { return getRightFootControllerPoseInWorldFrame().transform(invAvatarMatrix); } +void MyAvatar::setSpineControllerPosesInSensorFrame(const controller::Pose& hips, const controller::Pose& spine2) { + if (controller::InputDevice::getLowVelocityFilter()) { + auto oldHipsPose = getHipsControllerPoseInSensorFrame(); + auto oldSpine2Pose = getSpine2ControllerPoseInSensorFrame(); + _hipsControllerPoseInSensorFrameCache.set(applyLowVelocityFilter(oldHipsPose, hips)); + _spine2ControllerPoseInSensorFrameCache.set(applyLowVelocityFilter(oldSpine2Pose, spine2)); + } else { + _hipsControllerPoseInSensorFrameCache.set(hips); + _spine2ControllerPoseInSensorFrameCache.set(spine2); + } +} + +controller::Pose MyAvatar::getHipsControllerPoseInSensorFrame() const { + return _hipsControllerPoseInSensorFrameCache.get(); +} + +controller::Pose MyAvatar::getSpine2ControllerPoseInSensorFrame() const { + return _spine2ControllerPoseInSensorFrameCache.get(); +} + +controller::Pose MyAvatar::getHipsControllerPoseInWorldFrame() const { + return _hipsControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix()); +} + +controller::Pose MyAvatar::getSpine2ControllerPoseInWorldFrame() const { + return _spine2ControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix()); +} + +controller::Pose MyAvatar::getHipsControllerPoseInAvatarFrame() const { + glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition())); + return getHipsControllerPoseInWorldFrame().transform(invAvatarMatrix); +} + +controller::Pose MyAvatar::getSpine2ControllerPoseInAvatarFrame() const { + glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition())); + return getSpine2ControllerPoseInWorldFrame().transform(invAvatarMatrix); +} + +void MyAvatar::setHeadControllerPoseInSensorFrame(const controller::Pose& head) { + if (controller::InputDevice::getLowVelocityFilter()) { + auto oldHeadPose = getHeadControllerPoseInSensorFrame(); + _headControllerPoseInSensorFrameCache.set(applyLowVelocityFilter(oldHeadPose, head)); + } else { + _headControllerPoseInSensorFrameCache.set(head); + } +} + +controller::Pose MyAvatar::getHeadControllerPoseInSensorFrame() const { + return _headControllerPoseInSensorFrameCache.get(); +} + +controller::Pose MyAvatar::getHeadControllerPoseInWorldFrame() const { + return _headControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix()); +} + +controller::Pose MyAvatar::getHeadControllerPoseInAvatarFrame() const { + glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition())); + return getHeadControllerPoseInWorldFrame().transform(invAvatarMatrix); +} void MyAvatar::updateMotors() { _characterController.clearMotors(); @@ -2220,22 +2286,17 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const { const glm::quat hmdOrientation = getHMDSensorOrientation(); const glm::quat hmdOrientationYawOnly = cancelOutRollAndPitch(hmdOrientation); - // 2 meter tall dude (in rig coordinates) - const glm::vec3 DEFAULT_RIG_MIDDLE_EYE_POS(0.0f, 0.9f, 0.0f); - const glm::vec3 DEFAULT_RIG_NECK_POS(0.0f, 0.70f, 0.0f); - const glm::vec3 DEFAULT_RIG_HIPS_POS(0.0f, 0.05f, 0.0f); - int rightEyeIndex = _rig->indexOfJoint("RightEye"); int leftEyeIndex = _rig->indexOfJoint("LeftEye"); int neckIndex = _rig->indexOfJoint("Neck"); int hipsIndex = _rig->indexOfJoint("Hips"); - glm::vec3 rigMiddleEyePos = DEFAULT_RIG_MIDDLE_EYE_POS; + glm::vec3 rigMiddleEyePos = DEFAULT_AVATAR_MIDDLE_EYE_POS; if (leftEyeIndex >= 0 && rightEyeIndex >= 0) { rigMiddleEyePos = (_rig->getAbsoluteDefaultPose(leftEyeIndex).trans() + _rig->getAbsoluteDefaultPose(rightEyeIndex).trans()) / 2.0f; } - glm::vec3 rigNeckPos = neckIndex != -1 ? _rig->getAbsoluteDefaultPose(neckIndex).trans() : DEFAULT_RIG_NECK_POS; - glm::vec3 rigHipsPos = hipsIndex != -1 ? _rig->getAbsoluteDefaultPose(hipsIndex).trans() : DEFAULT_RIG_HIPS_POS; + glm::vec3 rigNeckPos = neckIndex != -1 ? _rig->getAbsoluteDefaultPose(neckIndex).trans() : DEFAULT_AVATAR_NECK_POS; + glm::vec3 rigHipsPos = hipsIndex != -1 ? _rig->getAbsoluteDefaultPose(hipsIndex).trans() : DEFAULT_AVATAR_HIPS_POS; glm::vec3 localEyes = (rigMiddleEyePos - rigHipsPos); glm::vec3 localNeck = (rigNeckPos - rigHipsPos); @@ -2599,6 +2660,79 @@ glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const { } } +glm::mat4 MyAvatar::getCenterEyeCalibrationMat() const { + // TODO: as an optimization cache this computation, then invalidate the cache when the avatar model is changed. + int rightEyeIndex = _rig->indexOfJoint("RightEye"); + int leftEyeIndex = _rig->indexOfJoint("LeftEye"); + if (rightEyeIndex >= 0 && leftEyeIndex >= 0) { + auto centerEyePos = (getAbsoluteDefaultJointTranslationInObjectFrame(rightEyeIndex) + getAbsoluteDefaultJointTranslationInObjectFrame(leftEyeIndex)) * 0.5f; + auto centerEyeRot = Quaternions::Y_180; + return createMatFromQuatAndPos(centerEyeRot, centerEyePos); + } else { + return createMatFromQuatAndPos(DEFAULT_AVATAR_MIDDLE_EYE_POS, DEFAULT_AVATAR_MIDDLE_EYE_POS); + } +} + +glm::mat4 MyAvatar::getHeadCalibrationMat() const { + // TODO: as an optimization cache this computation, then invalidate the cache when the avatar model is changed. + int headIndex = _rig->indexOfJoint("Head"); + if (headIndex >= 0) { + auto headPos = getAbsoluteDefaultJointTranslationInObjectFrame(headIndex); + auto headRot = getAbsoluteDefaultJointRotationInObjectFrame(headIndex); + return createMatFromQuatAndPos(headRot, headPos); + } else { + return createMatFromQuatAndPos(DEFAULT_AVATAR_HEAD_POS, DEFAULT_AVATAR_HEAD_POS); + } +} + +glm::mat4 MyAvatar::getSpine2CalibrationMat() const { + // TODO: as an optimization cache this computation, then invalidate the cache when the avatar model is changed. + int spine2Index = _rig->indexOfJoint("Spine2"); + if (spine2Index >= 0) { + auto spine2Pos = getAbsoluteDefaultJointTranslationInObjectFrame(spine2Index); + auto spine2Rot = getAbsoluteDefaultJointRotationInObjectFrame(spine2Index); + return createMatFromQuatAndPos(spine2Rot, spine2Pos); + } else { + return createMatFromQuatAndPos(DEFAULT_AVATAR_SPINE2_POS, DEFAULT_AVATAR_SPINE2_POS); + } +} + +glm::mat4 MyAvatar::getHipsCalibrationMat() const { + // TODO: as an optimization cache this computation, then invalidate the cache when the avatar model is changed. + int hipsIndex = _rig->indexOfJoint("Hips"); + if (hipsIndex >= 0) { + auto hipsPos = getAbsoluteDefaultJointTranslationInObjectFrame(hipsIndex); + auto hipsRot = getAbsoluteDefaultJointRotationInObjectFrame(hipsIndex); + return createMatFromQuatAndPos(hipsRot, hipsPos); + } else { + return createMatFromQuatAndPos(DEFAULT_AVATAR_HIPS_POS, DEFAULT_AVATAR_HIPS_POS); + } +} + +glm::mat4 MyAvatar::getLeftFootCalibrationMat() const { + // TODO: as an optimization cache this computation, then invalidate the cache when the avatar model is changed. + int leftFootIndex = _rig->indexOfJoint("LeftFoot"); + if (leftFootIndex >= 0) { + auto leftFootPos = getAbsoluteDefaultJointTranslationInObjectFrame(leftFootIndex); + auto leftFootRot = getAbsoluteDefaultJointRotationInObjectFrame(leftFootIndex); + return createMatFromQuatAndPos(leftFootRot, leftFootPos); + } else { + return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTFOOT_POS, DEFAULT_AVATAR_LEFTFOOT_POS); + } +} + +glm::mat4 MyAvatar::getRightFootCalibrationMat() const { + // TODO: as an optimization cache this computation, then invalidate the cache when the avatar model is changed. + int rightFootIndex = _rig->indexOfJoint("RightFoot"); + if (rightFootIndex >= 0) { + auto rightFootPos = getAbsoluteDefaultJointTranslationInObjectFrame(rightFootIndex); + auto rightFootRot = getAbsoluteDefaultJointRotationInObjectFrame(rightFootIndex); + return createMatFromQuatAndPos(rightFootRot, rightFootPos); + } else { + return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTFOOT_POS, DEFAULT_AVATAR_RIGHTFOOT_POS); + } +} + bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& orientation) { auto hipsIndex = getJointIndex("Hips"); if (index != hipsIndex) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index a201ee54c9..6a1e457a97 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -353,7 +353,6 @@ public: eyeContactTarget getEyeContactTarget(); - Q_INVOKABLE glm::vec3 getTrackedHeadPosition() const { return _trackedHeadPosition; } Q_INVOKABLE glm::vec3 getHeadPosition() const { return getHead()->getPosition(); } Q_INVOKABLE float getHeadFinalYaw() const { return getHead()->getFinalYaw(); } Q_INVOKABLE float getHeadFinalRoll() const { return getHead()->getFinalRoll(); } @@ -453,6 +452,19 @@ public: controller::Pose getLeftFootControllerPoseInAvatarFrame() const; controller::Pose getRightFootControllerPoseInAvatarFrame() const; + void setSpineControllerPosesInSensorFrame(const controller::Pose& hips, const controller::Pose& spine2); + controller::Pose getHipsControllerPoseInSensorFrame() const; + controller::Pose getSpine2ControllerPoseInSensorFrame() const; + controller::Pose getHipsControllerPoseInWorldFrame() const; + controller::Pose getSpine2ControllerPoseInWorldFrame() const; + controller::Pose getHipsControllerPoseInAvatarFrame() const; + controller::Pose getSpine2ControllerPoseInAvatarFrame() const; + + void setHeadControllerPoseInSensorFrame(const controller::Pose& head); + controller::Pose getHeadControllerPoseInSensorFrame() const; + controller::Pose getHeadControllerPoseInWorldFrame() const; + controller::Pose getHeadControllerPoseInAvatarFrame() const; + bool hasDriveInput() const; Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); @@ -461,10 +473,22 @@ public: virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; + // all calibration matrices are in absolute avatar space. + glm::mat4 getCenterEyeCalibrationMat() const; + glm::mat4 getHeadCalibrationMat() const; + glm::mat4 getSpine2CalibrationMat() const; + glm::mat4 getHipsCalibrationMat() const; + glm::mat4 getLeftFootCalibrationMat() const; + glm::mat4 getRightFootCalibrationMat() const; + void addHoldAction(AvatarActionHold* holdAction); // thread-safe void removeHoldAction(AvatarActionHold* holdAction); // thread-safe void updateHoldActions(const AnimPose& prePhysicsPose, const AnimPose& postUpdatePose); + // derive avatar body position and orientation from the current HMD Sensor location. + // results are in HMD frame + glm::mat4 deriveBodyFromHMDSensor() const; + public slots: void increaseSize(); void decreaseSize(); @@ -553,9 +577,7 @@ private: void setVisibleInSceneIfReady(Model* model, const render::ScenePointer& scene, bool visiblity); - // derive avatar body position and orientation from the current HMD Sensor location. - // results are in HMD frame - glm::mat4 deriveBodyFromHMDSensor() const; +private: virtual void updatePalms() override {} void lateUpdatePalms(); @@ -691,9 +713,11 @@ private: // These are stored in SENSOR frame ThreadSafeValueCache _leftHandControllerPoseInSensorFrameCache { controller::Pose() }; ThreadSafeValueCache _rightHandControllerPoseInSensorFrameCache { controller::Pose() }; - ThreadSafeValueCache _leftFootControllerPoseInSensorFrameCache{ controller::Pose() }; ThreadSafeValueCache _rightFootControllerPoseInSensorFrameCache{ controller::Pose() }; + ThreadSafeValueCache _hipsControllerPoseInSensorFrameCache{ controller::Pose() }; + ThreadSafeValueCache _spine2ControllerPoseInSensorFrameCache{ controller::Pose() }; + ThreadSafeValueCache _headControllerPoseInSensorFrameCache{ controller::Pose() }; bool _hmdLeanRecenterEnabled = true; diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 81a5bf38dc..f81a83523b 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -107,27 +107,49 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { Rig::HeadParameters headParams; - if (qApp->isHMDMode()) { - headParams.isInHMD = true; - - // get HMD position from sensor space into world space, and back into rig space - glm::mat4 worldHMDMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - glm::mat4 rigToWorld = createMatFromQuatAndPos(getRotation(), getTranslation()); - glm::mat4 worldToRig = glm::inverse(rigToWorld); - glm::mat4 rigHMDMat = worldToRig * worldHMDMat; - - headParams.rigHeadPosition = extractTranslation(rigHMDMat); - headParams.rigHeadOrientation = extractRotation(rigHMDMat); - headParams.worldHeadOrientation = extractRotation(worldHMDMat); + // input action is the highest priority source for head orientation. + auto avatarHeadPose = myAvatar->getHeadControllerPoseInAvatarFrame(); + if (avatarHeadPose.isValid()) { + glm::mat4 rigHeadMat = Matrices::Y_180 * createMatFromQuatAndPos(avatarHeadPose.getRotation(), avatarHeadPose.getTranslation()); + headParams.rigHeadPosition = extractTranslation(rigHeadMat); + headParams.rigHeadOrientation = glmExtractRotation(rigHeadMat); + headParams.headEnabled = true; } else { - headParams.isInHMD = false; - - // We don't have a valid localHeadPosition. - headParams.rigHeadOrientation = Quaternions::Y_180 * head->getFinalOrientationInLocalFrame(); - headParams.worldHeadOrientation = head->getFinalOrientationInWorldFrame(); + if (qApp->isHMDMode()) { + // get HMD position from sensor space into world space, and back into rig space + glm::mat4 worldHMDMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); + glm::mat4 rigToWorld = createMatFromQuatAndPos(getRotation(), getTranslation()); + glm::mat4 worldToRig = glm::inverse(rigToWorld); + glm::mat4 rigHMDMat = worldToRig * worldHMDMat; + _rig->computeHeadFromHMD(AnimPose(rigHMDMat), headParams.rigHeadPosition, headParams.rigHeadOrientation); + headParams.headEnabled = true; + } else { + // even though full head IK is disabled, the rig still needs the head orientation to rotate the head up and down in desktop mode. + // preMult 180 is necessary to convert from avatar to rig coordinates. + // postMult 180 is necessary to convert head from -z forward to z forward. + headParams.rigHeadOrientation = Quaternions::Y_180 * head->getFinalOrientationInLocalFrame() * Quaternions::Y_180; + headParams.headEnabled = false; + } + } + + auto avatarHipsPose = myAvatar->getHipsControllerPoseInAvatarFrame(); + if (avatarHipsPose.isValid()) { + glm::mat4 rigHipsMat = Matrices::Y_180 * createMatFromQuatAndPos(avatarHipsPose.getRotation(), avatarHipsPose.getTranslation()); + headParams.hipsMatrix = rigHipsMat; + headParams.hipsEnabled = true; + } else { + headParams.hipsEnabled = false; + } + + auto avatarSpine2Pose = myAvatar->getSpine2ControllerPoseInAvatarFrame(); + if (avatarSpine2Pose.isValid()) { + glm::mat4 rigSpine2Mat = Matrices::Y_180 * createMatFromQuatAndPos(avatarSpine2Pose.getRotation(), avatarSpine2Pose.getTranslation()); + headParams.spine2Matrix = rigSpine2Mat; + headParams.spine2Enabled = true; + } else { + headParams.spine2Enabled = false; } - headParams.neckJointIndex = geometry.neckJointIndex; headParams.isTalking = head->getTimeWithoutTalking() <= 1.5f; _rig->updateFromHeadParameters(headParams, deltaTime); @@ -187,7 +209,6 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { Model::updateRig(deltaTime, parentTransform); Rig::EyeParameters eyeParams; - eyeParams.worldHeadOrientation = headParams.worldHeadOrientation; eyeParams.eyeLookAt = lookAt; eyeParams.eyeSaccade = head->getSaccade(); eyeParams.modelRotation = getRotation(); @@ -219,7 +240,6 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { head->setBaseRoll(glm::degrees(-eulers.z)); Rig::EyeParameters eyeParams; - eyeParams.worldHeadOrientation = head->getFinalOrientationInWorldFrame(); eyeParams.eyeLookAt = lookAt; eyeParams.eyeSaccade = glm::vec3(0.0f); eyeParams.modelRotation = getRotation(); diff --git a/interface/src/devices/DdeFaceTracker.h b/interface/src/devices/DdeFaceTracker.h index 973c3b224e..f125dfc3cf 100644 --- a/interface/src/devices/DdeFaceTracker.h +++ b/interface/src/devices/DdeFaceTracker.h @@ -22,7 +22,7 @@ #include #include -#include "FaceTracker.h" +#include class DdeFaceTracker : public FaceTracker, public Dependency { Q_OBJECT diff --git a/interface/src/devices/Faceshift.cpp b/interface/src/devices/Faceshift.cpp deleted file mode 100644 index 81c099c740..0000000000 --- a/interface/src/devices/Faceshift.cpp +++ /dev/null @@ -1,310 +0,0 @@ -// -// Faceshift.cpp -// interface/src/devices -// -// Created by Andrzej Kapolka on 9/3/13. -// Copyright 2013 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 -// - -#include - -#include -#include -#include - -#include "Faceshift.h" -#include "Menu.h" -#include "Util.h" -#include "InterfaceLogging.h" - -#ifdef HAVE_FACESHIFT -using namespace fs; -#endif - -using namespace std; - -const QString DEFAULT_FACESHIFT_HOSTNAME = "localhost"; -const quint16 FACESHIFT_PORT = 33433; - -Faceshift::Faceshift() : - _hostname("faceshiftHostname", DEFAULT_FACESHIFT_HOSTNAME) -{ -#ifdef HAVE_FACESHIFT - connect(&_tcpSocket, SIGNAL(connected()), SLOT(noteConnected())); - connect(&_tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(noteError(QAbstractSocket::SocketError))); - connect(&_tcpSocket, SIGNAL(readyRead()), SLOT(readFromSocket())); - connect(&_tcpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), SIGNAL(connectionStateChanged())); - connect(&_tcpSocket, SIGNAL(disconnected()), SLOT(noteDisconnected())); - - connect(&_udpSocket, SIGNAL(readyRead()), SLOT(readPendingDatagrams())); - - _udpSocket.bind(FACESHIFT_PORT); -#endif -} - -#ifdef HAVE_FACESHIFT -void Faceshift::init() { - FaceTracker::init(); - setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::Faceshift) && !_isMuted); -} - -void Faceshift::update(float deltaTime) { - if (!isActive()) { - return; - } - FaceTracker::update(deltaTime); - - // get the euler angles relative to the window - glm::vec3 eulers = glm::degrees(safeEulerAngles(_headRotation * glm::quat(glm::radians(glm::vec3( - (_eyeGazeLeftPitch + _eyeGazeRightPitch) / 2.0f, (_eyeGazeLeftYaw + _eyeGazeRightYaw) / 2.0f, 0.0f))))); - - // compute and subtract the long term average - const float LONG_TERM_AVERAGE_SMOOTHING = 0.999f; - if (!_longTermAverageInitialized) { - _longTermAverageEyePitch = eulers.x; - _longTermAverageEyeYaw = eulers.y; - _longTermAverageInitialized = true; - - } else { - _longTermAverageEyePitch = glm::mix(eulers.x, _longTermAverageEyePitch, LONG_TERM_AVERAGE_SMOOTHING); - _longTermAverageEyeYaw = glm::mix(eulers.y, _longTermAverageEyeYaw, LONG_TERM_AVERAGE_SMOOTHING); - } - _estimatedEyePitch = eulers.x - _longTermAverageEyePitch; - _estimatedEyeYaw = eulers.y - _longTermAverageEyeYaw; -} - -void Faceshift::reset() { - if (_tcpSocket.state() == QAbstractSocket::ConnectedState) { - qCDebug(interfaceapp, "Faceshift: Reset"); - - FaceTracker::reset(); - - string message; - fsBinaryStream::encode_message(message, fsMsgCalibrateNeutral()); - send(message); - } - _longTermAverageInitialized = false; -} - -bool Faceshift::isActive() const { - const quint64 ACTIVE_TIMEOUT_USECS = 1000000; - return (usecTimestampNow() - _lastReceiveTimestamp) < ACTIVE_TIMEOUT_USECS; -} - -bool Faceshift::isTracking() const { - return isActive() && _tracking; -} -#endif - - -bool Faceshift::isConnectedOrConnecting() const { - return _tcpSocket.state() == QAbstractSocket::ConnectedState || - (_tcpRetryCount == 0 && _tcpSocket.state() != QAbstractSocket::UnconnectedState); -} - -void Faceshift::updateFakeCoefficients(float leftBlink, float rightBlink, float browUp, - float jawOpen, float mouth2, float mouth3, float mouth4, QVector& coefficients) const { - const int MMMM_BLENDSHAPE = 34; - const int FUNNEL_BLENDSHAPE = 40; - const int SMILE_LEFT_BLENDSHAPE = 28; - const int SMILE_RIGHT_BLENDSHAPE = 29; - const int MAX_FAKE_BLENDSHAPE = 40; // Largest modified blendshape from above and below - - coefficients.resize(max((int)coefficients.size(), MAX_FAKE_BLENDSHAPE + 1)); - qFill(coefficients.begin(), coefficients.end(), 0.0f); - coefficients[_leftBlinkIndex] = leftBlink; - coefficients[_rightBlinkIndex] = rightBlink; - coefficients[_browUpCenterIndex] = browUp; - coefficients[_browUpLeftIndex] = browUp; - coefficients[_browUpRightIndex] = browUp; - coefficients[_jawOpenIndex] = jawOpen; - coefficients[SMILE_LEFT_BLENDSHAPE] = coefficients[SMILE_RIGHT_BLENDSHAPE] = mouth4; - coefficients[MMMM_BLENDSHAPE] = mouth2; - coefficients[FUNNEL_BLENDSHAPE] = mouth3; -} - -void Faceshift::setEnabled(bool enabled) { - // Don't enable until have explicitly initialized - if (!_isInitialized) { - return; - } -#ifdef HAVE_FACESHIFT - if ((_tcpEnabled = enabled)) { - connectSocket(); - } else { - qCDebug(interfaceapp, "Faceshift: Disconnecting..."); - _tcpSocket.disconnectFromHost(); - } -#endif -} - -void Faceshift::connectSocket() { - if (_tcpEnabled) { - if (!_tcpRetryCount) { - qCDebug(interfaceapp, "Faceshift: Connecting..."); - } - - _tcpSocket.connectToHost(_hostname.get(), FACESHIFT_PORT); - _tracking = false; - } -} - -void Faceshift::noteConnected() { -#ifdef HAVE_FACESHIFT - qCDebug(interfaceapp, "Faceshift: Connected"); - // request the list of blendshape names - string message; - fsBinaryStream::encode_message(message, fsMsgSendBlendshapeNames()); - send(message); -#endif -} - -void Faceshift::noteDisconnected() { -#ifdef HAVE_FACESHIFT - qCDebug(interfaceapp, "Faceshift: Disconnected"); -#endif -} - -void Faceshift::noteError(QAbstractSocket::SocketError error) { - if (!_tcpRetryCount) { - // Only spam log with fail to connect the first time, so that we can keep waiting for server - qCWarning(interfaceapp) << "Faceshift: " << _tcpSocket.errorString(); - } - // retry connection after a 2 second delay - if (_tcpEnabled) { - _tcpRetryCount++; - QTimer::singleShot(2000, this, SLOT(connectSocket())); - } -} - -void Faceshift::readPendingDatagrams() { - QByteArray buffer; - while (_udpSocket.hasPendingDatagrams()) { - buffer.resize(_udpSocket.pendingDatagramSize()); - _udpSocket.readDatagram(buffer.data(), buffer.size()); - receive(buffer); - } -} - -void Faceshift::readFromSocket() { - receive(_tcpSocket.readAll()); -} - -void Faceshift::send(const std::string& message) { - _tcpSocket.write(message.data(), message.size()); -} - -void Faceshift::receive(const QByteArray& buffer) { -#ifdef HAVE_FACESHIFT - _lastReceiveTimestamp = usecTimestampNow(); - - _stream.received(buffer.size(), buffer.constData()); - fsMsgPtr msg; - for (fsMsgPtr msg; (msg = _stream.get_message()); ) { - switch (msg->id()) { - case fsMsg::MSG_OUT_TRACKING_STATE: { - const fsTrackingData& data = static_pointer_cast(msg)->tracking_data(); - if ((_tracking = data.m_trackingSuccessful)) { - glm::quat newRotation = glm::quat(data.m_headRotation.w, -data.m_headRotation.x, - data.m_headRotation.y, -data.m_headRotation.z); - // Compute angular velocity of the head - glm::quat r = glm::normalize(newRotation * glm::inverse(_headRotation)); - float theta = 2 * acos(r.w); - if (theta > EPSILON) { - float rMag = glm::length(glm::vec3(r.x, r.y, r.z)); - _headAngularVelocity = theta / _averageFrameTime * glm::vec3(r.x, r.y, r.z) / rMag; - } else { - _headAngularVelocity = glm::vec3(0,0,0); - } - const float ANGULAR_VELOCITY_FILTER_STRENGTH = 0.3f; - _headRotation = safeMix(_headRotation, newRotation, glm::clamp(glm::length(_headAngularVelocity) * - ANGULAR_VELOCITY_FILTER_STRENGTH, 0.0f, 1.0f)); - - const float TRANSLATION_SCALE = 0.02f; - glm::vec3 newHeadTranslation = glm::vec3(data.m_headTranslation.x, data.m_headTranslation.y, - -data.m_headTranslation.z) * TRANSLATION_SCALE; - - _headLinearVelocity = (newHeadTranslation - _lastHeadTranslation) / _averageFrameTime; - - const float LINEAR_VELOCITY_FILTER_STRENGTH = 0.3f; - float velocityFilter = glm::clamp(1.0f - glm::length(_headLinearVelocity) * - LINEAR_VELOCITY_FILTER_STRENGTH, 0.0f, 1.0f); - _filteredHeadTranslation = velocityFilter * _filteredHeadTranslation + (1.0f - velocityFilter) * newHeadTranslation; - - _lastHeadTranslation = newHeadTranslation; - _headTranslation = _filteredHeadTranslation; - - _eyeGazeLeftPitch = -data.m_eyeGazeLeftPitch; - _eyeGazeLeftYaw = data.m_eyeGazeLeftYaw; - _eyeGazeRightPitch = -data.m_eyeGazeRightPitch; - _eyeGazeRightYaw = data.m_eyeGazeRightYaw; - _blendshapeCoefficients = QVector::fromStdVector(data.m_coeffs); - - const float FRAME_AVERAGING_FACTOR = 0.99f; - quint64 usecsNow = usecTimestampNow(); - if (_lastMessageReceived != 0) { - _averageFrameTime = FRAME_AVERAGING_FACTOR * _averageFrameTime + - (1.0f - FRAME_AVERAGING_FACTOR) * (float)(usecsNow - _lastMessageReceived) / 1000000.0f; - } - _lastMessageReceived = usecsNow; - } - break; - } - case fsMsg::MSG_OUT_BLENDSHAPE_NAMES: { - const vector& names = static_pointer_cast(msg)->blendshape_names(); - for (int i = 0; i < (int)names.size(); i++) { - if (names[i] == "EyeBlink_L") { - _leftBlinkIndex = i; - - } else if (names[i] == "EyeBlink_R") { - _rightBlinkIndex = i; - - } else if (names[i] == "EyeOpen_L") { - _leftEyeOpenIndex = i; - - } else if (names[i] == "EyeOpen_R") { - _rightEyeOpenIndex = i; - - } else if (names[i] == "BrowsD_L") { - _browDownLeftIndex = i; - - } else if (names[i] == "BrowsD_R") { - _browDownRightIndex = i; - - } else if (names[i] == "BrowsU_C") { - _browUpCenterIndex = i; - - } else if (names[i] == "BrowsU_L") { - _browUpLeftIndex = i; - - } else if (names[i] == "BrowsU_R") { - _browUpRightIndex = i; - - } else if (names[i] == "JawOpen") { - _jawOpenIndex = i; - - } else if (names[i] == "MouthSmile_L") { - _mouthSmileLeftIndex = i; - - } else if (names[i] == "MouthSmile_R") { - _mouthSmileRightIndex = i; - } - } - break; - } - default: - break; - } - } -#endif - - FaceTracker::countFrame(); -} - -void Faceshift::setHostname(const QString& hostname) { - _hostname.set(hostname); -} - diff --git a/interface/src/devices/Faceshift.h b/interface/src/devices/Faceshift.h deleted file mode 100644 index 2c5889857c..0000000000 --- a/interface/src/devices/Faceshift.h +++ /dev/null @@ -1,155 +0,0 @@ -// -// Faceshift.h -// interface/src/devices -// -// Created by Andrzej Kapolka on 9/3/13. -// Copyright 2013 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 -// - -#ifndef hifi_Faceshift_h -#define hifi_Faceshift_h - -#include -#include - -#ifdef HAVE_FACESHIFT -#include -#endif - -#include -#include - -#include "FaceTracker.h" - -const float STARTING_FACESHIFT_FRAME_TIME = 0.033f; - -/// Handles interaction with the Faceshift software, which provides head position/orientation and facial features. -class Faceshift : public FaceTracker, public Dependency { - Q_OBJECT - SINGLETON_DEPENDENCY - -public: -#ifdef HAVE_FACESHIFT - // If we don't have faceshift, use the base class' methods - virtual void init() override; - virtual void update(float deltaTime) override; - virtual void reset() override; - - virtual bool isActive() const override; - virtual bool isTracking() const override; -#endif - - bool isConnectedOrConnecting() const; - - const glm::vec3& getHeadAngularVelocity() const { return _headAngularVelocity; } - - // these pitch/yaw angles are in degrees - float getEyeGazeLeftPitch() const { return _eyeGazeLeftPitch; } - float getEyeGazeLeftYaw() const { return _eyeGazeLeftYaw; } - - float getEyeGazeRightPitch() const { return _eyeGazeRightPitch; } - float getEyeGazeRightYaw() const { return _eyeGazeRightYaw; } - - float getLeftBlink() const { return getBlendshapeCoefficient(_leftBlinkIndex); } - float getRightBlink() const { return getBlendshapeCoefficient(_rightBlinkIndex); } - float getLeftEyeOpen() const { return getBlendshapeCoefficient(_leftEyeOpenIndex); } - float getRightEyeOpen() const { return getBlendshapeCoefficient(_rightEyeOpenIndex); } - - float getBrowDownLeft() const { return getBlendshapeCoefficient(_browDownLeftIndex); } - float getBrowDownRight() const { return getBlendshapeCoefficient(_browDownRightIndex); } - float getBrowUpCenter() const { return getBlendshapeCoefficient(_browUpCenterIndex); } - float getBrowUpLeft() const { return getBlendshapeCoefficient(_browUpLeftIndex); } - float getBrowUpRight() const { return getBlendshapeCoefficient(_browUpRightIndex); } - - float getMouthSize() const { return getBlendshapeCoefficient(_jawOpenIndex); } - float getMouthSmileLeft() const { return getBlendshapeCoefficient(_mouthSmileLeftIndex); } - float getMouthSmileRight() const { return getBlendshapeCoefficient(_mouthSmileRightIndex); } - - QString getHostname() { return _hostname.get(); } - void setHostname(const QString& hostname); - - void updateFakeCoefficients(float leftBlink, - float rightBlink, - float browUp, - float jawOpen, - float mouth2, - float mouth3, - float mouth4, - QVector& coefficients) const; - -signals: - void connectionStateChanged(); - -public slots: - void setEnabled(bool enabled) override; - -private slots: - void connectSocket(); - void noteConnected(); - void noteError(QAbstractSocket::SocketError error); - void readPendingDatagrams(); - void readFromSocket(); - void noteDisconnected(); - -private: - Faceshift(); - virtual ~Faceshift() {} - - void send(const std::string& message); - void receive(const QByteArray& buffer); - - QTcpSocket _tcpSocket; - QUdpSocket _udpSocket; - -#ifdef HAVE_FACESHIFT - fs::fsBinaryStream _stream; -#endif - - bool _tcpEnabled = true; - int _tcpRetryCount = 0; - bool _tracking = false; - quint64 _lastReceiveTimestamp = 0; - quint64 _lastMessageReceived = 0; - float _averageFrameTime = STARTING_FACESHIFT_FRAME_TIME; - - glm::vec3 _headAngularVelocity = glm::vec3(0.0f); - glm::vec3 _headLinearVelocity = glm::vec3(0.0f); - glm::vec3 _lastHeadTranslation = glm::vec3(0.0f); - glm::vec3 _filteredHeadTranslation = glm::vec3(0.0f); - - // degrees - float _eyeGazeLeftPitch = 0.0f; - float _eyeGazeLeftYaw = 0.0f; - float _eyeGazeRightPitch = 0.0f; - float _eyeGazeRightYaw = 0.0f; - - // degrees - float _longTermAverageEyePitch = 0.0f; - float _longTermAverageEyeYaw = 0.0f; - bool _longTermAverageInitialized = false; - - Setting::Handle _hostname; - - // see http://support.faceshift.com/support/articles/35129-export-of-blendshapes - int _leftBlinkIndex = 0; - int _rightBlinkIndex = 1; - int _leftEyeOpenIndex = 8; - int _rightEyeOpenIndex = 9; - - // Brows - int _browDownLeftIndex = 14; - int _browDownRightIndex = 15; - int _browUpCenterIndex = 16; - int _browUpLeftIndex = 17; - int _browUpRightIndex = 18; - - int _mouthSmileLeftIndex = 28; - int _mouthSmileRightIndex = 29; - - int _jawOpenIndex = 21; -}; - -#endif // hifi_Faceshift_h diff --git a/interface/src/devices/Leapmotion.h b/interface/src/devices/Leapmotion.h index d7981a65e8..6ecec8ccf9 100644 --- a/interface/src/devices/Leapmotion.h +++ b/interface/src/devices/Leapmotion.h @@ -14,7 +14,7 @@ #include -#include "MotionTracker.h" +#include #ifdef HAVE_LEAPMOTION #include diff --git a/interface/src/scripting/ControllerScriptingInterface.cpp b/interface/src/scripting/ControllerScriptingInterface.cpp index 0d0c2ef668..f3ec3cd79d 100644 --- a/interface/src/scripting/ControllerScriptingInterface.cpp +++ b/interface/src/scripting/ControllerScriptingInterface.cpp @@ -17,7 +17,7 @@ #include #include "Application.h" -#include "devices/MotionTracker.h" +#include void ControllerScriptingInterface::handleMetaEvent(HFMetaEvent* event) { if (event->type() == HFActionEvent::startType()) { diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 39cf99f349..1e14c24da3 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -168,6 +168,28 @@ void WindowScriptingInterface::ensureReticleVisible() const { } } +/// Display a "browse to directory" dialog. If `directory` is an invalid file or directory the browser will start at the current +/// working directory. +/// \param const QString& title title of the window +/// \param const QString& directory directory to start the file browser at +/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` +/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` +QScriptValue WindowScriptingInterface::browseDir(const QString& title, const QString& directory) { + ensureReticleVisible(); + QString path = directory; + if (path.isEmpty()) { + path = getPreviousBrowseLocation(); + } +#ifndef Q_OS_WIN + path = fixupPathForMac(directory); +#endif + QString result = OffscreenUi::getExistingDirectory(nullptr, title, path); + if (!result.isEmpty()) { + setPreviousBrowseLocation(QFileInfo(result).absolutePath()); + } + return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); +} + /// Display an open file dialog. If `directory` is an invalid file or directory the browser will start at the current /// working directory. /// \param const QString& title title of the window @@ -278,6 +300,10 @@ void WindowScriptingInterface::makeConnection(bool success, const QString& userN } } +void WindowScriptingInterface::displayAnnouncement(const QString& message) { + emit announcement(message); +} + bool WindowScriptingInterface::isPhysicsEnabled() { return qApp->isPhysicsEnabled(); } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 6934dea0af..2b1e48d918 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -51,6 +51,7 @@ public slots: QScriptValue confirm(const QString& message = ""); QScriptValue prompt(const QString& message = "", const QString& defaultText = ""); CustomPromptResult customPrompt(const QVariant& config); + QScriptValue browseDir(const QString& title = "", const QString& directory = ""); QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); @@ -58,6 +59,7 @@ public slots: void copyToClipboard(const QString& text); void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f); void makeConnection(bool success, const QString& userNameOrError); + void displayAnnouncement(const QString& message); void shareSnapshot(const QString& path, const QUrl& href = QUrl("")); bool isPhysicsEnabled(); @@ -73,12 +75,13 @@ signals: void svoImportRequested(const QString& url); void domainConnectionRefused(const QString& reasonMessage, int reasonCode, const QString& extraInfo); void stillSnapshotTaken(const QString& pathStillSnapshot, bool notify); - void snapshotShared(const QString& error); + void snapshotShared(bool isError, const QString& reply); void processingGifStarted(const QString& pathStillSnapshot); void processingGifCompleted(const QString& pathAnimatedSnapshot); void connectionAdded(const QString& connectionName); void connectionError(const QString& errorString); + void announcement(const QString& message); void messageBoxClosed(int id, int button); diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 341915e57f..2b715eac9d 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -11,9 +11,9 @@ #include #include +#include #include "Application.h" -#include "devices/FaceTracker.h" #include "Menu.h" HIFI_QML_DEF(AvatarInputs) diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index a12d9020ae..bf4be7fd17 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -116,11 +115,6 @@ void setupPreferences() { auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter); preferences->addPreference(preference); } - { - auto getter = []()->bool { return SnapshotAnimated::alsoTakeAnimatedSnapshot.get(); }; - auto setter = [](bool value) { SnapshotAnimated::alsoTakeAnimatedSnapshot.set(value); }; - preferences->addPreference(new CheckPreference(SNAPSHOTS, "Take Animated GIF Snapshot", getter, setter)); - } { auto getter = []()->float { return SnapshotAnimated::snapshotAnimatedDuration.get(); }; auto setter = [](float value) { SnapshotAnimated::snapshotAnimatedDuration.set(value); }; @@ -207,13 +201,6 @@ void setupPreferences() { auto setter = [](float value) { FaceTracker::setEyeDeflection(value); }; preferences->addPreference(new SliderPreference(AVATAR_TUNING, "Face tracker eye deflection", getter, setter)); } - { - auto getter = []()->QString { return DependencyManager::get()->getHostname(); }; - auto setter = [](const QString& value) { DependencyManager::get()->setHostname(value); }; - auto preference = new EditPreference(AVATAR_TUNING, "Faceshift hostname", getter, setter); - preference->setPlaceholderText("localhost"); - preferences->addPreference(preference); - } { auto getter = [=]()->QString { return myAvatar->getAnimGraphOverrideUrl().toString(); }; auto setter = [=](const QString& value) { myAvatar->setAnimGraphOverrideUrl(QUrl(value)); }; diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index f75190530c..59ecce5bc7 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -194,3 +194,10 @@ void Snapshot::uploadSnapshot(const QString& filename, const QUrl& href) { multiPart); } +QString Snapshot::getSnapshotsLocation() { + return snapshotsLocation.get(""); +} + +void Snapshot::setSnapshotsLocation(const QString& location) { + snapshotsLocation.set(location); +} diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index 14e1bc2e9f..93ffbbc7bb 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -18,6 +18,7 @@ #include #include +#include class QFile; class QTemporaryFile; @@ -32,7 +33,9 @@ private: QUrl _URL; }; -class Snapshot { +class Snapshot : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY public: static QString saveSnapshot(QImage image); static QTemporaryFile* saveTempSnapshot(QImage image); @@ -40,6 +43,10 @@ public: static Setting::Handle snapshotsLocation; static void uploadSnapshot(const QString& filename, const QUrl& href = QUrl("")); + +public slots: + Q_INVOKABLE QString getSnapshotsLocation(); + Q_INVOKABLE void setSnapshotsLocation(const QString& location); private: static QFile* savedFileForSnapshot(QImage & image, bool isTemporary); }; diff --git a/interface/src/ui/SnapshotUploader.cpp b/interface/src/ui/SnapshotUploader.cpp index 411e892de5..aa37608476 100644 --- a/interface/src/ui/SnapshotUploader.cpp +++ b/interface/src/ui/SnapshotUploader.cpp @@ -49,6 +49,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { userStoryObject.insert("place_name", placeName); userStoryObject.insert("path", currentPath); userStoryObject.insert("action", "snapshot"); + userStoryObject.insert("audience", "for_url"); rootObject.insert("user_story", userStoryObject); auto accountManager = DependencyManager::get(); @@ -61,7 +62,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { QJsonDocument(rootObject).toJson()); } else { - emit DependencyManager::get()->snapshotShared(contents); + emit DependencyManager::get()->snapshotShared(true, contents); delete this; } } @@ -72,12 +73,13 @@ void SnapshotUploader::uploadFailure(QNetworkReply& reply) { if (replyString.size() == 0) { replyString = reply.errorString(); } - emit DependencyManager::get()->snapshotShared(replyString); // maybe someday include _inWorldLocation, _filename? + emit DependencyManager::get()->snapshotShared(true, replyString); // maybe someday include _inWorldLocation, _filename? delete this; } void SnapshotUploader::createStorySuccess(QNetworkReply& reply) { - emit DependencyManager::get()->snapshotShared(QString()); + QString replyString = reply.readAll(); + emit DependencyManager::get()->snapshotShared(false, replyString); delete this; } @@ -87,7 +89,7 @@ void SnapshotUploader::createStoryFailure(QNetworkReply& reply) { if (replyString.size() == 0) { replyString = reply.errorString(); } - emit DependencyManager::get()->snapshotShared(replyString); + emit DependencyManager::get()->snapshotShared(true, replyString); delete this; } diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 24b12dacf0..fedead5aa5 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -51,6 +51,7 @@ #include "ui/AvatarInputs.h" #include "avatar/AvatarManager.h" #include "scripting/GlobalServicesScriptingInterface.h" +#include "ui/Snapshot.h" static const float DPI = 30.47f; static const float INCHES_TO_METERS = 1.0f / 39.3701f; @@ -177,6 +178,7 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getRootContext()->setContextProperty("Quat", new Quat()); _webSurface->getRootContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); _webSurface->getRootContext()->setContextProperty("Entities", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("Snapshot", DependencyManager::get().data()); if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { auto tabletScriptingInterface = DependencyManager::get(); diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 2c9376d591..6edd969568 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -86,7 +86,9 @@ void AnimInverseKinematics::setTargetVars( void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std::vector& targets, const AnimPoseVec& underPoses) { // build a list of valid targets from _targetVarVec and animVars _maxTargetIndex = -1; + _hipsTargetIndex = -1; bool removeUnfoundJoints = false; + for (auto& targetVar : _targetVarVec) { if (targetVar.jointIndex == -1) { // this targetVar hasn't been validated yet... @@ -105,15 +107,18 @@ void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std:: AnimPose defaultPose = _skeleton->getAbsolutePose(targetVar.jointIndex, underPoses); glm::quat rotation = animVars.lookupRigToGeometry(targetVar.rotationVar, defaultPose.rot()); glm::vec3 translation = animVars.lookupRigToGeometry(targetVar.positionVar, defaultPose.trans()); - if (target.getType() == IKTarget::Type::HipsRelativeRotationAndPosition) { - translation += _hipsOffset; - } + target.setPose(rotation, translation); target.setIndex(targetVar.jointIndex); targets.push_back(target); if (targetVar.jointIndex > _maxTargetIndex) { _maxTargetIndex = targetVar.jointIndex; } + + // record the index of the hips ik target. + if (target.getIndex() == _hipsIndex) { + _hipsTargetIndex = (int)targets.size() - 1; + } } } } @@ -242,18 +247,21 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe // the tip's parent-relative as we proceed up the chain glm::quat tipParentOrientation = absolutePoses[pivotIndex].rot(); + // NOTE: if this code is removed, the head will remain rigid, causing the spine/hips to thrust forward backward + // as the head is nodded. if (targetType == IKTarget::Type::HmdHead) { + // rotate tip directly to target orientation tipOrientation = target.getRotation(); - glm::quat tipRelativeRotation = glm::normalize(tipOrientation * glm::inverse(tipParentOrientation)); + glm::quat tipRelativeRotation = glm::inverse(tipParentOrientation) * tipOrientation; - // enforce tip's constraint + // then enforce tip's constraint RotationConstraint* constraint = getConstraint(tipIndex); if (constraint) { bool constrained = constraint->apply(tipRelativeRotation); if (constrained) { - tipOrientation = glm::normalize(tipRelativeRotation * tipParentOrientation); - tipRelativeRotation = glm::normalize(tipOrientation * glm::inverse(tipParentOrientation)); + tipOrientation = tipParentOrientation * tipRelativeRotation; + tipRelativeRotation = tipRelativeRotation; } } // store the relative rotation change in the accumulator @@ -277,7 +285,9 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe const float MIN_AXIS_LENGTH = 1.0e-4f; RotationConstraint* constraint = getConstraint(pivotIndex); - if (constraint && constraint->isLowerSpine() && tipIndex != _headIndex) { + + // only allow swing on lowerSpine if there is a hips IK target. + if (_hipsTargetIndex < 0 && constraint && constraint->isLowerSpine() && tipIndex != _headIndex) { // for these types of targets we only allow twist at the lower-spine // (this prevents the hand targets from bending the spine too much and thereby driving the hips too far) glm::vec3 twistAxis = absolutePoses[pivotIndex].trans() - absolutePoses[pivotsParentIndex].trans(); @@ -420,13 +430,13 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars _relativePoses[i].trans() = underPoses[i].trans(); } - if (!_relativePoses.empty()) { + if (!underPoses.empty()) { // Sometimes the underpose itself can violate the constraints. Rather than // clamp the animation we dynamically expand each constraint to accomodate it. std::map::iterator constraintItr = _constraints.begin(); while (constraintItr != _constraints.end()) { int index = constraintItr->first; - constraintItr->second->dynamicallyAdjustLimits(_relativePoses[index].rot()); + constraintItr->second->dynamicallyAdjustLimits(underPoses[index].rot()); ++constraintItr; } } @@ -441,64 +451,76 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars computeTargets(animVars, targets, underPoses); } - // debug render ik targets - if (context.getEnableDebugDrawIKTargets()) { - const vec4 WHITE(1.0f); - glm::mat4 rigToAvatarMat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3()); - - for (auto& target : targets) { - glm::mat4 geomTargetMat = createMatFromQuatAndPos(target.getRotation(), target.getTranslation()); - glm::mat4 avatarTargetMat = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat; - - QString name = QString("ikTarget%1").arg(target.getIndex()); - DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), WHITE); - } - } else if (context.getEnableDebugDrawIKTargets() != _previousEnableDebugIKTargets) { - // remove markers if they were added last frame. - for (auto& target : targets) { - QString name = QString("ikTarget%1").arg(target.getIndex()); - DebugDraw::getInstance().removeMyAvatarMarker(name); - } - } - - _previousEnableDebugIKTargets = context.getEnableDebugDrawIKTargets(); - if (targets.empty()) { - // no IK targets but still need to enforce constraints - std::map::iterator constraintItr = _constraints.begin(); - while (constraintItr != _constraints.end()) { - int index = constraintItr->first; - glm::quat rotation = _relativePoses[index].rot(); - constraintItr->second->apply(rotation); - _relativePoses[index].rot() = rotation; - ++constraintItr; - } + _relativePoses = underPoses; } else { { PROFILE_RANGE_EX(simulation_animation, "ik/shiftHips", 0xffff00ff, 0); - // shift hips according to the _hipsOffset from the previous frame - float offsetLength = glm::length(_hipsOffset); - const float MIN_HIPS_OFFSET_LENGTH = 0.03f; - if (offsetLength > MIN_HIPS_OFFSET_LENGTH && _hipsIndex >= 0) { - // but only if offset is long enough - float scaleFactor = ((offsetLength - MIN_HIPS_OFFSET_LENGTH) / offsetLength); - if (_hipsParentIndex == -1) { - // the hips are the root so _hipsOffset is in the correct frame - _relativePoses[_hipsIndex].trans() = underPoses[_hipsIndex].trans() + scaleFactor * _hipsOffset; + if (_hipsTargetIndex >= 0 && _hipsTargetIndex < (int)targets.size()) { + // slam the hips to match the _hipsTarget + AnimPose absPose = targets[_hipsTargetIndex].getPose(); + int parentIndex = _skeleton->getParentIndex(targets[_hipsTargetIndex].getIndex()); + if (parentIndex != -1) { + _relativePoses[_hipsIndex] = _skeleton->getAbsolutePose(parentIndex, _relativePoses).inverse() * absPose; } else { - // the hips are NOT the root so we need to transform _hipsOffset into hips local-frame - glm::quat hipsFrameRotation = _relativePoses[_hipsParentIndex].rot(); - int index = _skeleton->getParentIndex(_hipsParentIndex); - while (index != -1) { - hipsFrameRotation *= _relativePoses[index].rot(); - index = _skeleton->getParentIndex(index); + _relativePoses[_hipsIndex] = absPose; + } + } else { + // if there is no hips target, shift hips according to the _hipsOffset from the previous frame + float offsetLength = glm::length(_hipsOffset); + const float MIN_HIPS_OFFSET_LENGTH = 0.03f; + if (offsetLength > MIN_HIPS_OFFSET_LENGTH && _hipsIndex >= 0) { + float scaleFactor = ((offsetLength - MIN_HIPS_OFFSET_LENGTH) / offsetLength); + glm::vec3 hipsOffset = scaleFactor * _hipsOffset; + if (_hipsParentIndex == -1) { + _relativePoses[_hipsIndex].trans() = underPoses[_hipsIndex].trans() + hipsOffset; + } else { + auto absHipsPose = _skeleton->getAbsolutePose(_hipsIndex, underPoses); + absHipsPose.trans() += hipsOffset; + _relativePoses[_hipsIndex] = _skeleton->getAbsolutePose(_hipsParentIndex, _relativePoses).inverse() * absHipsPose; } - _relativePoses[_hipsIndex].trans() = underPoses[_hipsIndex].trans() - + glm::inverse(glm::normalize(hipsFrameRotation)) * (scaleFactor * _hipsOffset); } } + + // update all HipsRelative targets to account for the hips shift/ik target. + auto shiftedHipsAbsPose = _skeleton->getAbsolutePose(_hipsIndex, _relativePoses); + auto underHipsAbsPose = _skeleton->getAbsolutePose(_hipsIndex, underPoses); + auto absHipsOffset = shiftedHipsAbsPose.trans() - underHipsAbsPose.trans(); + for (auto& target: targets) { + if (target.getType() == IKTarget::Type::HipsRelativeRotationAndPosition) { + auto pose = target.getPose(); + pose.trans() = pose.trans() + absHipsOffset; + target.setPose(pose.rot(), pose.trans()); + } + } + } + + { + PROFILE_RANGE_EX(simulation_animation, "ik/debugDraw", 0xffff00ff, 0); + + // debug render ik targets + if (context.getEnableDebugDrawIKTargets()) { + const vec4 WHITE(1.0f); + glm::mat4 rigToAvatarMat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3()); + + for (auto& target : targets) { + glm::mat4 geomTargetMat = createMatFromQuatAndPos(target.getRotation(), target.getTranslation()); + glm::mat4 avatarTargetMat = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat; + + QString name = QString("ikTarget%1").arg(target.getIndex()); + DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), WHITE); + } + } else if (context.getEnableDebugDrawIKTargets() != _previousEnableDebugIKTargets) { + // remove markers if they were added last frame. + for (auto& target : targets) { + QString name = QString("ikTarget%1").arg(target.getIndex()); + DebugDraw::getInstance().removeMyAvatarMarker(name); + } + } + + _previousEnableDebugIKTargets = context.getEnableDebugDrawIKTargets(); } { @@ -506,64 +528,70 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars solveWithCyclicCoordinateDescent(targets); } - { + if (_hipsTargetIndex < 0) { PROFILE_RANGE_EX(simulation_animation, "ik/measureHipsOffset", 0xffff00ff, 0); - - // measure new _hipsOffset for next frame - // by looking for discrepancies between where a targeted endEffector is - // and where it wants to be (after IK solutions are done) - glm::vec3 newHipsOffset = Vectors::ZERO; - for (auto& target: targets) { - int targetIndex = target.getIndex(); - if (targetIndex == _headIndex && _headIndex != -1) { - // special handling for headTarget - if (target.getType() == IKTarget::Type::RotationOnly) { - // we want to shift the hips to bring the underPose closer - // to where the head happens to be (overpose) - glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans(); - glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); - const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f; - newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under); - } else if (target.getType() == IKTarget::Type::HmdHead) { - // we want to shift the hips to bring the head to its designated position - glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); - _hipsOffset += target.getTranslation() - actual; - // and ignore all other targets - newHipsOffset = _hipsOffset; - break; - } else if (target.getType() == IKTarget::Type::RotationAndPosition) { - glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); - glm::vec3 targetPosition = target.getTranslation(); - newHipsOffset += targetPosition - actualPosition; - - // Add downward pressure on the hips - newHipsOffset *= 0.95f; - newHipsOffset -= 1.0f; - } - } else if (target.getType() == IKTarget::Type::RotationAndPosition) { - glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); - glm::vec3 targetPosition = target.getTranslation(); - newHipsOffset += targetPosition - actualPosition; - } - } - - // smooth transitions by relaxing _hipsOffset toward the new value - const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f; - float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f; - _hipsOffset += (newHipsOffset - _hipsOffset) * tau; - - // clamp the hips offset - float hipsOffsetLength = glm::length(_hipsOffset); - if (hipsOffsetLength > _maxHipsOffsetLength) { - _hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength; - } - + computeHipsOffset(targets, underPoses, dt); + } else { + _hipsOffset = Vectors::ZERO; } } } return _relativePoses; } +void AnimInverseKinematics::computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt) { + // measure new _hipsOffset for next frame + // by looking for discrepancies between where a targeted endEffector is + // and where it wants to be (after IK solutions are done) + glm::vec3 newHipsOffset = Vectors::ZERO; + for (auto& target: targets) { + int targetIndex = target.getIndex(); + if (targetIndex == _headIndex && _headIndex != -1) { + // special handling for headTarget + if (target.getType() == IKTarget::Type::RotationOnly) { + // we want to shift the hips to bring the underPose closer + // to where the head happens to be (overpose) + glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans(); + glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); + const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f; + newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under); + } else if (target.getType() == IKTarget::Type::HmdHead) { + // we want to shift the hips to bring the head to its designated position + glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); + _hipsOffset += target.getTranslation() - actual; + // and ignore all other targets + newHipsOffset = _hipsOffset; + break; + } else if (target.getType() == IKTarget::Type::RotationAndPosition) { + glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); + glm::vec3 targetPosition = target.getTranslation(); + newHipsOffset += targetPosition - actualPosition; + + // Add downward pressure on the hips + const float PRESSURE_SCALE_FACTOR = 0.95f; + const float PRESSURE_TRANSLATION_OFFSET = 1.0f; + newHipsOffset *= PRESSURE_SCALE_FACTOR; + newHipsOffset -= PRESSURE_TRANSLATION_OFFSET; + } + } else if (target.getType() == IKTarget::Type::RotationAndPosition) { + glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); + glm::vec3 targetPosition = target.getTranslation(); + newHipsOffset += targetPosition - actualPosition; + } + } + + // smooth transitions by relaxing _hipsOffset toward the new value + const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f; + float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f; + _hipsOffset += (newHipsOffset - _hipsOffset) * tau; + + // clamp the hips offset + float hipsOffsetLength = glm::length(_hipsOffset); + if (hipsOffsetLength > _maxHipsOffsetLength) { + _hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength; + } +} + void AnimInverseKinematics::setMaxHipsOffsetLength(float maxLength) { // manually adjust scale here const float METERS_TO_CENTIMETERS = 100.0f; @@ -594,6 +622,22 @@ void AnimInverseKinematics::clearConstraints() { _constraints.clear(); } +// set up swing limits around a swingTwistConstraint in an ellipse, where lateralSwingTheta is the swing limit for lateral swings (side to side) +// anteriorSwingTheta is swing limit for forward and backward swings. (where x-axis of reference rotation is sideways and -z-axis is forward) +static void setEllipticalSwingLimits(SwingTwistConstraint* stConstraint, float lateralSwingTheta, float anteriorSwingTheta) { + assert(stConstraint); + const int NUM_SUBDIVISIONS = 8; + std::vector minDots; + minDots.reserve(NUM_SUBDIVISIONS); + float dTheta = TWO_PI / NUM_SUBDIVISIONS; + float theta = 0.0f; + for (int i = 0; i < NUM_SUBDIVISIONS; i++) { + minDots.push_back(cosf(glm::length(glm::vec2(anteriorSwingTheta * cosf(theta), lateralSwingTheta * sinf(theta))))); + theta += dTheta; + } + stConstraint->setSwingLimits(minDots); +} + void AnimInverseKinematics::initConstraints() { if (!_skeleton) { return; @@ -783,41 +827,31 @@ void AnimInverseKinematics::initConstraints() { } else if (baseName.startsWith("Spine", Qt::CaseSensitive)) { SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot()); - const float MAX_SPINE_TWIST = PI / 12.0f; + const float MAX_SPINE_TWIST = PI / 20.0f; stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST); - std::vector minDots; - const float MAX_SPINE_SWING = PI / 10.0f; - minDots.push_back(cosf(MAX_SPINE_SWING)); - stConstraint->setSwingLimits(minDots); + // limit lateral swings more then forward-backward swings + const float MAX_SPINE_LATERAL_SWING = PI / 30.0f; + const float MAX_SPINE_ANTERIOR_SWING = PI / 20.0f; + setEllipticalSwingLimits(stConstraint, MAX_SPINE_LATERAL_SWING, MAX_SPINE_ANTERIOR_SWING); + if (0 == baseName.compare("Spine1", Qt::CaseSensitive) || 0 == baseName.compare("Spine", Qt::CaseSensitive)) { stConstraint->setLowerSpine(true); } constraint = static_cast(stConstraint); - } else if (baseName.startsWith("Hips2", Qt::CaseSensitive)) { - SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); - stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot()); - const float MAX_SPINE_TWIST = PI / 8.0f; - stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST); - std::vector minDots; - const float MAX_SPINE_SWING = PI / 14.0f; - minDots.push_back(cosf(MAX_SPINE_SWING)); - stConstraint->setSwingLimits(minDots); - - constraint = static_cast(stConstraint); } else if (0 == baseName.compare("Neck", Qt::CaseSensitive)) { SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot()); - const float MAX_NECK_TWIST = PI / 9.0f; + const float MAX_NECK_TWIST = PI / 10.0f; stConstraint->setTwistLimits(-MAX_NECK_TWIST, MAX_NECK_TWIST); - std::vector minDots; - const float MAX_NECK_SWING = PI / 8.0f; - minDots.push_back(cosf(MAX_NECK_SWING)); - stConstraint->setSwingLimits(minDots); + // limit lateral swings more then forward-backward swings + const float MAX_NECK_LATERAL_SWING = PI / 10.0f; + const float MAX_NECK_ANTERIOR_SWING = PI / 8.0f; + setEllipticalSwingLimits(stConstraint, MAX_NECK_LATERAL_SWING, MAX_NECK_ANTERIOR_SWING); constraint = static_cast(stConstraint); } else if (0 == baseName.compare("Head", Qt::CaseSensitive)) { @@ -872,7 +906,7 @@ void AnimInverseKinematics::initConstraints() { // we determine the max/min angles by rotating the swing limit lines from parent- to child-frame // then measure the angles to swing the yAxis into alignment - const float MIN_KNEE_ANGLE = 0.0f; + const float MIN_KNEE_ANGLE = 0.097f; // ~5 deg const float MAX_KNEE_ANGLE = 7.0f * PI / 8.0f; glm::quat invReferenceRotation = glm::inverse(referenceRotation); glm::vec3 minSwingAxis = invReferenceRotation * glm::angleAxis(MIN_KNEE_ANGLE, hingeAxis) * Vectors::UNIT_Y; diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index 366e5f765e..c91b7aa9c4 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -55,6 +55,7 @@ protected: RotationConstraint* getConstraint(int index); void clearConstraints(); void initConstraints(); + void computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt); // no copies AnimInverseKinematics(const AnimInverseKinematics&) = delete; @@ -91,6 +92,7 @@ protected: int _headIndex { -1 }; int _hipsIndex { -1 }; int _hipsParentIndex { -1 }; + int _hipsTargetIndex { -1 }; // _maxTargetIndex is tracked to help optimize the recalculation of absolute poses // during the the cyclic coordinate descent algorithm diff --git a/libraries/animation/src/AnimManipulator.cpp b/libraries/animation/src/AnimManipulator.cpp index 111501898a..070949ab3b 100644 --- a/libraries/animation/src/AnimManipulator.cpp +++ b/libraries/animation/src/AnimManipulator.cpp @@ -12,6 +12,16 @@ #include "AnimUtil.h" #include "AnimationLogging.h" +AnimManipulator::JointVar::JointVar(const QString& jointNameIn, Type rotationTypeIn, Type translationTypeIn, + const QString& rotationVarIn, const QString& translationVarIn) : + jointName(jointNameIn), + rotationType(rotationTypeIn), + translationType(translationTypeIn), + rotationVar(rotationVarIn), + translationVar(translationVarIn), + jointIndex(-1), + hasPerformedJointLookup(false) {} + AnimManipulator::AnimManipulator(const QString& id, float alpha) : AnimNode(AnimNode::Type::Manipulator, id), _alpha(alpha) { @@ -36,7 +46,10 @@ const AnimPoseVec& AnimManipulator::overlay(const AnimVariantMap& animVars, cons } for (auto& jointVar : _jointVars) { + if (!jointVar.hasPerformedJointLookup) { + + // map from joint name to joint index and cache the result. jointVar.jointIndex = _skeleton->nameToJointIndex(jointVar.jointName); if (jointVar.jointIndex < 0) { qCWarning(animation) << "AnimManipulator could not find jointName" << jointVar.jointName << "in skeleton"; @@ -100,34 +113,62 @@ AnimPose AnimManipulator::computeRelativePoseFromJointVar(const AnimVariantMap& AnimPose defaultAbsPose = _skeleton->getAbsolutePose(jointVar.jointIndex, underPoses); - if (jointVar.type == JointVar::Type::AbsoluteRotation || jointVar.type == JointVar::Type::AbsolutePosition) { + // compute relative translation + glm::vec3 relTrans; + switch (jointVar.translationType) { + case JointVar::Type::Absolute: { + glm::vec3 absTrans = animVars.lookupRigToGeometry(jointVar.translationVar, defaultAbsPose.trans()); - if (jointVar.type == JointVar::Type::AbsoluteRotation) { - defaultAbsPose.rot() = animVars.lookupRigToGeometry(jointVar.var, defaultAbsPose.rot()); - } else if (jointVar.type == JointVar::Type::AbsolutePosition) { - defaultAbsPose.trans() = animVars.lookupRigToGeometry(jointVar.var, defaultAbsPose.trans()); + // convert to from absolute to relative. + AnimPose parentAbsPose; + int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex); + if (parentIndex >= 0) { + parentAbsPose = _skeleton->getAbsolutePose(parentIndex, underPoses); + } + + // convert from absolute to relative + relTrans = transformPoint(parentAbsPose.inverse(), absTrans); + break; } - - // because jointVar is absolute, we must use an absolute parent frame to convert into a relative pose. - AnimPose parentAbsPose = AnimPose::identity; - int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex); - if (parentIndex >= 0) { - parentAbsPose = _skeleton->getAbsolutePose(parentIndex, underPoses); - } - - // convert from absolute to relative - return parentAbsPose.inverse() * defaultAbsPose; - - } else { - - // override the default rel pose - AnimPose relPose = defaultRelPose; - if (jointVar.type == JointVar::Type::RelativeRotation) { - relPose.rot() = animVars.lookupRigToGeometry(jointVar.var, defaultRelPose.rot()); - } else if (jointVar.type == JointVar::Type::RelativePosition) { - relPose.trans() = animVars.lookupRigToGeometry(jointVar.var, defaultRelPose.trans()); - } - - return relPose; + case JointVar::Type::Relative: + relTrans = animVars.lookupRigToGeometryVector(jointVar.translationVar, defaultRelPose.trans()); + break; + case JointVar::Type::UnderPose: + relTrans = underPoses[jointVar.jointIndex].trans(); + break; + case JointVar::Type::Default: + default: + relTrans = defaultRelPose.trans(); + break; } + + glm::quat relRot; + switch (jointVar.rotationType) { + case JointVar::Type::Absolute: { + glm::quat absRot = animVars.lookupRigToGeometry(jointVar.translationVar, defaultAbsPose.rot()); + + // convert to from absolute to relative. + AnimPose parentAbsPose; + int parentIndex = _skeleton->getParentIndex(jointVar.jointIndex); + if (parentIndex >= 0) { + parentAbsPose = _skeleton->getAbsolutePose(parentIndex, underPoses); + } + + // convert from absolute to relative + relRot = glm::inverse(parentAbsPose.rot()) * absRot; + break; + } + case JointVar::Type::Relative: + relRot = animVars.lookupRigToGeometry(jointVar.translationVar, defaultRelPose.rot()); + break; + case JointVar::Type::UnderPose: + relRot = underPoses[jointVar.jointIndex].rot(); + break; + case JointVar::Type::Default: + default: + relRot = defaultRelPose.rot(); + break; + } + + return AnimPose(glm::vec3(1), relRot, relTrans); } diff --git a/libraries/animation/src/AnimManipulator.h b/libraries/animation/src/AnimManipulator.h index 26f50a7dd9..1134f75da9 100644 --- a/libraries/animation/src/AnimManipulator.h +++ b/libraries/animation/src/AnimManipulator.h @@ -31,17 +31,20 @@ public: struct JointVar { enum class Type { - AbsoluteRotation = 0, - AbsolutePosition, - RelativeRotation, - RelativePosition, + Absolute, + Relative, + UnderPose, + Default, NumTypes }; - JointVar(const QString& varIn, const QString& jointNameIn, Type typeIn) : var(varIn), jointName(jointNameIn), type(typeIn), jointIndex(-1), hasPerformedJointLookup(false) {} - QString var = ""; + JointVar(const QString& jointNameIn, Type rotationType, Type translationType, const QString& rotationVarIn, const QString& translationVarIn); QString jointName = ""; - Type type = Type::AbsoluteRotation; + Type rotationType = Type::Absolute; + Type translationType = Type::Absolute; + QString rotationVar = ""; + QString translationVar = ""; + int jointIndex = -1; bool hasPerformedJointLookup = false; bool isRelative = false; diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index 876913fc58..bda4541f36 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -79,10 +79,10 @@ static AnimStateMachine::InterpType stringToInterpType(const QString& str) { static const char* animManipulatorJointVarTypeToString(AnimManipulator::JointVar::Type type) { switch (type) { - case AnimManipulator::JointVar::Type::AbsoluteRotation: return "absoluteRotation"; - case AnimManipulator::JointVar::Type::AbsolutePosition: return "absolutePosition"; - case AnimManipulator::JointVar::Type::RelativeRotation: return "relativeRotation"; - case AnimManipulator::JointVar::Type::RelativePosition: return "relativePosition"; + case AnimManipulator::JointVar::Type::Absolute: return "absolute"; + case AnimManipulator::JointVar::Type::Relative: return "relative"; + case AnimManipulator::JointVar::Type::UnderPose: return "underPose"; + case AnimManipulator::JointVar::Type::Default: return "default"; case AnimManipulator::JointVar::Type::NumTypes: return nullptr; }; return nullptr; @@ -339,7 +339,8 @@ static const char* boneSetStrings[AnimOverlay::NumBoneSets] = { "spineOnly", "empty", "leftHand", - "rightHand" + "rightHand", + "hipsOnly" }; static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) { @@ -406,17 +407,25 @@ static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const Q } auto jointObj = jointValue.toObject(); - READ_STRING(type, jointObj, id, jsonUrl, nullptr); READ_STRING(jointName, jointObj, id, jsonUrl, nullptr); - READ_STRING(var, jointObj, id, jsonUrl, nullptr); + READ_STRING(rotationType, jointObj, id, jsonUrl, nullptr); + READ_STRING(translationType, jointObj, id, jsonUrl, nullptr); + READ_STRING(rotationVar, jointObj, id, jsonUrl, nullptr); + READ_STRING(translationVar, jointObj, id, jsonUrl, nullptr); - AnimManipulator::JointVar::Type jointVarType = stringToAnimManipulatorJointVarType(type); - if (jointVarType == AnimManipulator::JointVar::Type::NumTypes) { - qCCritical(animation) << "AnimNodeLoader, bad type in \"joints\", id =" << id << ", url =" << jsonUrl.toDisplayString(); - return nullptr; + AnimManipulator::JointVar::Type jointVarRotationType = stringToAnimManipulatorJointVarType(rotationType); + if (jointVarRotationType == AnimManipulator::JointVar::Type::NumTypes) { + qCWarning(animation) << "AnimNodeLoader, bad rotationType in \"joints\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + jointVarRotationType = AnimManipulator::JointVar::Type::Default; } - AnimManipulator::JointVar jointVar(var, jointName, jointVarType); + AnimManipulator::JointVar::Type jointVarTranslationType = stringToAnimManipulatorJointVarType(translationType); + if (jointVarTranslationType == AnimManipulator::JointVar::Type::NumTypes) { + qCWarning(animation) << "AnimNodeLoader, bad translationType in \"joints\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + jointVarTranslationType = AnimManipulator::JointVar::Type::Default; + } + + AnimManipulator::JointVar jointVar(jointName, jointVarRotationType, jointVarTranslationType, rotationVar, translationVar); node->addJointVar(jointVar); }; diff --git a/libraries/animation/src/AnimOverlay.cpp b/libraries/animation/src/AnimOverlay.cpp index dbc635af66..e086413dde 100644 --- a/libraries/animation/src/AnimOverlay.cpp +++ b/libraries/animation/src/AnimOverlay.cpp @@ -34,6 +34,7 @@ void AnimOverlay::buildBoneSet(BoneSet boneSet) { case SpineOnlyBoneSet: buildSpineOnlyBoneSet(); break; case LeftHandBoneSet: buildLeftHandBoneSet(); break; case RightHandBoneSet: buildRightHandBoneSet(); break; + case HipsOnlyBoneSet: buildHipsOnlyBoneSet(); break; default: case EmptyBoneSet: buildEmptyBoneSet(); break; } @@ -188,6 +189,13 @@ void AnimOverlay::buildRightHandBoneSet() { }); } +void AnimOverlay::buildHipsOnlyBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int hipsJoint = _skeleton->nameToJointIndex("Hips"); + _boneSetVec[hipsJoint] = 1.0f; +} + // for AnimDebugDraw rendering const AnimPoseVec& AnimOverlay::getPosesInternal() const { return _poses; diff --git a/libraries/animation/src/AnimOverlay.h b/libraries/animation/src/AnimOverlay.h index 2f34c07309..ed9439feb7 100644 --- a/libraries/animation/src/AnimOverlay.h +++ b/libraries/animation/src/AnimOverlay.h @@ -37,6 +37,7 @@ public: EmptyBoneSet, LeftHandBoneSet, RightHandBoneSet, + HipsOnlyBoneSet, NumBoneSets }; @@ -75,6 +76,7 @@ public: void buildEmptyBoneSet(); void buildLeftHandBoneSet(); void buildRightHandBoneSet(); + void buildHipsOnlyBoneSet(); // no copies AnimOverlay(const AnimOverlay&) = delete; diff --git a/libraries/animation/src/AnimVariant.h b/libraries/animation/src/AnimVariant.h index 3466013ff6..d383b5abb8 100644 --- a/libraries/animation/src/AnimVariant.h +++ b/libraries/animation/src/AnimVariant.h @@ -165,6 +165,15 @@ public: } } + glm::vec3 lookupRigToGeometryVector(const QString& key, const glm::vec3& defaultValue) const { + if (key.isEmpty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? transformVectorFast(_rigToGeometryMat, iter->second.getVec3()) : defaultValue; + } + } + const glm::quat& lookupRaw(const QString& key, const glm::quat& defaultValue) const { if (key.isEmpty()) { return defaultValue; diff --git a/libraries/animation/src/IKTarget.h b/libraries/animation/src/IKTarget.h index 9ea34a6165..acb01d9861 100644 --- a/libraries/animation/src/IKTarget.h +++ b/libraries/animation/src/IKTarget.h @@ -21,13 +21,14 @@ public: RotationOnly, HmdHead, HipsRelativeRotationAndPosition, - Unknown, + Unknown }; IKTarget() {} const glm::vec3& getTranslation() const { return _pose.trans(); } const glm::quat& getRotation() const { return _pose.rot(); } + const AnimPose& getPose() const { return _pose; } int getIndex() const { return _index; } Type getType() const { return _type; } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index fb0867e2de..700761b248 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -46,7 +46,6 @@ static bool isEqual(const glm::quat& p, const glm::quat& q) { const glm::vec3 DEFAULT_RIGHT_EYE_POS(-0.3f, 0.9f, 0.0f); const glm::vec3 DEFAULT_LEFT_EYE_POS(0.3f, 0.9f, 0.0f); const glm::vec3 DEFAULT_HEAD_POS(0.0f, 0.75f, 0.0f); -const glm::vec3 DEFAULT_NECK_POS(0.0f, 0.70f, 0.0f); void Rig::overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame) { @@ -1020,98 +1019,81 @@ glm::quat Rig::getJointDefaultRotationInParentFrame(int jointIndex) { } void Rig::updateFromHeadParameters(const HeadParameters& params, float dt) { - updateNeckJoint(params.neckJointIndex, params); + updateHeadAnimVars(params); _animVars.set("isTalking", params.isTalking); _animVars.set("notIsTalking", !params.isTalking); + + if (params.hipsEnabled) { + _animVars.set("hipsType", (int)IKTarget::Type::RotationAndPosition); + _animVars.set("hipsPosition", extractTranslation(params.hipsMatrix)); + _animVars.set("hipsRotation", glmExtractRotation(params.hipsMatrix)); + } else { + _animVars.set("hipsType", (int)IKTarget::Type::Unknown); + } + + if (params.spine2Enabled) { + _animVars.set("spine2Type", (int)IKTarget::Type::RotationAndPosition); + _animVars.set("spine2Position", extractTranslation(params.spine2Matrix)); + _animVars.set("spine2Rotation", glmExtractRotation(params.spine2Matrix)); + } else { + _animVars.set("spine2Type", (int)IKTarget::Type::Unknown); + } } void Rig::updateFromEyeParameters(const EyeParameters& params) { - updateEyeJoint(params.leftEyeJointIndex, params.modelTranslation, params.modelRotation, - params.worldHeadOrientation, params.eyeLookAt, params.eyeSaccade); - updateEyeJoint(params.rightEyeJointIndex, params.modelTranslation, params.modelRotation, - params.worldHeadOrientation, params.eyeLookAt, params.eyeSaccade); + updateEyeJoint(params.leftEyeJointIndex, params.modelTranslation, params.modelRotation, params.eyeLookAt, params.eyeSaccade); + updateEyeJoint(params.rightEyeJointIndex, params.modelTranslation, params.modelRotation, params.eyeLookAt, params.eyeSaccade); } -void Rig::computeHeadNeckAnimVars(const AnimPose& hmdPose, glm::vec3& headPositionOut, glm::quat& headOrientationOut, - glm::vec3& neckPositionOut, glm::quat& neckOrientationOut) const { +void Rig::computeHeadFromHMD(const AnimPose& hmdPose, glm::vec3& headPositionOut, glm::quat& headOrientationOut) const { // the input hmd values are in avatar/rig space const glm::vec3& hmdPosition = hmdPose.trans(); - const glm::quat& hmdOrientation = hmdPose.rot(); + + // the HMD looks down the negative z axis, but the head bone looks down the z axis, so apply a 180 degree rotation. + const glm::quat& hmdOrientation = hmdPose.rot() * Quaternions::Y_180; // TODO: cache jointIndices int rightEyeIndex = indexOfJoint("RightEye"); int leftEyeIndex = indexOfJoint("LeftEye"); int headIndex = indexOfJoint("Head"); - int neckIndex = indexOfJoint("Neck"); glm::vec3 absRightEyePos = rightEyeIndex != -1 ? getAbsoluteDefaultPose(rightEyeIndex).trans() : DEFAULT_RIGHT_EYE_POS; glm::vec3 absLeftEyePos = leftEyeIndex != -1 ? getAbsoluteDefaultPose(leftEyeIndex).trans() : DEFAULT_LEFT_EYE_POS; glm::vec3 absHeadPos = headIndex != -1 ? getAbsoluteDefaultPose(headIndex).trans() : DEFAULT_HEAD_POS; - glm::vec3 absNeckPos = neckIndex != -1 ? getAbsoluteDefaultPose(neckIndex).trans() : DEFAULT_NECK_POS; glm::vec3 absCenterEyePos = (absRightEyePos + absLeftEyePos) / 2.0f; glm::vec3 eyeOffset = absCenterEyePos - absHeadPos; - glm::vec3 headOffset = absHeadPos - absNeckPos; - // apply simplistic head/neck model - - // head headPositionOut = hmdPosition - hmdOrientation * eyeOffset; + headOrientationOut = hmdOrientation; - - // neck - neckPositionOut = hmdPosition - hmdOrientation * (headOffset + eyeOffset); - - // slerp between default orientation and hmdOrientation - neckOrientationOut = safeMix(hmdOrientation, _animSkeleton->getRelativeDefaultPose(neckIndex).rot(), 0.5f); } -void Rig::updateNeckJoint(int index, const HeadParameters& params) { - if (_animSkeleton && index >= 0 && index < _animSkeleton->getNumJoints()) { - glm::quat yFlip180 = glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); - if (params.isInHMD) { - glm::vec3 headPos, neckPos; - glm::quat headRot, neckRot; - - AnimPose hmdPose(glm::vec3(1.0f), params.rigHeadOrientation * yFlip180, params.rigHeadPosition); - computeHeadNeckAnimVars(hmdPose, headPos, headRot, neckPos, neckRot); - - // debug rendering -#ifdef DEBUG_RENDERING - const glm::vec4 red(1.0f, 0.0f, 0.0f, 1.0f); - const glm::vec4 green(0.0f, 1.0f, 0.0f, 1.0f); - - // transform from bone into avatar space - AnimPose headPose(glm::vec3(1), headRot, headPos); - DebugDraw::getInstance().addMyAvatarMarker("headTarget", headPose.rot, headPose.trans, red); - - // transform from bone into avatar space - AnimPose neckPose(glm::vec3(1), neckRot, neckPos); - DebugDraw::getInstance().addMyAvatarMarker("neckTarget", neckPose.rot, neckPose.trans, green); -#endif - - _animVars.set("headPosition", headPos); - _animVars.set("headRotation", headRot); - _animVars.set("headType", (int)IKTarget::Type::HmdHead); - _animVars.set("neckPosition", neckPos); - _animVars.set("neckRotation", neckRot); - _animVars.set("neckType", (int)IKTarget::Type::Unknown); // 'Unknown' disables the target - +void Rig::updateHeadAnimVars(const HeadParameters& params) { + if (_animSkeleton) { + if (params.headEnabled) { + _animVars.set("headPosition", params.rigHeadPosition); + _animVars.set("headRotation", params.rigHeadOrientation); + if (params.hipsEnabled) { + // Since there is an explicit hips ik target, switch the head to use the more generic RotationAndPosition IK chain type. + // this will allow the spine to bend more, ensuring that it can reach the head target position. + _animVars.set("headType", (int)IKTarget::Type::RotationAndPosition); + } else { + // When there is no hips IK target, use the HmdHead IK chain type. This will make the spine very stiff, + // but because the IK _hipsOffset is enabled, the hips will naturally follow underneath the head. + _animVars.set("headType", (int)IKTarget::Type::HmdHead); + } } else { _animVars.unset("headPosition"); - _animVars.set("headRotation", params.rigHeadOrientation * yFlip180); - _animVars.set("headAndNeckType", (int)IKTarget::Type::RotationOnly); + _animVars.set("headRotation", params.rigHeadOrientation); _animVars.set("headType", (int)IKTarget::Type::RotationOnly); - _animVars.unset("neckPosition"); - _animVars.unset("neckRotation"); - _animVars.set("neckType", (int)IKTarget::Type::RotationOnly); } } } -void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::quat& worldHeadOrientation, const glm::vec3& lookAtSpot, const glm::vec3& saccade) { +void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::vec3& lookAtSpot, const glm::vec3& saccade) { // TODO: does not properly handle avatar scale. @@ -1161,13 +1143,19 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f const glm::vec3 bodyCapsuleStart = bodyCapsuleCenter - glm::vec3(0, params.bodyCapsuleHalfHeight, 0); const glm::vec3 bodyCapsuleEnd = bodyCapsuleCenter + glm::vec3(0, params.bodyCapsuleHalfHeight, 0); + // TODO: add isHipsEnabled + bool bodySensorTrackingEnabled = params.isLeftFootEnabled || params.isRightFootEnabled; + if (params.isLeftEnabled) { - // prevent the hand IK targets from intersecting the body capsule glm::vec3 handPosition = params.leftPosition; - glm::vec3 displacement; - if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) { - handPosition -= displacement; + + if (!bodySensorTrackingEnabled) { + // prevent the hand IK targets from intersecting the body capsule + glm::vec3 displacement; + if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) { + handPosition -= displacement; + } } _animVars.set("leftHandPosition", handPosition); @@ -1181,11 +1169,14 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f if (params.isRightEnabled) { - // prevent the hand IK targets from intersecting the body capsule glm::vec3 handPosition = params.rightPosition; - glm::vec3 displacement; - if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) { - handPosition -= displacement; + + if (!bodySensorTrackingEnabled) { + // prevent the hand IK targets from intersecting the body capsule + glm::vec3 displacement; + if (findSphereCapsulePenetration(handPosition, HAND_RADIUS, bodyCapsuleStart, bodyCapsuleEnd, bodyCapsuleRadius, displacement)) { + handPosition -= displacement; + } } _animVars.set("rightHandPosition", handPosition); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 2cd20c2704..2d024628f5 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -42,16 +42,17 @@ public: }; struct HeadParameters { - glm::quat worldHeadOrientation = glm::quat(); // world space (-z forward) - glm::quat rigHeadOrientation = glm::quat(); // rig space (-z forward) - glm::vec3 rigHeadPosition = glm::vec3(); // rig space - bool isInHMD = false; - int neckJointIndex = -1; + glm::mat4 hipsMatrix = glm::mat4(); // rig space + glm::mat4 spine2Matrix = glm::mat4(); // rig space + glm::quat rigHeadOrientation = glm::quat(); // rig space (-z forward) + glm::vec3 rigHeadPosition = glm::vec3(); // rig space + bool hipsEnabled = false; + bool headEnabled = false; + bool spine2Enabled = false; bool isTalking = false; }; struct EyeParameters { - glm::quat worldHeadOrientation = glm::quat(); glm::vec3 eyeLookAt = glm::vec3(); // world space glm::vec3 eyeSaccade = glm::vec3(); // world space glm::vec3 modelTranslation = glm::vec3(); @@ -228,6 +229,9 @@ public: void setEnableDebugDrawIKTargets(bool enableDebugDrawIKTargets) { _enableDebugDrawIKTargets = enableDebugDrawIKTargets; } + // input assumed to be in rig space + void computeHeadFromHMD(const AnimPose& hmdPose, glm::vec3& headPositionOut, glm::quat& headOrientationOut) const; + signals: void onLoadComplete(); @@ -237,10 +241,9 @@ protected: void applyOverridePoses(); void buildAbsoluteRigPoses(const AnimPoseVec& relativePoses, AnimPoseVec& absolutePosesOut); - void updateNeckJoint(int index, const HeadParameters& params); - void computeHeadNeckAnimVars(const AnimPose& hmdPose, glm::vec3& headPositionOut, glm::quat& headOrientationOut, - glm::vec3& neckPositionOut, glm::quat& neckOrientationOut) const; - void updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::quat& worldHeadOrientation, const glm::vec3& lookAt, const glm::vec3& saccade); + void updateHeadAnimVars(const HeadParameters& params); + + void updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::vec3& lookAt, const glm::vec3& saccade); void calcAnimAlpha(float speed, const std::vector& referenceSpeeds, float* alphaOut) const; AnimPose _modelOffset; // model to rig space diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 1427ce6359..6c265ef1b6 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -393,9 +393,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent if (isFingerPointing) { setAtBit(flags, HAND_STATE_FINGER_POINTING_BIT); } - // faceshift state + // face tracker state if (_headData->_isFaceTrackerConnected) { - setAtBit(flags, IS_FACESHIFT_CONNECTED); + setAtBit(flags, IS_FACE_TRACKER_CONNECTED); } // eye tracker state if (_headData->_isEyeTrackerConnected) { @@ -883,7 +883,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { auto newHandState = getSemiNibbleAt(bitItems, HAND_STATE_START_BIT) + (oneAtBit(bitItems, HAND_STATE_FINGER_POINTING_BIT) ? IS_FINGER_POINTING_FLAG : 0); - auto newFaceTrackerConnected = oneAtBit(bitItems, IS_FACESHIFT_CONNECTED); + auto newFaceTrackerConnected = oneAtBit(bitItems, IS_FACE_TRACKER_CONNECTED); auto newEyeTrackerConnected = oneAtBit(bitItems, IS_EYE_TRACKER_CONNECTED); bool keyStateChanged = (_keyState != newKeyState); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 545a5f1f8c..b2cc912007 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -99,7 +99,7 @@ const quint32 AVATAR_MOTION_SCRIPTABLE_BITS = // Referential Data - R is found in the 7th bit const int KEY_STATE_START_BIT = 0; // 1st and 2nd bits const int HAND_STATE_START_BIT = 2; // 3rd and 4th bits -const int IS_FACESHIFT_CONNECTED = 4; // 5th bit +const int IS_FACE_TRACKER_CONNECTED = 4; // 5th bit const int IS_EYE_TRACKER_CONNECTED = 5; // 6th bit (was CHAT_CIRCLING) const int HAS_REFERENTIAL = 6; // 7th bit const int HAND_STATE_FINGER_POINTING_BIT = 7; // 8th bit @@ -218,7 +218,7 @@ namespace AvatarDataPacket { } PACKED_END; const size_t AVATAR_LOCAL_POSITION_SIZE = 12; - // only present if IS_FACESHIFT_CONNECTED flag is set in AvatarInfo.flags + // only present if IS_FACE_TRACKER_CONNECTED flag is set in AvatarInfo.flags PACKED_BEGIN struct FaceTrackerInfo { float leftEyeBlink; float rightEyeBlink; diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 300fa684d9..62a10c851f 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -53,6 +53,9 @@ namespace controller { makePosePair(Action::RIGHT_HAND, "RightHand"), makePosePair(Action::LEFT_FOOT, "LeftFoot"), makePosePair(Action::RIGHT_FOOT, "RightFoot"), + makePosePair(Action::HIPS, "Hips"), + makePosePair(Action::SPINE2, "Spine2"), + makePosePair(Action::HEAD, "Head"), makeButtonPair(Action::LEFT_HAND_CLICK, "LeftHandClick"), makeButtonPair(Action::RIGHT_HAND_CLICK, "RightHandClick"), diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index edf3dee07a..534f01d865 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -44,6 +44,9 @@ enum class Action { RIGHT_HAND, LEFT_FOOT, RIGHT_FOOT, + HIPS, + SPINE2, + HEAD, LEFT_HAND_CLICK, RIGHT_HAND_CLICK, diff --git a/libraries/controllers/src/controllers/Input.h b/libraries/controllers/src/controllers/Input.h index 9c7f09d526..65c78cd6ea 100644 --- a/libraries/controllers/src/controllers/Input.h +++ b/libraries/controllers/src/controllers/Input.h @@ -16,9 +16,15 @@ namespace controller { struct InputCalibrationData { - glm::mat4 sensorToWorldMat; - glm::mat4 avatarMat; - glm::mat4 hmdSensorMat; + glm::mat4 sensorToWorldMat; // sensor to world + glm::mat4 avatarMat; // avatar to world + glm::mat4 hmdSensorMat; // hmd pos and orientation in sensor space + glm::mat4 defaultCenterEyeMat; // default pose for the center of the eyes in avatar space. + glm::mat4 defaultHeadMat; // default pose for head joint in avatar space + glm::mat4 defaultSpine2; // default pose for spine2 joint in avatar space + glm::mat4 defaultHips; // default pose for hips joint in avatar space + glm::mat4 defaultLeftFoot; // default pose for leftFoot joint in avatar space + glm::mat4 defaultRightFoot; // default pose for leftFoot joint in avatar space }; enum class ChannelType { diff --git a/libraries/controllers/src/controllers/Pose.cpp b/libraries/controllers/src/controllers/Pose.cpp index b86391bbba..fc0891799e 100644 --- a/libraries/controllers/src/controllers/Pose.cpp +++ b/libraries/controllers/src/controllers/Pose.cpp @@ -69,5 +69,23 @@ namespace controller { pose.valid = valid; return pose; } + + Pose Pose::postTransform(const glm::mat4& mat) const { + glm::mat4 original = ::createMatFromQuatAndPos(rotation, translation); + glm::mat4 result = original * mat; + auto translationOut = ::extractTranslation(result); + auto rotationOut = ::glmExtractRotation(result); + auto velocityOut = velocity + glm::cross(angularVelocity, translationOut - translation); // warning: this may be completely wrong + auto angularVelocityOut = angularVelocity; + + Pose pose(translationOut, + rotationOut, + velocityOut, + angularVelocityOut); + + pose.valid = valid; + return pose; + } + } diff --git a/libraries/controllers/src/controllers/Pose.h b/libraries/controllers/src/controllers/Pose.h index 47ba59279a..a6d1360f9f 100644 --- a/libraries/controllers/src/controllers/Pose.h +++ b/libraries/controllers/src/controllers/Pose.h @@ -41,6 +41,7 @@ namespace controller { vec3 getAngularVelocity() const { return angularVelocity; } Pose transform(const glm::mat4& mat) const; + Pose postTransform(const glm::mat4& mat) const; static QScriptValue toScriptValue(QScriptEngine* engine, const Pose& event); static void fromScriptValue(const QScriptValue& object, Pose& event); diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp index cc90ee7b49..d8c98eb63b 100644 --- a/libraries/controllers/src/controllers/StandardController.cpp +++ b/libraries/controllers/src/controllers/StandardController.cpp @@ -104,6 +104,9 @@ Input::NamedVector StandardController::getAvailableInputs() const { makePair(RIGHT_HAND, "RightHand"), makePair(LEFT_FOOT, "LeftFoot"), makePair(RIGHT_FOOT, "RightFoot"), + makePair(HIPS, "Hips"), + makePair(SPINE2, "Spine2"), + makePair(HEAD, "Head"), // Aliases, PlayStation style names makePair(LB, "L1"), diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index 71b052bfe4..570081d1f1 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -20,6 +20,8 @@ #include #include +#include + #include "StandardController.h" #include "StateController.h" #include "InputRecorder.h" @@ -563,7 +565,18 @@ bool UserInputMapper::applyRoute(const Route::Pointer& route, bool force) { if (source->isPose()) { Pose value = getPose(source, route->peek); static const Pose IDENTITY_POSE { vec3(), quat() }; + if (debugRoutes && route->debug) { + qCDebug(controllers) << "Value was t:" << value.translation << "r:" << value.rotation; + } + // Apply each of the filters. + for (const auto& filter : route->filters) { + value = filter->apply(value); + } + + if (debugRoutes && route->debug) { + qCDebug(controllers) << "Filtered value was t:" << value.translation << "r:" << value.rotation; + if (!value.valid) { qCDebug(controllers) << "Applying invalid pose"; } else if (value == IDENTITY_POSE) { diff --git a/libraries/controllers/src/controllers/impl/Filter.cpp b/libraries/controllers/src/controllers/impl/Filter.cpp index 09188318eb..2cb35d85ce 100644 --- a/libraries/controllers/src/controllers/impl/Filter.cpp +++ b/libraries/controllers/src/controllers/impl/Filter.cpp @@ -24,6 +24,10 @@ #include "filters/InvertFilter.h" #include "filters/PulseFilter.h" #include "filters/ScaleFilter.h" +#include "filters/TranslateFilter.h" +#include "filters/TransformFilter.h" +#include "filters/PostTransformFilter.h" +#include "filters/RotateFilter.h" using namespace controller; @@ -37,6 +41,10 @@ REGISTER_FILTER_CLASS_INSTANCE(HysteresisFilter, "hysteresis") REGISTER_FILTER_CLASS_INSTANCE(InvertFilter, "invert") REGISTER_FILTER_CLASS_INSTANCE(ScaleFilter, "scale") REGISTER_FILTER_CLASS_INSTANCE(PulseFilter, "pulse") +REGISTER_FILTER_CLASS_INSTANCE(TranslateFilter, "translate") +REGISTER_FILTER_CLASS_INSTANCE(TransformFilter, "transform") +REGISTER_FILTER_CLASS_INSTANCE(PostTransformFilter, "postTransform") +REGISTER_FILTER_CLASS_INSTANCE(RotateFilter, "rotate") const QString JSON_FILTER_TYPE = QStringLiteral("type"); const QString JSON_FILTER_PARAMS = QStringLiteral("params"); @@ -76,7 +84,6 @@ bool Filter::parseSingleFloatParameter(const QJsonValue& parameters, const QStri return true; } } else if (parameters.isObject()) { - static const QString JSON_MIN = QStringLiteral("interval"); auto objectParameters = parameters.toObject(); if (objectParameters.contains(name)) { output = objectParameters[name].toDouble(); @@ -86,6 +93,92 @@ bool Filter::parseSingleFloatParameter(const QJsonValue& parameters, const QStri return false; } +bool Filter::parseVec3Parameter(const QJsonValue& parameters, glm::vec3& output) { + if (parameters.isDouble()) { + output = glm::vec3(parameters.toDouble()); + return true; + } else if (parameters.isArray()) { + auto arrayParameters = parameters.toArray(); + if (arrayParameters.size() == 3) { + output = glm::vec3(arrayParameters[0].toDouble(), + arrayParameters[1].toDouble(), + arrayParameters[2].toDouble()); + return true; + } + } else if (parameters.isObject()) { + auto objectParameters = parameters.toObject(); + if (objectParameters.contains("x") && objectParameters.contains("y") && objectParameters.contains("z")) { + output = glm::vec3(objectParameters["x"].toDouble(), + objectParameters["y"].toDouble(), + objectParameters["z"].toDouble()); + return true; + } + } + return false; +} + +bool Filter::parseMat4Parameter(const QJsonValue& parameters, glm::mat4& output) { + if (parameters.isObject()) { + auto objectParameters = parameters.toObject(); + + + if (objectParameters.contains("r0c0") && + objectParameters.contains("r1c0") && + objectParameters.contains("r2c0") && + objectParameters.contains("r3c0") && + objectParameters.contains("r0c1") && + objectParameters.contains("r1c1") && + objectParameters.contains("r2c1") && + objectParameters.contains("r3c1") && + objectParameters.contains("r0c2") && + objectParameters.contains("r1c2") && + objectParameters.contains("r2c2") && + objectParameters.contains("r3c2") && + objectParameters.contains("r0c3") && + objectParameters.contains("r1c3") && + objectParameters.contains("r2c3") && + objectParameters.contains("r3c3")) { + + output[0][0] = objectParameters["r0c0"].toDouble(); + output[0][1] = objectParameters["r1c0"].toDouble(); + output[0][2] = objectParameters["r2c0"].toDouble(); + output[0][3] = objectParameters["r3c0"].toDouble(); + output[1][0] = objectParameters["r0c1"].toDouble(); + output[1][1] = objectParameters["r1c1"].toDouble(); + output[1][2] = objectParameters["r2c1"].toDouble(); + output[1][3] = objectParameters["r3c1"].toDouble(); + output[2][0] = objectParameters["r0c2"].toDouble(); + output[2][1] = objectParameters["r1c2"].toDouble(); + output[2][2] = objectParameters["r2c2"].toDouble(); + output[2][3] = objectParameters["r3c2"].toDouble(); + output[3][0] = objectParameters["r0c3"].toDouble(); + output[3][1] = objectParameters["r1c3"].toDouble(); + output[3][2] = objectParameters["r2c3"].toDouble(); + output[3][3] = objectParameters["r3c3"].toDouble(); + + return true; + } + } + return false; +} + +bool Filter::parseQuatParameter(const QJsonValue& parameters, glm::quat& output) { + if (parameters.isObject()) { + auto objectParameters = parameters.toObject(); + if (objectParameters.contains("w") && + objectParameters.contains("x") && + objectParameters.contains("y") && + objectParameters.contains("z")) { + + output = glm::quat(objectParameters["w"].toDouble(), + objectParameters["x"].toDouble(), + objectParameters["y"].toDouble(), + objectParameters["z"].toDouble()); + return true; + } + } + return false; +} #if 0 diff --git a/libraries/controllers/src/controllers/impl/Filter.h b/libraries/controllers/src/controllers/impl/Filter.h index 77585c8ebb..cde8f991b7 100644 --- a/libraries/controllers/src/controllers/impl/Filter.h +++ b/libraries/controllers/src/controllers/impl/Filter.h @@ -21,6 +21,8 @@ #include +#include "../Pose.h" + class QJsonValue; namespace controller { @@ -34,6 +36,8 @@ namespace controller { using Factory = hifi::SimpleFactory; virtual float apply(float value) const = 0; + virtual Pose apply(Pose value) const = 0; + // Factory features virtual bool parseParameters(const QJsonValue& parameters) { return true; } @@ -42,6 +46,9 @@ namespace controller { static Factory& getFactory() { return _factory; } static bool parseSingleFloatParameter(const QJsonValue& parameters, const QString& name, float& output); + static bool parseVec3Parameter(const QJsonValue& parameters, glm::vec3& output); + static bool parseQuatParameter(const QJsonValue& parameters, glm::quat& output); + static bool parseMat4Parameter(const QJsonValue& parameters, glm::mat4& output); protected: static Factory _factory; }; diff --git a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp index 7dedfda3cb..f3c447238a 100644 --- a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp +++ b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp @@ -26,6 +26,10 @@ #include "filters/InvertFilter.h" #include "filters/PulseFilter.h" #include "filters/ScaleFilter.h" +#include "filters/TranslateFilter.h" +#include "filters/TransformFilter.h" +#include "filters/PostTransformFilter.h" +#include "filters/RotateFilter.h" #include "conditionals/AndConditional.h" using namespace controller; @@ -103,6 +107,26 @@ QObject* RouteBuilderProxy::deadZone(float min) { return this; } +QObject* RouteBuilderProxy::translate(glm::vec3 translate) { + addFilter(std::make_shared(translate)); + return this; +} + +QObject* RouteBuilderProxy::transform(glm::mat4 transform) { + addFilter(std::make_shared(transform)); + return this; +} + +QObject* RouteBuilderProxy::postTransform(glm::mat4 transform) { + addFilter(std::make_shared(transform)); + return this; +} + +QObject* RouteBuilderProxy::rotate(glm::quat rotation) { + addFilter(std::make_shared(rotation)); + return this; +} + QObject* RouteBuilderProxy::constrainToInteger() { addFilter(std::make_shared()); return this; diff --git a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h index 1c0ed6931d..de9c23d2cd 100644 --- a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h +++ b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h @@ -48,6 +48,10 @@ class RouteBuilderProxy : public QObject { Q_INVOKABLE QObject* deadZone(float min); Q_INVOKABLE QObject* constrainToInteger(); Q_INVOKABLE QObject* constrainToPositiveInteger(); + Q_INVOKABLE QObject* translate(glm::vec3 translate); + Q_INVOKABLE QObject* transform(glm::mat4 transform); + Q_INVOKABLE QObject* postTransform(glm::mat4 transform); + Q_INVOKABLE QObject* rotate(glm::quat rotation); private: void to(const Endpoint::Pointer& destination); diff --git a/libraries/controllers/src/controllers/impl/filters/ClampFilter.h b/libraries/controllers/src/controllers/impl/filters/ClampFilter.h index fd82821b3e..b06a43515f 100644 --- a/libraries/controllers/src/controllers/impl/filters/ClampFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/ClampFilter.h @@ -21,6 +21,9 @@ public: virtual float apply(float value) const override { return glm::clamp(value, _min, _max); } + + virtual Pose apply(Pose value) const override { return value; } + virtual bool parseParameters(const QJsonValue& parameters) override; protected: float _min = 0.0f; diff --git a/libraries/controllers/src/controllers/impl/filters/ConstrainToIntegerFilter.h b/libraries/controllers/src/controllers/impl/filters/ConstrainToIntegerFilter.h index 580dc2a856..c9a25fde72 100644 --- a/libraries/controllers/src/controllers/impl/filters/ConstrainToIntegerFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/ConstrainToIntegerFilter.h @@ -22,6 +22,9 @@ public: virtual float apply(float value) const override { return glm::sign(value); } + + virtual Pose apply(Pose value) const override { return value; } + protected: }; diff --git a/libraries/controllers/src/controllers/impl/filters/ConstrainToPositiveIntegerFilter.h b/libraries/controllers/src/controllers/impl/filters/ConstrainToPositiveIntegerFilter.h index 27395cde24..e3f4ee8929 100644 --- a/libraries/controllers/src/controllers/impl/filters/ConstrainToPositiveIntegerFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/ConstrainToPositiveIntegerFilter.h @@ -22,6 +22,9 @@ public: virtual float apply(float value) const override { return (value <= 0.0f) ? 0.0f : 1.0f; } + + virtual Pose apply(Pose value) const override { return value; } + protected: }; diff --git a/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.h b/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.h index 70ac657415..d898647126 100644 --- a/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.h @@ -20,6 +20,9 @@ public: DeadZoneFilter(float min = 0.0) : _min(min) {}; virtual float apply(float value) const override; + + virtual Pose apply(Pose value) const override { return value; } + virtual bool parseParameters(const QJsonValue& parameters) override; protected: float _min = 0.0f; diff --git a/libraries/controllers/src/controllers/impl/filters/HysteresisFilter.h b/libraries/controllers/src/controllers/impl/filters/HysteresisFilter.h index 4f7e07928d..4eb563754f 100644 --- a/libraries/controllers/src/controllers/impl/filters/HysteresisFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/HysteresisFilter.h @@ -19,6 +19,9 @@ class HysteresisFilter : public Filter { public: HysteresisFilter(float min = 0.25, float max = 0.75); virtual float apply(float value) const override; + + virtual Pose apply(Pose value) const override { return value; } + virtual bool parseParameters(const QJsonValue& parameters) override; protected: float _min; diff --git a/libraries/controllers/src/controllers/impl/filters/PostTransformFilter.h b/libraries/controllers/src/controllers/impl/filters/PostTransformFilter.h new file mode 100644 index 0000000000..656a146ff2 --- /dev/null +++ b/libraries/controllers/src/controllers/impl/filters/PostTransformFilter.h @@ -0,0 +1,33 @@ +// +// Created by Brad Hefta-Gaub 2017/04/11 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_Controllers_Filters_PostTransform_h +#define hifi_Controllers_Filters_PostTransform_h + +#include + +#include "../Filter.h" + +namespace controller { + +class PostTransformFilter : public Filter { + REGISTER_FILTER_CLASS(PostTransformFilter); +public: + PostTransformFilter() { } + PostTransformFilter(glm::mat4 transform) : _transform(transform) {} + virtual float apply(float value) const override { return value; } + virtual Pose apply(Pose value) const override { return value.postTransform(_transform); } + virtual bool parseParameters(const QJsonValue& parameters) override { return parseMat4Parameter(parameters, _transform); } +private: + glm::mat4 _transform; +}; + +} + +#endif diff --git a/libraries/controllers/src/controllers/impl/filters/PulseFilter.h b/libraries/controllers/src/controllers/impl/filters/PulseFilter.h index 271f4a04f6..a8c7cbf9e6 100644 --- a/libraries/controllers/src/controllers/impl/filters/PulseFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/PulseFilter.h @@ -23,6 +23,8 @@ public: virtual float apply(float value) const override; + virtual Pose apply(Pose value) const override { return value; } + virtual bool parseParameters(const QJsonValue& parameters) override; private: diff --git a/libraries/controllers/src/controllers/impl/filters/RotateFilter.h b/libraries/controllers/src/controllers/impl/filters/RotateFilter.h new file mode 100644 index 0000000000..ee2e081393 --- /dev/null +++ b/libraries/controllers/src/controllers/impl/filters/RotateFilter.h @@ -0,0 +1,39 @@ +// +// Created by Brad Hefta-Gaub 2017/04/11 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_Controllers_Filters_Rotate_h +#define hifi_Controllers_Filters_Rotate_h + +#include + +#include "../Filter.h" + +namespace controller { + +class RotateFilter : public Filter { + REGISTER_FILTER_CLASS(RotateFilter); +public: + RotateFilter() { } + RotateFilter(glm::quat rotation) : _rotation(rotation) {} + + virtual float apply(float value) const override { return value; } + + virtual Pose apply(Pose value) const override { + return value.transform(glm::mat4(glm::quat(_rotation))); + } + + virtual bool parseParameters(const QJsonValue& parameters) override { return parseQuatParameter(parameters, _rotation); } + +private: + glm::quat _rotation; +}; + +} + +#endif diff --git a/libraries/controllers/src/controllers/impl/filters/ScaleFilter.h b/libraries/controllers/src/controllers/impl/filters/ScaleFilter.h index 670da53fe8..7b03e2ce48 100644 --- a/libraries/controllers/src/controllers/impl/filters/ScaleFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/ScaleFilter.h @@ -10,6 +10,8 @@ #ifndef hifi_Controllers_Filters_Scale_h #define hifi_Controllers_Filters_Scale_h +#include + #include "../Filter.h" namespace controller { @@ -23,6 +25,11 @@ public: virtual float apply(float value) const override { return value * _scale; } + + virtual Pose apply(Pose value) const override { + return value.transform(glm::scale(glm::mat4(), glm::vec3(_scale))); + } + virtual bool parseParameters(const QJsonValue& parameters) override; private: diff --git a/libraries/controllers/src/controllers/impl/filters/TransformFilter.h b/libraries/controllers/src/controllers/impl/filters/TransformFilter.h new file mode 100644 index 0000000000..263b70c9b4 --- /dev/null +++ b/libraries/controllers/src/controllers/impl/filters/TransformFilter.h @@ -0,0 +1,35 @@ +// +// Created by Brad Hefta-Gaub 2017/04/11 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_Controllers_Filters_Transform_h +#define hifi_Controllers_Filters_Transform_h + +#include + +#include "../Filter.h" + +namespace controller { + +class TransformFilter : public Filter { + REGISTER_FILTER_CLASS(TransformFilter); +public: + TransformFilter() { } + TransformFilter(glm::mat4 transform) : _transform(transform) {} + + virtual float apply(float value) const override { return value; } + virtual Pose apply(Pose value) const override { return value.transform(_transform); } + virtual bool parseParameters(const QJsonValue& parameters) override { return parseMat4Parameter(parameters, _transform); } + +private: + glm::mat4 _transform; +}; + +} + +#endif diff --git a/libraries/controllers/src/controllers/impl/filters/TranslateFilter.h b/libraries/controllers/src/controllers/impl/filters/TranslateFilter.h new file mode 100644 index 0000000000..eda2912a8a --- /dev/null +++ b/libraries/controllers/src/controllers/impl/filters/TranslateFilter.h @@ -0,0 +1,35 @@ +// +// Created by Brad Hefta-Gaub 2017/04/11 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_Controllers_Filters_Translate_h +#define hifi_Controllers_Filters_Translate_h + +#include + +#include "../Filter.h" + +namespace controller { + +class TranslateFilter : public Filter { + REGISTER_FILTER_CLASS(TranslateFilter); +public: + TranslateFilter() { } + TranslateFilter(glm::vec3 translate) : _translate(translate) {} + + virtual float apply(float value) const override { return value; } + virtual Pose apply(Pose value) const override { return value.transform(glm::translate(_translate)); } + virtual bool parseParameters(const QJsonValue& parameters) override { return parseVec3Parameter(parameters, _translate); } + +private: + glm::vec3 _translate { 0.0f }; +}; + +} + +#endif diff --git a/libraries/entities/src/EntityDynamicInterface.cpp b/libraries/entities/src/EntityDynamicInterface.cpp index bed3185b8f..2ab9a60397 100644 --- a/libraries/entities/src/EntityDynamicInterface.cpp +++ b/libraries/entities/src/EntityDynamicInterface.cpp @@ -49,20 +49,20 @@ -An dynamic is a callback which is registered with bullet. An dynamic is called-back every physics +A dynamic is a callback which is registered with bullet. A dynamic is called-back every physics simulation step and can do whatever it wants with the various datastructures it has available. An dynamic, for example, can pull an EntityItem toward a point as if that EntityItem were connected to that point by a spring. -In this system, an dynamic is a property of an EntityItem (rather, an EntityItem has a property which +In this system, a dynamic is a property of an EntityItem (rather, an EntityItem has a property which encodes a list of dynamics). Each dynamic has a type and some arguments. Dynamics can be created by a script or when receiving information via an EntityTree data-stream (either over the network or from an svo file). In the interface, if an EntityItem has dynamics, this EntityItem will have pointers to ObjectDynamic -subclass (like ObjectDynamicSpring) instantiations. Code in the entities library affects an dynamic-object +subclass (like ObjectDynamicSpring) instantiations. Code in the entities library affects a dynamic-object via the EntityDynamicInterface (which knows nothing about bullet). When the ObjectDynamic subclass -instance is created, it is registered as an dynamic with bullet. Bullet will call into code in this +instance is created, it is registered as a dynamic with bullet. Bullet will call into code in this instance with the btDynamicInterface every physics-simulation step. Because the dynamic can exist next to the interface's EntityTree or the entity-server's EntityTree, diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 55704236e3..be3bfcc0e9 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -50,7 +50,8 @@ Q_LOGGING_CATEGORY(trace_resource_parse_image_ktx, "trace.resource.parse.image.k const std::string TextureCache::KTX_DIRNAME { "ktx_cache" }; const std::string TextureCache::KTX_EXT { "ktx" }; -static const int SKYBOX_LOAD_PRIORITY { 10 }; // Make sure skybox loads first +static const float SKYBOX_LOAD_PRIORITY { 10.0f }; // Make sure skybox loads first +static const float HIGH_MIPS_LOAD_PRIORITY { 9.0f }; // Make sure high mips loads after skybox but before models TextureCache::TextureCache() : _ktxCache(KTX_DIRNAME, KTX_EXT) { @@ -261,9 +262,6 @@ QSharedPointer TextureCache::createResource(const QUrl& url, const QSh auto content = textureExtra ? textureExtra->content : QByteArray(); auto maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS; NetworkTexture* texture = new NetworkTexture(url, type, content, maxNumPixels); - if (type == image::TextureUsage::CUBE_TEXTURE) { - texture->setLoadPriority(this, SKYBOX_LOAD_PRIORITY); - } return QSharedPointer(texture, &Resource::deleter); } @@ -276,6 +274,12 @@ NetworkTexture::NetworkTexture(const QUrl& url, image::TextureUsage::Type type, _textureSource = std::make_shared(); _lowestRequestedMipLevel = 0; + if (type == image::TextureUsage::CUBE_TEXTURE) { + setLoadPriority(this, SKYBOX_LOAD_PRIORITY); + } else if (_sourceIsKTX) { + setLoadPriority(this, HIGH_MIPS_LOAD_PRIORITY); + } + if (!url.isValid()) { _loaded = true; } @@ -397,7 +401,8 @@ void NetworkTexture::startRequestForNextMipLevel() { _ktxResourceState = PENDING_MIP_REQUEST; init(); - setLoadPriority(this, -static_cast(_originalKtxDescriptor->header.numberOfMipmapLevels) + _lowestKnownPopulatedMip); + float priority = -(float)_originalKtxDescriptor->header.numberOfMipmapLevels + (float)_lowestKnownPopulatedMip; + setLoadPriority(this, priority); _url.setFragment(QString::number(_lowestKnownPopulatedMip - 1)); TextureCache::attemptRequest(_self); } diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 15e0b8c9b5..054557e920 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -348,18 +348,19 @@ void AssetClient::handleAssetGetReply(QSharedPointer message, S // Store message in case we need to disconnect from it later. callbacks.message = message; + + auto weakNode = senderNode.toWeakRef(); + connect(message.data(), &ReceivedMessage::progress, this, [this, weakNode, messageID, length](qint64 size) { + handleProgressCallback(weakNode, messageID, size, length); + }); + connect(message.data(), &ReceivedMessage::completed, this, [this, weakNode, messageID]() { + handleCompleteCallback(weakNode, messageID); + }); + if (message->isComplete()) { + disconnect(message.data(), nullptr, this, nullptr); callbacks.completeCallback(true, error, message->readAll()); messageCallbackMap.erase(requestIt); - } else { - auto weakNode = senderNode.toWeakRef(); - - connect(message.data(), &ReceivedMessage::progress, this, [this, weakNode, messageID, length](qint64 size) { - handleProgressCallback(weakNode, messageID, size, length); - }); - connect(message.data(), &ReceivedMessage::completed, this, [this, weakNode, messageID]() { - handleCompleteCallback(weakNode, messageID); - }); } } diff --git a/libraries/networking/src/AssetRequest.cpp b/libraries/networking/src/AssetRequest.cpp index 341c3b45da..920c7ae036 100644 --- a/libraries/networking/src/AssetRequest.cpp +++ b/libraries/networking/src/AssetRequest.cpp @@ -77,7 +77,7 @@ void AssetRequest::start() { _assetRequestID = assetClient->getAsset(_hash, _byteRange.fromInclusive, _byteRange.toExclusive, [this, that, hash](bool responseReceived, AssetServerError serverError, const QByteArray& data) { - if (!that) { + if (!that) { qCWarning(asset_client) << "Got reply for dead asset request " << hash << "- error code" << _error; // If the request is dead, return return; @@ -113,8 +113,10 @@ void AssetRequest::start() { _data = data; _totalReceived += data.size(); emit progress(_totalReceived, data.size()); - - saveToCache(getUrl(), data); + + if (!_byteRange.isSet()) { + saveToCache(getUrl(), data); + } } } diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index bd30cdd29c..adaa7a848c 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -39,7 +39,7 @@ const QSet NON_SOURCED_PACKETS = QSet() << PacketType::ICEServerPeerInformation << PacketType::ICEServerQuery << PacketType::ICEServerHeartbeat << PacketType::ICEServerHeartbeatACK << PacketType::ICEPing << PacketType::ICEPingReply << PacketType::ICEServerHeartbeatDenied << PacketType::AssignmentClientStatus << PacketType::StopNode - << PacketType::DomainServerRemovedNode << PacketType::UsernameFromIDReply; + << PacketType::DomainServerRemovedNode << PacketType::UsernameFromIDReply << PacketType::OctreeFileReplacement; PacketVersion versionForPacketType(PacketType packetType) { switch (packetType) { diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index d312427ca7..f803b83887 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -113,7 +113,8 @@ public: EntityPhysics, EntityServerScriptLog, AdjustAvatarSorting, - LAST_PACKET_TYPE = AdjustAvatarSorting + OctreeFileReplacement, + LAST_PACKET_TYPE = OctreeFileReplacement }; }; diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index 7034790eaf..ea6bd28fc4 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -33,6 +33,7 @@ #include "OctreePersistThread.h" const int OctreePersistThread::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // every 30 seconds +const QString OctreePersistThread::REPLACEMENT_FILE_EXTENSION = ".replace"; OctreePersistThread::OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory, int persistInterval, bool wantBackup, const QJsonObject& settings, bool debugTimestampNow, @@ -131,10 +132,47 @@ quint64 OctreePersistThread::getMostRecentBackupTimeInUsecs(const QString& forma return mostRecentBackupInUsecs; } +void OctreePersistThread::possiblyReplaceContent() { + // before we load the normal file, check if there's a pending replacement file + auto replacementFileName = _filename + REPLACEMENT_FILE_EXTENSION; + + QFile replacementFile { replacementFileName }; + if (replacementFile.exists()) { + // we have a replacement file to process + qDebug() << "Replacing models file with" << replacementFileName; + + // first take the current models file and move it to a different filename, appended with the timestamp + QFile currentFile { _filename }; + if (currentFile.exists()) { + static const QString FILENAME_TIMESTAMP_FORMAT = "yyyyMMdd-hhmmss"; + auto backupFileName = _filename + ".backup." + QDateTime::currentDateTime().toString(FILENAME_TIMESTAMP_FORMAT); + + if (currentFile.rename(backupFileName)) { + qDebug() << "Moved previous models file to" << backupFileName; + } else { + qWarning() << "Could not backup previous models file to" << backupFileName << "- removing replacement models file"; + + if (!replacementFile.remove()) { + qWarning() << "Could not remove replacement models file from" << replacementFileName + << "- replacement will be re-attempted on next server restart"; + return; + } + } + } + + // rename the replacement file to match what the persist thread is just about to read + if (!replacementFile.rename(_filename)) { + qWarning() << "Could not replace models file with" << replacementFileName << "- starting with empty models file"; + } + } +} + bool OctreePersistThread::process() { if (!_initialLoadComplete) { + possiblyReplaceContent(); + quint64 loadStarted = usecTimestampNow(); qCDebug(octree) << "loading Octrees from file: " << _filename << "..."; diff --git a/libraries/octree/src/OctreePersistThread.h b/libraries/octree/src/OctreePersistThread.h index 927304e862..2441223467 100644 --- a/libraries/octree/src/OctreePersistThread.h +++ b/libraries/octree/src/OctreePersistThread.h @@ -32,6 +32,7 @@ public: }; static const int DEFAULT_PERSIST_INTERVAL; + static const QString REPLACEMENT_FILE_EXTENSION; OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory, int persistInterval = DEFAULT_PERSIST_INTERVAL, bool wantBackup = false, @@ -60,6 +61,7 @@ protected: bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); quint64 getMostRecentBackupTimeInUsecs(const QString& format); void parseSettings(const QJsonObject& settings); + void possiblyReplaceContent(); private: OctreePointer _tree; diff --git a/libraries/recording/src/recording/Deck.cpp b/libraries/recording/src/recording/Deck.cpp index 186516e01c..c9ac0524ad 100644 --- a/libraries/recording/src/recording/Deck.cpp +++ b/libraries/recording/src/recording/Deck.cpp @@ -166,6 +166,12 @@ void Deck::processFrames() { if (!overLimit) { auto nextFrameTime = nextClip->positionFrameTime(); nextInterval = (int)Frame::frameTimeToMilliseconds(nextFrameTime - _position); + if (nextInterval < 0) { + qCWarning(recordingLog) << "Unexpected nextInterval < 0 nextFrameTime:" << nextFrameTime + << "_position:" << _position << "-- setting nextInterval to 0"; + nextInterval = 0; + } + #ifdef WANT_RECORDING_DEBUG qCDebug(recordingLog) << "Now " << _position; qCDebug(recordingLog) << "Next frame time " << nextInterval; diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index 6d49ed27c1..05002dcf5d 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -122,3 +122,10 @@ bool Quat::equal(const glm::quat& q1, const glm::quat& q2) { return q1 == q2; } +glm::quat Quat::cancelOutRollAndPitch(const glm::quat& q) { + return ::cancelOutRollAndPitch(q); +} + +glm::quat Quat::cancelOutRoll(const glm::quat& q) { + return ::cancelOutRoll(q); +} diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index 8a88767a41..ee3ab9aa7c 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -60,6 +60,8 @@ public slots: float dot(const glm::quat& q1, const glm::quat& q2); void print(const QString& label, const glm::quat& q); bool equal(const glm::quat& q1, const glm::quat& q2); + glm::quat cancelOutRollAndPitch(const glm::quat& q); + glm::quat cancelOutRoll(const glm::quat& q); }; #endif // hifi_Quat_h diff --git a/libraries/script-engine/src/TabletScriptingInterface.cpp b/libraries/script-engine/src/TabletScriptingInterface.cpp index bffe318c11..d4eeecc82e 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.cpp +++ b/libraries/script-engine/src/TabletScriptingInterface.cpp @@ -508,7 +508,7 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS if (root) { removeButtonsFromHomeScreen(); - QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, QVariant(WEB_VIEW_SOURCE_URL))); + QMetaObject::invokeMethod(root, "loadWebBase"); QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); QMetaObject::invokeMethod(root, "loadWebUrl", Q_ARG(const QVariant&, QVariant(url)), Q_ARG(const QVariant&, QVariant(injectedJavaScriptUrl))); } diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index ec244553f8..db42fef8bc 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -38,6 +38,11 @@ const quat Quaternions::X_180{ 0.0f, 1.0f, 0.0f, 0.0f }; const quat Quaternions::Y_180{ 0.0f, 0.0f, 1.0f, 0.0f }; const quat Quaternions::Z_180{ 0.0f, 0.0f, 0.0f, 1.0f }; +const mat4 Matrices::IDENTITY { glm::mat4() }; +const mat4 Matrices::X_180 { createMatFromQuatAndPos(Quaternions::X_180, Vectors::ZERO) }; +const mat4 Matrices::Y_180 { createMatFromQuatAndPos(Quaternions::Y_180, Vectors::ZERO) }; +const mat4 Matrices::Z_180 { createMatFromQuatAndPos(Quaternions::Z_180, Vectors::ZERO) }; + // Safe version of glm::mix; based on the code in Nick Bobick's article, // http://www.gamasutra.com/features/19980703/quaternions_01.htm (via Clyde, // https://github.com/threerings/clyde/blob/master/src/main/java/com/threerings/math/Quaternion.java) diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index deb87930fc..fd75fa416c 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -54,6 +54,13 @@ const glm::vec3 IDENTITY_FORWARD = glm::vec3( 0.0f, 0.0f,-1.0f); glm::quat safeMix(const glm::quat& q1, const glm::quat& q2, float alpha); +class Matrices { +public: + static const mat4 IDENTITY; + static const mat4 X_180; + static const mat4 Y_180; + static const mat4 Z_180; +}; class Quaternions { public: diff --git a/libraries/trackers/CMakeLists.txt b/libraries/trackers/CMakeLists.txt new file mode 100644 index 0000000000..0999a45b59 --- /dev/null +++ b/libraries/trackers/CMakeLists.txt @@ -0,0 +1,6 @@ +set(TARGET_NAME trackers) +setup_hifi_library() +GroupSources("src") +link_hifi_libraries(shared) + +target_bullet() diff --git a/interface/src/devices/DeviceTracker.cpp b/libraries/trackers/src/trackers/DeviceTracker.cpp similarity index 98% rename from interface/src/devices/DeviceTracker.cpp rename to libraries/trackers/src/trackers/DeviceTracker.cpp index 2cd4950064..93aeb607bc 100644 --- a/interface/src/devices/DeviceTracker.cpp +++ b/libraries/trackers/src/trackers/DeviceTracker.cpp @@ -1,7 +1,4 @@ // -// DeviceTracker.cpp -// interface/src/devices -// // Created by Sam Cake on 6/20/14. // Copyright 2014 High Fidelity, Inc. // diff --git a/interface/src/devices/DeviceTracker.h b/libraries/trackers/src/trackers/DeviceTracker.h similarity index 98% rename from interface/src/devices/DeviceTracker.h rename to libraries/trackers/src/trackers/DeviceTracker.h index 543e9bab1a..8a7f509cb3 100644 --- a/interface/src/devices/DeviceTracker.h +++ b/libraries/trackers/src/trackers/DeviceTracker.h @@ -1,7 +1,4 @@ // -// DeviceTracker.h -// interface/src/devices -// // Created by Sam Cake on 6/20/14. // Copyright 2014 High Fidelity, Inc. // diff --git a/interface/src/devices/EyeTracker.cpp b/libraries/trackers/src/trackers/EyeTracker.cpp similarity index 98% rename from interface/src/devices/EyeTracker.cpp rename to libraries/trackers/src/trackers/EyeTracker.cpp index 367aa52aae..8733461dbb 100644 --- a/interface/src/devices/EyeTracker.cpp +++ b/libraries/trackers/src/trackers/EyeTracker.cpp @@ -1,7 +1,4 @@ // -// EyeTracker.cpp -// interface/src/devices -// // Created by David Rowe on 27 Jul 2015. // Copyright 2015 High Fidelity, Inc. // @@ -17,8 +14,8 @@ #include -#include "InterfaceLogging.h" -#include "OctreeConstants.h" +#include "Logging.h" +#include #ifdef HAVE_IVIEWHMD char* HIGH_FIDELITY_EYE_TRACKER_CALIBRATION = "HighFidelityEyeTrackerCalibration"; @@ -115,7 +112,7 @@ void EyeTracker::processData(smi_CallbackDataStruct* data) { void EyeTracker::init() { if (_isInitialized) { - qCWarning(interfaceapp) << "Eye Tracker: Already initialized"; + qCWarning(trackers) << "Eye Tracker: Already initialized"; return; } } diff --git a/interface/src/devices/EyeTracker.h b/libraries/trackers/src/trackers/EyeTracker.h similarity index 97% rename from interface/src/devices/EyeTracker.h rename to libraries/trackers/src/trackers/EyeTracker.h index 0e760d9454..9cf35d0f2a 100644 --- a/interface/src/devices/EyeTracker.h +++ b/libraries/trackers/src/trackers/EyeTracker.h @@ -1,7 +1,4 @@ // -// EyeTracker.h -// interface/src/devices -// // Created by David Rowe on 27 Jul 2015. // Copyright 2015 High Fidelity, Inc. // diff --git a/interface/src/devices/FaceTracker.cpp b/libraries/trackers/src/trackers/FaceTracker.cpp similarity index 73% rename from interface/src/devices/FaceTracker.cpp rename to libraries/trackers/src/trackers/FaceTracker.cpp index 76a4534952..034787f19a 100644 --- a/interface/src/devices/FaceTracker.cpp +++ b/libraries/trackers/src/trackers/FaceTracker.cpp @@ -1,7 +1,4 @@ // -// FaceTracker.cpp -// interface/src/devices -// // Created by Andrzej Kapolka on 4/9/14. // Copyright 2014 High Fidelity, Inc. // @@ -9,22 +6,21 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include - -#include - #include "FaceTracker.h" -#include "InterfaceLogging.h" -#include "Menu.h" + +#include +#include +#include "Logging.h" +//#include "Menu.h" const int FPS_TIMER_DELAY = 2000; // ms const int FPS_TIMER_DURATION = 2000; // ms const float DEFAULT_EYE_DEFLECTION = 0.25f; Setting::Handle FaceTracker::_eyeDeflection("faceshiftEyeDeflection", DEFAULT_EYE_DEFLECTION); +bool FaceTracker::_isMuted { true }; void FaceTracker::init() { - _isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking); _isInitialized = true; // FaceTracker can be used now } @@ -101,7 +97,7 @@ void FaceTracker::countFrame() { } void FaceTracker::finishFPSTimer() { - qCDebug(interfaceapp) << "Face tracker FPS =" << (float)_frameCount / ((float)FPS_TIMER_DURATION / 1000.0f); + qCDebug(trackers) << "Face tracker FPS =" << (float)_frameCount / ((float)FPS_TIMER_DURATION / 1000.0f); _isCalculatingFPS = false; } @@ -113,3 +109,25 @@ void FaceTracker::toggleMute() { void FaceTracker::setEyeDeflection(float eyeDeflection) { _eyeDeflection.set(eyeDeflection); } + +void FaceTracker::updateFakeCoefficients(float leftBlink, float rightBlink, float browUp, + float jawOpen, float mouth2, float mouth3, float mouth4, QVector& coefficients) { + const int MMMM_BLENDSHAPE = 34; + const int FUNNEL_BLENDSHAPE = 40; + const int SMILE_LEFT_BLENDSHAPE = 28; + const int SMILE_RIGHT_BLENDSHAPE = 29; + const int MAX_FAKE_BLENDSHAPE = 40; // Largest modified blendshape from above and below + + coefficients.resize(std::max((int)coefficients.size(), MAX_FAKE_BLENDSHAPE + 1)); + qFill(coefficients.begin(), coefficients.end(), 0.0f); + coefficients[_leftBlinkIndex] = leftBlink; + coefficients[_rightBlinkIndex] = rightBlink; + coefficients[_browUpCenterIndex] = browUp; + coefficients[_browUpLeftIndex] = browUp; + coefficients[_browUpRightIndex] = browUp; + coefficients[_jawOpenIndex] = jawOpen; + coefficients[SMILE_LEFT_BLENDSHAPE] = coefficients[SMILE_RIGHT_BLENDSHAPE] = mouth4; + coefficients[MMMM_BLENDSHAPE] = mouth2; + coefficients[FUNNEL_BLENDSHAPE] = mouth3; +} + diff --git a/interface/src/devices/FaceTracker.h b/libraries/trackers/src/trackers/FaceTracker.h similarity index 64% rename from interface/src/devices/FaceTracker.h rename to libraries/trackers/src/trackers/FaceTracker.h index 7126d19ca8..58d5c5e574 100644 --- a/interface/src/devices/FaceTracker.h +++ b/libraries/trackers/src/trackers/FaceTracker.h @@ -1,7 +1,4 @@ // -// FaceTracker.h -// interface/src/devices -// // Created by Andrzej Kapolka on 4/9/14. // Copyright 2014 High Fidelity, Inc. // @@ -20,7 +17,7 @@ #include -/// Base class for face trackers (Faceshift, DDE). +/// Base class for face trackers (DDE, BinaryVR). class FaceTracker : public QObject { Q_OBJECT @@ -45,12 +42,21 @@ public: const QVector& getBlendshapeCoefficients() const; float getBlendshapeCoefficient(int index) const; - bool isMuted() const { return _isMuted; } - void setIsMuted(bool isMuted) { _isMuted = isMuted; } + static bool isMuted() { return _isMuted; } + static void setIsMuted(bool isMuted) { _isMuted = isMuted; } static float getEyeDeflection() { return _eyeDeflection.get(); } static void setEyeDeflection(float eyeDeflection); + static void updateFakeCoefficients(float leftBlink, + float rightBlink, + float browUp, + float jawOpen, + float mouth2, + float mouth3, + float mouth4, + QVector& coefficients); + signals: void muteToggled(); @@ -63,7 +69,7 @@ protected: virtual ~FaceTracker() {}; bool _isInitialized = false; - bool _isMuted = true; + static bool _isMuted; glm::vec3 _headTranslation = glm::vec3(0.0f); glm::quat _headRotation = glm::quat(); @@ -84,6 +90,24 @@ private: bool _isCalculatingFPS = false; int _frameCount = 0; + // see http://support.faceshift.com/support/articles/35129-export-of-blendshapes + static const int _leftBlinkIndex = 0; + static const int _rightBlinkIndex = 1; + static const int _leftEyeOpenIndex = 8; + static const int _rightEyeOpenIndex = 9; + + // Brows + static const int _browDownLeftIndex = 14; + static const int _browDownRightIndex = 15; + static const int _browUpCenterIndex = 16; + static const int _browUpLeftIndex = 17; + static const int _browUpRightIndex = 18; + + static const int _mouthSmileLeftIndex = 28; + static const int _mouthSmileRightIndex = 29; + + static const int _jawOpenIndex = 21; + static Setting::Handle _eyeDeflection; }; diff --git a/libraries/trackers/src/trackers/Logging.cpp b/libraries/trackers/src/trackers/Logging.cpp new file mode 100644 index 0000000000..a4dcf1b711 --- /dev/null +++ b/libraries/trackers/src/trackers/Logging.cpp @@ -0,0 +1,11 @@ +// +// Created by Bradley Austin Davis on 2017/04/25 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Logging.h" + +Q_LOGGING_CATEGORY(trackers, "hifi.trackers") diff --git a/libraries/trackers/src/trackers/Logging.h b/libraries/trackers/src/trackers/Logging.h new file mode 100644 index 0000000000..554429b61d --- /dev/null +++ b/libraries/trackers/src/trackers/Logging.h @@ -0,0 +1,16 @@ +// +// Created by Bradley Austin Davis on 2017/04/25 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_TrackersLogging_h +#define hifi_TrackersLogging_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(trackers) + +#endif // hifi_TrackersLogging_h diff --git a/interface/src/devices/MotionTracker.cpp b/libraries/trackers/src/trackers/MotionTracker.cpp similarity index 98% rename from interface/src/devices/MotionTracker.cpp rename to libraries/trackers/src/trackers/MotionTracker.cpp index 234a8d0c0c..c6012c0422 100644 --- a/interface/src/devices/MotionTracker.cpp +++ b/libraries/trackers/src/trackers/MotionTracker.cpp @@ -1,7 +1,4 @@ // -// MotionTracker.cpp -// interface/src/devices -// // Created by Sam Cake on 6/20/14. // Copyright 2014 High Fidelity, Inc. // @@ -10,8 +7,6 @@ // #include "MotionTracker.h" -#include "GLMHelpers.h" - // glm::mult(mat43, mat43) just the composition of the 2 matrices assuming they are in fact mat44 with the last raw = { 0, 0, 0, 1 } namespace glm { diff --git a/interface/src/devices/MotionTracker.h b/libraries/trackers/src/trackers/MotionTracker.h similarity index 93% rename from interface/src/devices/MotionTracker.h rename to libraries/trackers/src/trackers/MotionTracker.h index a4b5e6735e..26c8dcea2c 100644 --- a/interface/src/devices/MotionTracker.h +++ b/libraries/trackers/src/trackers/MotionTracker.h @@ -1,7 +1,4 @@ // -// MotionTracker.h -// interface/src/devices -// // Created by Sam Cake on 6/20/14. // Copyright 2014 High Fidelity, Inc. // @@ -14,20 +11,7 @@ #include "DeviceTracker.h" -#ifdef __GNUC__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wsign-compare" -#endif - -#include - -#ifdef __GNUC__ -#pragma GCC diagnostic pop -#endif - - -#include -#include +#include /// Base class for device trackers. class MotionTracker : public DeviceTracker { diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index 50833e90fc..7511448c38 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -223,6 +223,18 @@ QAction* Menu::addCheckableActionToQMenuAndActionHash(MenuWrapper* destinationMe return action; } +QAction* Menu::addCheckableActionToQMenuAndActionHash(MenuWrapper* destinationMenu, + const QString& actionName, + const std::function& handler, + const QKeySequence& shortcut, + const bool checked, + int menuItemLocation, + const QString& grouping) { + auto action = addCheckableActionToQMenuAndActionHash(destinationMenu, actionName, shortcut, checked, nullptr, nullptr, menuItemLocation, grouping); + connect(action, &QAction::triggered, handler); + return action; +} + void Menu::removeAction(MenuWrapper* menu, const QString& actionName) { auto action = _actionHash.value(actionName); menu->removeAction(action); diff --git a/libraries/ui/src/ui/Menu.h b/libraries/ui/src/ui/Menu.h index 9839bd1eb6..25f8f74063 100644 --- a/libraries/ui/src/ui/Menu.h +++ b/libraries/ui/src/ui/Menu.h @@ -9,6 +9,8 @@ #ifndef hifi_ui_Menu_h #define hifi_ui_Menu_h +#include + #include #include #include @@ -90,6 +92,14 @@ public: int menuItemLocation = UNSPECIFIED_POSITION, const QString& grouping = QString()); + QAction* addCheckableActionToQMenuAndActionHash(MenuWrapper* destinationMenu, + const QString& actionName, + const std::function& handler, + const QKeySequence& shortcut = 0, + const bool checked = false, + int menuItemLocation = UNSPECIFIED_POSITION, + const QString& grouping = QString()); + void removeAction(MenuWrapper* menu, const QString& actionName); public slots: diff --git a/script-archive/controllers/puppetFeet3.js b/script-archive/controllers/puppetFeet3.js new file mode 100644 index 0000000000..3c9618edd9 --- /dev/null +++ b/script-archive/controllers/puppetFeet3.js @@ -0,0 +1,34 @@ +// +// puppetFeet3.js +// examples/controllers +// +// Created by Brad Hefta-Gaub on 2017/04/11 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +var MAPPING_NAME = "com.highfidelity.examples.puppetFeet3"; +var mapping = Controller.newMapping(MAPPING_NAME); +var puppetOffset = { x: 0, y: -1, z: 0 }; + +var rotation = Quat.fromPitchYawRollDegrees(0, 0, -90); +var noTranslation = { x: 0, y: 0, z: 0 }; +var transformMatrix = Mat4.createFromRotAndTrans(rotation, noTranslation); +var rotateAndTranslate = Mat4.createFromRotAndTrans(rotation, puppetOffset); + + +mapping.from(Controller.Standard.LeftHand).peek().rotate(rotation).translate(puppetOffset).to(Controller.Standard.LeftFoot); + +//mapping.from(Controller.Standard.LeftHand).peek().translate(puppetOffset).to(Controller.Standard.LeftFoot); +//mapping.from(Controller.Standard.LeftHand).peek().transform(transformMatrix).translate(puppetOffset).to(Controller.Standard.LeftFoot); +//mapping.from(Controller.Standard.LeftHand).peek().transform(rotateAndTranslate).to(Controller.Standard.LeftFoot); + +Controller.enableMapping(MAPPING_NAME); + + +Script.scriptEnding.connect(function(){ + mapping.disable(); +}); diff --git a/scripts/developer/tests/hipsControllerTest.js b/scripts/developer/tests/hipsControllerTest.js new file mode 100644 index 0000000000..5c6a4811e5 --- /dev/null +++ b/scripts/developer/tests/hipsControllerTest.js @@ -0,0 +1,105 @@ +// +// hipsControllerTest.js +// +// Created by Anthony Thibault on 4/24/17 +// Copyright 2017 High Fidelity, Inc. +// +// Test procedural manipulation of the Avatar hips via the controller system. +// Pull the left and right triggers on your hand controllers, you hips should begin to gyrate in an amusing mannor. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Xform */ +Script.include("/~/system/libraries/Xform.js"); + +var triggerPressHandled = false; +var rightTriggerPressed = false; +var leftTriggerPressed = false; + +var MAPPING_NAME = "com.highfidelity.hipsIkTest"; + +var mapping = Controller.newMapping(MAPPING_NAME); +mapping.from([Controller.Standard.RTClick]).peek().to(function (value) { + rightTriggerPressed = (value !== 0) ? true : false; +}); +mapping.from([Controller.Standard.LTClick]).peek().to(function (value) { + leftTriggerPressed = (value !== 0) ? true : false; +}); + +Controller.enableMapping(MAPPING_NAME); + +var CONTROLLER_MAPPING_NAME = "com.highfidelity.hipsIkTest.controller"; +var controllerMapping; + +var ZERO = {x: 0, y: 0, z: 0}; +var X_AXIS = {x: 1, y: 0, z: 0}; +var Y_AXIS = {x: 0, y: 1, z: 0}; +var Y_180 = {x: 0, y: 1, z: 0, w: 0}; +var Y_180_XFORM = new Xform(Y_180, {x: 0, y: 0, z: 0}); + +var hips = undefined; + +function computeCurrentXform(jointIndex) { + var currentXform = new Xform(MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex), + MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex)); + return currentXform; +} + +function calibrate() { + hips = computeCurrentXform(MyAvatar.getJointIndex("Hips")); +} + +function circleOffset(radius, theta, normal) { + var pos = {x: radius * Math.cos(theta), y: radius * Math.sin(theta), z: 0}; + var lookAtRot = Quat.lookAt(normal, ZERO, X_AXIS); + return Vec3.multiplyQbyV(lookAtRot, pos); +} + +var calibrationCount = 0; + +function update(dt) { + if (rightTriggerPressed && leftTriggerPressed) { + if (!triggerPressHandled) { + triggerPressHandled = true; + if (controllerMapping) { + hips = undefined; + Controller.disableMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + controllerMapping = undefined; + } else { + calibrate(); + calibrationCount++; + controllerMapping = Controller.newMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + + var n = Y_AXIS; + var t = 0; + if (hips) { + controllerMapping.from(function () { + t += (1 / 60) * 4; + return { + valid: true, + translation: Vec3.sum(hips.pos, circleOffset(0.1, t, n)), + rotation: hips.rot, + velocity: ZERO, + angularVelocity: ZERO + }; + }).to(Controller.Standard.Hips); + } + Controller.enableMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + } + } + } else { + triggerPressHandled = false; + } +} + +Script.update.connect(update); + +Script.scriptEnding.connect(function () { + Controller.disableMapping(MAPPING_NAME); + if (controllerMapping) { + Controller.disableMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + } + Script.update.disconnect(update); +}); + diff --git a/scripts/developer/tests/hipsIkTest.js b/scripts/developer/tests/hipsIkTest.js new file mode 100644 index 0000000000..340d1ae7a0 --- /dev/null +++ b/scripts/developer/tests/hipsIkTest.js @@ -0,0 +1,118 @@ +// +// hipsIKTest.js +// +// Created by Anthony Thibault on 4/24/17 +// Copyright 2017 High Fidelity, Inc. +// +// Test procedural manipulation of the Avatar hips IK target. +// Pull the left and right triggers on your hand controllers, you hips should begin to gyrate in an amusing mannor. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Xform */ +Script.include("/~/system/libraries/Xform.js"); + +var calibrated = false; +var rightTriggerPressed = false; +var leftTriggerPressed = false; + +var MAPPING_NAME = "com.highfidelity.hipsIkTest"; + +var mapping = Controller.newMapping(MAPPING_NAME); +mapping.from([Controller.Standard.RTClick]).peek().to(function (value) { + rightTriggerPressed = (value !== 0) ? true : false; +}); +mapping.from([Controller.Standard.LTClick]).peek().to(function (value) { + leftTriggerPressed = (value !== 0) ? true : false; +}); + +Controller.enableMapping(MAPPING_NAME); + +var ANIM_VARS = [ + "headType", + "hipsType", + "hipsPosition", + "hipsRotation" +]; + +var ZERO = {x: 0, y: 0, z: 0}; +var X_AXIS = {x: 1, y: 0, z: 0}; +var Y_AXIS = {x: 0, y: 1, z: 0}; +var Y_180 = {x: 0, y: 1, z: 0, w: 0}; +var Y_180_XFORM = new Xform(Y_180, {x: 0, y: 0, z: 0}); + +var hips = undefined; + +function computeCurrentXform(jointIndex) { + var currentXform = new Xform(MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex), + MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex)); + return Xform.mul(Y_180_XFORM, currentXform); +} + +function calibrate() { + hips = computeCurrentXform(MyAvatar.getJointIndex("Hips")); +} + +var ikTypes = { + RotationAndPosition: 0, + RotationOnly: 1, + HmdHead: 2, + HipsRelativeRotationAndPosition: 3, + Off: 4 +}; + +function circleOffset(radius, theta, normal) { + var pos = {x: radius * Math.cos(theta), y: radius * Math.sin(theta), z: 0}; + var lookAtRot = Quat.lookAt(normal, ZERO, X_AXIS); + return Vec3.multiplyQbyV(lookAtRot, pos); +} + +var handlerId; + +function update(dt) { + if (rightTriggerPressed && leftTriggerPressed) { + if (!calibrated) { + calibrate(); + calibrated = true; + + if (handlerId) { + MyAvatar.removeAnimationStateHandler(handlerId); + handlerId = undefined; + } else { + + var n = Y_AXIS; + var t = 0; + handlerId = MyAvatar.addAnimationStateHandler(function (props) { + + t += (1 / 60) * 4; + var result = {}, xform; + if (hips) { + xform = hips; + result.hipsType = ikTypes.RotationAndPosition; + result.hipsPosition = Vec3.sum(xform.pos, circleOffset(0.1, t, n)); + result.hipsRotation = xform.rot; + result.headType = ikTypes.RotationAndPosition; + } else { + result.headType = ikTypes.HmdHead; + result.hipsType = props.hipsType; + result.hipsPosition = props.hipsPosition; + result.hipsRotation = props.hipsRotation; + } + + return result; + }, ANIM_VARS); + } + } + } else { + calibrated = false; + } +} + +Script.update.connect(update); + +Script.scriptEnding.connect(function () { + Controller.disableMapping(MAPPING_NAME); + Script.update.disconnect(update); +}); + diff --git a/scripts/developer/tests/viveMotionCapture.js b/scripts/developer/tests/viveMotionCapture.js new file mode 100644 index 0000000000..e7fb8566dc --- /dev/null +++ b/scripts/developer/tests/viveMotionCapture.js @@ -0,0 +1,348 @@ +/* global Xform */ +Script.include("/~/system/libraries/Xform.js"); + +var TRACKED_OBJECT_POSES = [ + "TrackedObject00", "TrackedObject01", "TrackedObject02", "TrackedObject03", + "TrackedObject04", "TrackedObject05", "TrackedObject06", "TrackedObject07", + "TrackedObject08", "TrackedObject09", "TrackedObject10", "TrackedObject11", + "TrackedObject12", "TrackedObject13", "TrackedObject14", "TrackedObject15" +]; + +var triggerPressHandled = false; +var rightTriggerPressed = false; +var leftTriggerPressed = false; +var calibrationCount = 0; + +var TRIGGER_MAPPING_NAME = "com.highfidelity.viveMotionCapture.triggers"; +var triggerMapping = Controller.newMapping(TRIGGER_MAPPING_NAME); +triggerMapping.from([Controller.Standard.RTClick]).peek().to(function (value) { + rightTriggerPressed = (value !== 0) ? true : false; +}); +triggerMapping.from([Controller.Standard.LTClick]).peek().to(function (value) { + leftTriggerPressed = (value !== 0) ? true : false; +}); +Controller.enableMapping(TRIGGER_MAPPING_NAME); + +var CONTROLLER_MAPPING_NAME = "com.highfidelity.viveMotionCapture.controller"; +var controllerMapping; + +var head; +var leftFoot; +var rightFoot; +var hips; +var spine2; + +var FEET_ONLY = 0; +var FEET_AND_HIPS = 1; +var FEET_AND_CHEST = 2; +var FEET_HIPS_AND_CHEST = 3; +var AUTO = 4; + +var SENSOR_CONFIG_NAMES = [ + "FeetOnly", + "FeetAndHips", + "FeetAndChest", + "FeetHipsAndChest", + "Auto" +]; + +var sensorConfig = AUTO; + +var Y_180 = {x: 0, y: 1, z: 0, w: 0}; +var Y_180_XFORM = new Xform(Y_180, {x: 0, y: 0, z: 0}); + +function computeOffsetXform(defaultToReferenceXform, pose, jointIndex) { + var poseXform = new Xform(pose.rotation, pose.translation); + + var defaultJointXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(jointIndex), + MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(jointIndex)); + + var referenceJointXform = Xform.mul(defaultToReferenceXform, defaultJointXform); + + return Xform.mul(poseXform.inv(), referenceJointXform); +} + +function computeDefaultToReferenceXform() { + var headIndex = MyAvatar.getJointIndex("Head"); + if (headIndex >= 0) { + var defaultHeadXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(headIndex), + MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(headIndex)); + var currentHeadXform = new Xform(Quat.cancelOutRollAndPitch(MyAvatar.getAbsoluteJointRotationInObjectFrame(headIndex)), + MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex)); + + var defaultToReferenceXform = Xform.mul(currentHeadXform, defaultHeadXform.inv()); + + return defaultToReferenceXform; + } else { + return Xform.ident(); + } +} + +function computeHeadOffsetXform() { + var leftEyeIndex = MyAvatar.getJointIndex("LeftEye"); + var rightEyeIndex = MyAvatar.getJointIndex("RightEye"); + var headIndex = MyAvatar.getJointIndex("Head"); + if (leftEyeIndex > 0 && rightEyeIndex > 0 && headIndex > 0) { + var defaultHeadXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(headIndex), + MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(headIndex)); + var defaultLeftEyeXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(leftEyeIndex), + MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(leftEyeIndex)); + var defaultRightEyeXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(rightEyeIndex), + MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(rightEyeIndex)); + var defaultCenterEyePos = Vec3.multiply(0.5, Vec3.sum(defaultLeftEyeXform.pos, defaultRightEyeXform.pos)); + var defaultCenterEyeXform = new Xform(defaultLeftEyeXform.rot, defaultCenterEyePos); + + return Xform.mul(defaultCenterEyeXform.inv(), defaultHeadXform); + } else { + return undefined; + } +} + +function calibrate() { + + head = undefined; + leftFoot = undefined; + rightFoot = undefined; + hips = undefined; + spine2 = undefined; + + var defaultToReferenceXform = computeDefaultToReferenceXform(); + + var headOffsetXform = computeHeadOffsetXform(); + print("AJT: computed headOffsetXform " + (headOffsetXform ? JSON.stringify(headOffsetXform) : "undefined")); + + if (headOffsetXform) { + head = { offsetXform: headOffsetXform }; + } + + var poses = []; + if (Controller.Hardware.Vive) { + TRACKED_OBJECT_POSES.forEach(function (key) { + var channel = Controller.Hardware.Vive[key]; + var pose = Controller.getPoseValue(channel); + if (pose.valid) { + poses.push({ + channel: channel, + pose: pose, + lastestPose: pose + }); + } + }); + } + + print("AJT: calibrating, num tracked poses = " + poses.length + ", sensorConfig = " + SENSOR_CONFIG_NAMES[sensorConfig]); + + var config = sensorConfig; + + if (config === AUTO) { + if (poses.length === 2) { + config = FEET_ONLY; + } else if (poses.length === 3) { + config = FEET_AND_HIPS; + } else if (poses.length >= 4) { + config = FEET_HIPS_AND_CHEST; + } else { + print("AJT: auto config failed: poses.length = " + poses.length); + config = FEET_ONLY; + } + } + + if (poses.length >= 2) { + // sort by y + poses.sort(function(a, b) { + var ay = a.pose.translation.y; + var by = b.pose.translation.y; + return ay - by; + }); + + if (poses[0].pose.translation.x > poses[1].pose.translation.x) { + rightFoot = poses[0]; + leftFoot = poses[1]; + } else { + rightFoot = poses[1]; + leftFoot = poses[0]; + } + + // compute offsets + rightFoot.offsetXform = computeOffsetXform(defaultToReferenceXform, rightFoot.pose, MyAvatar.getJointIndex("RightFoot")); + leftFoot.offsetXform = computeOffsetXform(defaultToReferenceXform, leftFoot.pose, MyAvatar.getJointIndex("LeftFoot")); + + print("AJT: rightFoot = " + JSON.stringify(rightFoot)); + print("AJT: leftFoot = " + JSON.stringify(leftFoot)); + + if (config === FEET_ONLY) { + // we're done! + } else if (config === FEET_AND_HIPS && poses.length >= 3) { + hips = poses[2]; + } else if (config === FEET_AND_CHEST && poses.length >= 3) { + spine2 = poses[2]; + } else if (config === FEET_HIPS_AND_CHEST && poses.length >= 4) { + hips = poses[2]; + spine2 = poses[3]; + } else { + // TODO: better error messages + print("AJT: could not calibrate for sensor config " + SENSOR_CONFIG_NAMES[config] + ", please try again!"); + } + + if (hips) { + hips.offsetXform = computeOffsetXform(defaultToReferenceXform, hips.pose, MyAvatar.getJointIndex("Hips")); + print("AJT: hips = " + JSON.stringify(hips)); + } + + if (spine2) { + spine2.offsetXform = computeOffsetXform(defaultToReferenceXform, spine2.pose, MyAvatar.getJointIndex("Spine2")); + print("AJT: spine2 = " + JSON.stringify(spine2)); + } + + } else { + print("AJT: could not find two trackers, try again!"); + } +} + +var ikTypes = { + RotationAndPosition: 0, + RotationOnly: 1, + HmdHead: 2, + HipsRelativeRotationAndPosition: 3, + Off: 4 +}; + +var handlerId; + +function convertJointInfoToPose(jointInfo) { + var latestPose = jointInfo.latestPose; + var offsetXform = jointInfo.offsetXform; + var xform = Xform.mul(new Xform(latestPose.rotation, latestPose.translation), offsetXform); + return { + valid: true, + translation: xform.pos, + rotation: xform.rot, + velocity: Vec3.sum(latestPose.velocity, Vec3.cross(latestPose.angularVelocity, Vec3.subtract(xform.pos, latestPose.translation))), + angularVelocity: latestPose.angularVelocity + }; +} + +function update(dt) { + if (rightTriggerPressed && leftTriggerPressed) { + if (!triggerPressHandled) { + triggerPressHandled = true; + if (controllerMapping) { + + // go back to normal, vive pucks will be ignored. + print("AJT: UN-CALIBRATE!"); + + head = undefined; + leftFoot = undefined; + rightFoot = undefined; + hips = undefined; + spine2 = undefined; + + Controller.disableMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + controllerMapping = undefined; + + } else { + print("AJT: CALIBRATE!"); + calibrate(); + calibrationCount++; + + controllerMapping = Controller.newMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + + if (head) { + controllerMapping.from(function () { + var worldToAvatarXform = (new Xform(MyAvatar.orientation, MyAvatar.position)).inv(); + head.latestPose = { + valid: true, + translation: worldToAvatarXform.xformPoint(HMD.position), + rotation: Quat.multiply(worldToAvatarXform.rot, Quat.multiply(HMD.orientation, Y_180)), // postMult 180 rot flips head direction + velocity: {x: 0, y: 0, z: 0}, // TODO: currently this is unused anyway... + angularVelocity: {x: 0, y: 0, z: 0} + }; + return convertJointInfoToPose(head); + }).to(Controller.Standard.Head); + } + + if (leftFoot) { + controllerMapping.from(leftFoot.channel).to(function (pose) { + leftFoot.latestPose = pose; + }); + controllerMapping.from(function () { + return convertJointInfoToPose(leftFoot); + }).to(Controller.Standard.LeftFoot); + } + if (rightFoot) { + controllerMapping.from(rightFoot.channel).to(function (pose) { + rightFoot.latestPose = pose; + }); + controllerMapping.from(function () { + return convertJointInfoToPose(rightFoot); + }).to(Controller.Standard.RightFoot); + } + if (hips) { + controllerMapping.from(hips.channel).to(function (pose) { + hips.latestPose = pose; + }); + controllerMapping.from(function () { + return convertJointInfoToPose(hips); + }).to(Controller.Standard.Hips); + } + if (spine2) { + controllerMapping.from(spine2.channel).to(function (pose) { + spine2.latestPose = pose; + }); + controllerMapping.from(function () { + return convertJointInfoToPose(spine2); + }).to(Controller.Standard.Spine2); + } + Controller.enableMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + } + } + } else { + triggerPressHandled = false; + } + + var drawMarkers = false; + if (drawMarkers) { + var RED = {x: 1, y: 0, z: 0, w: 1}; + var BLUE = {x: 0, y: 0, z: 1, w: 1}; + + if (leftFoot) { + var leftFootPose = Controller.getPoseValue(leftFoot.channel); + DebugDraw.addMyAvatarMarker("leftFootTracker", leftFootPose.rotation, leftFootPose.translation, BLUE); + } + + if (rightFoot) { + var rightFootPose = Controller.getPoseValue(rightFoot.channel); + DebugDraw.addMyAvatarMarker("rightFootTracker", rightFootPose.rotation, rightFootPose.translation, RED); + } + + if (hips) { + var hipsPose = Controller.getPoseValue(hips.channel); + DebugDraw.addMyAvatarMarker("hipsTracker", hipsPose.rotation, hipsPose.translation, GREEN); + } + } + + var drawReferencePose = false; + if (drawReferencePose) { + var GREEN = {x: 0, y: 1, z: 0, w: 1}; + var defaultToReferenceXform = computeDefaultToReferenceXform(); + var leftFootIndex = MyAvatar.getJointIndex("LeftFoot"); + if (leftFootIndex > 0) { + var defaultLeftFootXform = new Xform(MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(leftFootIndex), + MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(leftFootIndex)); + var referenceLeftFootXform = Xform.mul(defaultToReferenceXform, defaultLeftFootXform); + DebugDraw.addMyAvatarMarker("leftFootTracker", referenceLeftFootXform.rot, referenceLeftFootXform.pos, GREEN); + } + } + +} + +Script.update.connect(update); + +Script.scriptEnding.connect(function () { + Controller.disableMapping(TRIGGER_MAPPING_NAME); + if (controllerMapping) { + Controller.disableMapping(CONTROLLER_MAPPING_NAME + calibrationCount); + } + Script.update.disconnect(update); +}); + diff --git a/scripts/developer/tests/viveTrackedObjects.js b/scripts/developer/tests/viveTrackedObjects.js index 78911538e4..4155afb82b 100644 --- a/scripts/developer/tests/viveTrackedObjects.js +++ b/scripts/developer/tests/viveTrackedObjects.js @@ -18,14 +18,14 @@ function shutdown() { }); } -var WHITE = {x: 1, y: 1, z: 1, w: 1}; +var BLUE = {x: 0, y: 0, z: 1, w: 1}; function update(dt) { if (Controller.Hardware.Vive) { TRACKED_OBJECT_POSES.forEach(function (key) { var pose = Controller.getPoseValue(Controller.Hardware.Vive[key]); if (pose.valid) { - DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, WHITE); + DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, BLUE); } else { DebugDraw.removeMyAvatarMarker(key); } diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index eca72f71d0..f867eb3332 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1804,15 +1804,15 @@ function MyController(hand) { } this.processStylus(); - - if (isInEditMode() && !this.isNearStylusTarget) { + + if (isInEditMode() && !this.isNearStylusTarget && HMD.isHandControllerAvailable()) { // Always showing lasers while in edit mode and hands/stylus is not active. var rayPickInfo = this.calcRayPickInfo(this.hand); this.intersectionDistance = (rayPickInfo.entityID || rayPickInfo.overlayID) ? rayPickInfo.distance : 0; this.searchIndicatorOn(rayPickInfo.searchRay); } else { this.searchIndicatorOff(); - } + } }; this.handleLaserOnHomeButton = function(rayPickInfo) { diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 6fabeb2ec6..a6d2d165f7 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -343,7 +343,7 @@ var toolBar = (function () { activeButton = tablet.addButton({ icon: "icons/tablet-icons/edit-i.svg", activeIcon: "icons/tablet-icons/edit-a.svg", - text: "EDIT", + text: "CREATE", sortOrder: 10 }); tablet.screenChanged.connect(function (type, url) { @@ -2094,3 +2094,4 @@ entityListTool.webView.webEventReceived.connect(function (data) { }); }()); // END LOCAL_SCOPE + diff --git a/scripts/system/html/SnapshotReview.html b/scripts/system/html/SnapshotReview.html index 145cfb16a9..fb40c04d05 100644 --- a/scripts/system/html/SnapshotReview.html +++ b/scripts/system/html/SnapshotReview.html @@ -1,15 +1,17 @@ Share - + -
    - +
    + + +

    @@ -17,30 +19,16 @@
    -
    -
    -
    - - - - -
    -
    -
    - -
    -
    -
    -
    - - - - - - - - +
    +
    +
    + +
    + +
    + +
    diff --git a/scripts/system/html/css/SnapshotReview.css b/scripts/system/html/css/SnapshotReview.css index 34b690a021..12b91d372b 100644 --- a/scripts/system/html/css/SnapshotReview.css +++ b/scripts/system/html/css/SnapshotReview.css @@ -8,142 +8,280 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html */ -body { - padding-top: 0; - padding-bottom: 14px; -} +/* +// START styling of top bar and its contents +*/ -.snapsection { - padding-top: 14px; - text-align: center; -} - -.snapsection.title { - padding-top: 0; +.title { + padding: 6px 10px; text-align: left; + height: 26px; + line-height: 26px; + clear: both; } .title label { - font-size: 18px; position: relative; - top: 12px; + font-size: 18px; + float: left; } +#snapshotSettings { + position: relative; + float: right; +} +#settingsLabel { + position: relative; + float: right; + font-family: Raleway-SemiBold; + font-size: 14px; +} +.hifi-glyph { + font-size: 30px; + top: -4px; +} +input[type=button].naked { + color: #afafaf; + background: none; +} +input[type=button].naked:hover { + color: #ffffff; +} +input[type=button].naked:active { + color: #afafaf; +} +/* +// END styling of top bar and its contents +*/ + +/* +// START styling of snapshot instructions panel +*/ +.snapshotInstructions { + font-family: Raleway-Regular; + margin: 0 20px; + width: 100%; + height: 50%; +} +/* +// END styling of snapshot instructions panel +*/ + +/* +// START styling of snapshot pane and its contents +*/ #snapshot-pane { width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - box-sizing: border-box; - padding-top: 56px; - padding-bottom: 175px; + height: 560px; + display: flex; + justify-content: center; + align-items: center; } #snapshot-images { - height: 100%; width: 100%; - position: relative; -} - -#snapshot-images > div { - position: relative; - text-align: center; } #snapshot-images img { max-width: 100%; max-height: 100%; +} + +.gifLabel { + position:absolute; + left: 15px; + top: 10px; + font-family: Raleway-SemiBold; + font-size: 18px; + color: white; + text-shadow: 2px 2px 3px #000000; +} +/* +// END styling of snapshot pane and its contents +*/ + +/* +// START styling of share bar +*/ +.shareControls { + display: flex; + justify-content: space-between; + flex-direction: row; + align-items: center; + height: 50px; + line-height: 60px; + width: calc(100% - 8px); position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - vertical-align: middle; + bottom: 4px; + left: 4px; + right: 4px; } - -#snapshot-images div.property { - margin-top: 0; +.shareButtons { + display: flex; + align-items: center; + margin-left: 30px; + height: 100%; + width: 80%; +} +.blastToConnections { + text-align: left; + margin-right: 25px; + height: 29px; +} +.shareWithEveryone { + background: #DDDDDD url(../img/shareToFeed.png) no-repeat scroll center; + border-width: 0px; + text-align: left; + margin-right: 8px; + height: 29px; + width: 30px; + border-radius: 3px; +} +.facebookButton { + background-image: url(../img/fb_logo.png); + width: 29px; + height: 29px; + display: inline-block; + margin-right: 8px; +} +.twitterButton { + background-image: url(../img/twitter_logo.png); + width: 29px; + height: 29px; + display: inline-block; + margin-right: 8px; + border-radius: 3px; +} +.showShareButtonsButtonDiv { + display: inline-flex; + align-items: center; + font-family: Raleway-SemiBold; + font-size: 14px; + color: white; + text-shadow: 2px 2px 3px #000000; + height: 100%; + margin-right: 10px; + width: 20%; +} +.showShareButton { + width: 40px; + height: 40px; + border-radius: 50%; + border-width: 0; + margin-left: 5px; + outline: none; +} +.showShareButton.active { + border-color: #00b4ef; + border-width: 3px; + background-color: white; +} +.showShareButton.active:hover { + background-color: #afafaf; +} +.showShareButton.active:active { + background-color: white; +} +.showShareButton.inactive { + border-width: 0; + background-color: white; +} +.showShareButton.inactive:hover { + background-color: #afafaf; +} +.showShareButton.inactive:active { + background-color: white; +} +.showShareButtonDots { + display: flex; + width: 32px; + height: 40px; position: absolute; - top: 50%; - left: 7px; - transform: translate(0%, -50%); + top: 5px; + right: 14px; + pointer-events: none; } - -#snapshot-images img { - box-sizing: border-box; - padding: 0 7px 0 7px; -} - -#snapshot-images img.multiple { - padding-left: 28px; +.showShareButtonDots > span { + width: 10px; + height: 10px; + margin: auto; + background-color: #0093C5; + border-radius: 50%; + border-width: 0; + display: inline; } +/* +// END styling of share overlay +*/ +/* +// START styling of snapshot controls (bottom panel) and its contents +*/ #snapshot-controls { width: 100%; position: absolute; left: 0; - bottom: 14px; + overflow: hidden; + display: flex; + justify-content: center; +} +#snap-settings { + display: inline; + width: 150px; + margin: 2px auto 0 auto; +} +#snap-settings form input { + margin-bottom: 5px; } -.prompt { - font-family: Raleway-SemiBold; - font-size: 14px; -} - -div.button { - padding-top: 21px; -} - -.compound-button { - position: relative; - height: auto; -} - -.compound-button input { - padding-left: 40px; -} - -.compound-button .glyph { - display: inline-block; - position: absolute; - left: 12px; - top: 16px; - width: 23px; - height: 23px; - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgaGVpZ2h0PSI0MCIKICAgd2lkdGg9IjQwIgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgdmlld0JveD0iMCAwIDQwIDQwIgogICB5PSIwcHgiCiAgIHg9IjBweCIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGEzNCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48ZGM6dGl0bGU+PC9kYzp0aXRsZT48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczMyIiAvPjxzdHlsZQogICAgIGlkPSJzdHlsZTQiCiAgICAgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiM0MTQwNDI7fQoJLnN0MXtmaWxsOiNDQ0NDQ0M7fQoJLnN0MntmaWxsOiMxMzk4QkI7fQoJLnN0M3tmaWxsOiMzMUQ4RkY7fQo8L3N0eWxlPjxnCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwtMTEwKSIKICAgICBpZD0iTGF5ZXJfMSI+PGNpcmNsZQogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJjaXJjbGUxMyIKICAgICAgIHI9IjQuNDQwMDAwMSIKICAgICAgIGN5PSIxMjYuMTciCiAgICAgICBjeD0iMjAuNTQwMDAxIgogICAgICAgY2xhc3M9InN0MSIgLz48cGF0aAogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJwYXRoMTUiCiAgICAgICBkPSJtIDI4Ljg3LDEzOS4yNiBjIDAuMDEsLTAuMDEgMC4wMiwtMC4wMiAwLjAzLC0wLjAzIGwgMCwtMS44NiBjIDAsLTIuNjggLTIuMzMsLTQuNzcgLTUsLTQuNzcgbCAtNi40MiwwIGMgLTIuNjgsMCAtNC44NSwyLjA5IC00Ljg1LDQuNzcgbCAwLDEuODggMTYuMjQsMCB6IgogICAgICAgY2xhc3M9InN0MSIgLz48cGF0aAogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJwYXRoMTciCiAgICAgICBkPSJtIDM4LjE3LDEyMy40MiBjIDAsLTMuOTcgLTMuMjIsLTcuMTkgLTcuMTksLTcuMTkgbCAtMjAuMzEsMCBjIC0zLjk3LDAgLTcuMTksMy4yMiAtNy4xOSw3LjE5IGwgMCwxNC4xOCBjIDAsMy45NyAzLjIyLDcuMTkgNy4xOSw3LjE5IGwgMjAuMzEsMCBjIDMuOTcsMCA3LjE5LC0zLjIyIDcuMTksLTcuMTkgbCAwLC0xNC4xOCB6IG0gLTEuNzgsMTQuMjcgYyAwLDMuMDMgLTIuNDYsNS40OSAtNS40OSw1LjQ5IGwgLTIwLjMyLDAgYyAtMy4wMywwIC01LjQ5LC0yLjQ2IC01LjQ5LC01LjQ5IGwgMCwtMTQuMTkgYyAwLC0zLjAzIDIuNDYsLTUuNDkgNS40OSwtNS40OSBsIDIwLjMzLDAgYyAzLjAzLDAgNS40OSwyLjQ2IDUuNDksNS40OSBsIDAsMTQuMTkgeiIKICAgICAgIGNsYXNzPSJzdDEiIC8+PC9nPjxnCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwtMTEwKSIKICAgICBpZD0iTGF5ZXJfMiIgLz48L3N2Zz4=); - background-repeat: no-repeat; - background-size: 23px 23px; -} - -.setting { - display: inline-table; - height: 28px; -} - -.setting label { - display: table-cell; - vertical-align: middle; - font-family: Raleway-SemiBold; - font-size: 14px; -} - -.setting + .setting { - margin-left: 18px; -} - -input[type=button].naked { - font-size: 40px; - line-height: 40px; - width: 30px; +#snap-button { + width: 72px; + height: 72px; padding: 0; - margin: 0 0 -6px 0; - position: relative; - top: -6px; - left: -8px; - background: none; + border-radius: 50%; + background: #EA4C5F; + border: 3px solid white; + margin: 2px auto 0 auto; + box-sizing: content-box; + display: inline; + outline:none; } +#snap-button:disabled { + background: gray; +} +#snap-button:hover:enabled { + background: #C62147; +} +#snap-button:active:enabled { + background: #EA4C5F; +} +#snap-settings-right { + display: inline; + width: 150px; + margin: auto; +} +/* +// END styling of snapshot controls (bottom panel) and its contents +*/ -input[type=button].naked:hover { - color: #00b4ef; - background: none; +/* +// START misc styling +*/ +body { + padding: 0; + margin: 0; + overflow: hidden; } +p { + margin: 2px 0; +} +h4 { + margin: 14px 0 0 0; +} +.centeredImage { + margin: 0 auto; + display: block; +} +/* +// END misc styling +*/ diff --git a/scripts/system/html/css/hifi-style.css b/scripts/system/html/css/hifi-style.css new file mode 100644 index 0000000000..f1ace02eb0 --- /dev/null +++ b/scripts/system/html/css/hifi-style.css @@ -0,0 +1,170 @@ +/* +// hifi-style.css +// +// Created by Zach Fox on 2017-04-18 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +*/ + +@font-face { + font-family: Raleway-Regular; + src: url(../../../../resources/fonts/Raleway-Regular.ttf), /* Windows production */ + url(../../../../fonts/Raleway-Regular.ttf), /* OSX production */ + url(../../../../interface/resources/fonts/Raleway-Regular.ttf); /* Development, running script in /HiFi/examples */ +} + +@font-face { + font-family: Raleway-Light; + src: url(../../../../resources/fonts/Raleway-Light.ttf), + url(../../../../fonts/Raleway-Light.ttf), + url(../../../../interface/resources/fonts/Raleway-Light.ttf); +} + +@font-face { + font-family: Raleway-Bold; + src: url(../../../../resources/fonts/Raleway-Bold.ttf), + url(../../../../fonts/Raleway-Bold.ttf), + url(../../../../interface/resources/fonts/Raleway-Bold.ttf); +} + +@font-face { + font-family: Raleway-SemiBold; + src: url(../../../../resources/fonts/Raleway-SemiBold.ttf), + url(../../../../fonts/Raleway-SemiBold.ttf), + url(../../../../interface/resources/fonts/Raleway-SemiBold.ttf); +} + +@font-face { + font-family: FiraSans-SemiBold; + src: url(../../../../resources/fonts/FiraSans-SemiBold.ttf), + url(../../../../fonts/FiraSans-SemiBold.ttf), + url(../../../../interface/resources/fonts/FiraSans-SemiBold.ttf); +} + +@font-face { + font-family: AnonymousPro-Regular; + src: url(../../../../resources/fonts/AnonymousPro-Regular.ttf), + url(../../../../fonts/AnonymousPro-Regular.ttf), + url(../../../../interface/resources/fonts/AnonymousPro-Regular.ttf); +} + +@font-face { + font-family: HiFi-Glyphs; + src: url(../../../../resources/fonts/hifi-glyphs.ttf), + url(../../../../fonts/hifi-glyphs.ttf), + url(../../../../interface/resources/fonts/hifi-glyphs.ttf); +} + +body { + color: #afafaf; + background-color: #404040; + font-family: Raleway-Regular; + font-size: 15px; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + overflow-x: hidden; + overflow-y: auto; +} + +hr { + border: none; + background: #404040 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAjSURBVBhXY1RVVf3PgARYjIyMoEwIYHRwcEBRwQSloYCBAQCwjgPMiI7W2QAAAABJRU5ErkJggg==) repeat-x top left; + padding: 1px; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + -webkit-margin-start: 0; + -webkit-margin-end: 0; + width: 100%; + position: absolute; +} + +.hifi-glyph { + font-family: HiFi-Glyphs; + border: none; + //margin: -10px; + padding: 0; +} + +input[type=radio] { + width: 2em; + margin: 0; + padding: 0; + font-size: 1em; + opacity: 0; +} +input[type=radio] + label{ + display: inline-block; + margin-left: -2em; + line-height: 2em; +} +input[type=radio] + label > span{ + display: inline-block; + width: 20px; + height: 20px; + margin: 5px; + border-radius: 50%; + background: #6B6A6B; + background-image: linear-gradient(#7D7D7D, #6B6A6B); + vertical-align: bottom; +} +input[type=radio]:checked + label > span{ + background-image: linear-gradient(#7D7D7D, #6B6A6B); +} +input[type=radio]:active + label > span, +input[type=radio]:hover + label > span{ + background-image: linear-gradient(#FFFFFF, #AFAFAF); +} +input[type=radio]:checked + label > span > span, +input[type=radio]:active + label > span > span{ + display: block; + width: 10px; + height: 10px; + margin: 3px; + border: 2px solid #36CDFF; + border-radius: 50%; + background: #00B4EF; +} + +.grayButton { + font-family: FiraSans-SemiBold; + color: white; + padding: 0px 10px; + border-width: 0px; + background-image: linear-gradient(#FFFFFF, #AFAFAF); +} +.grayButton:hover { + background-image: linear-gradient(#FFFFFF, #FFFFFF); +} +.grayButton:active { + background-image: linear-gradient(#AFAFAF, #AFAFAF); +} +.grayButton:disabled { + background-image: linear-gradient(#FFFFFF, ##AFAFAF); +} +.blueButton { + font-family: FiraSans-SemiBold; + color: white; + padding: 0px 10px; + border-radius: 3px; + border-width: 0px; + background-image: linear-gradient(#00B4EF, #1080B8); + min-height: 30px; + +} +.blueButton:hover { + background-image: linear-gradient(#00B4EF, #00B4EF); +} +.blueButton:active { + background-image: linear-gradient(#1080B8, #1080B8); +} +.blueButton:disabled { + background-image: linear-gradient(#FFFFFF, #AFAFAF); +} \ No newline at end of file diff --git a/scripts/system/html/img/fb_logo.png b/scripts/system/html/img/fb_logo.png new file mode 100644 index 0000000000..1de20bacd8 Binary files /dev/null and b/scripts/system/html/img/fb_logo.png differ diff --git a/scripts/system/html/img/shareIcon.png b/scripts/system/html/img/shareIcon.png new file mode 100644 index 0000000000..0486ac9202 Binary files /dev/null and b/scripts/system/html/img/shareIcon.png differ diff --git a/scripts/system/html/img/shareToFeed.png b/scripts/system/html/img/shareToFeed.png new file mode 100644 index 0000000000..f681c49d8f Binary files /dev/null and b/scripts/system/html/img/shareToFeed.png differ diff --git a/scripts/system/html/img/snapshotIcon.png b/scripts/system/html/img/snapshotIcon.png new file mode 100644 index 0000000000..5cb2742a32 Binary files /dev/null and b/scripts/system/html/img/snapshotIcon.png differ diff --git a/scripts/system/html/img/twitter_logo.png b/scripts/system/html/img/twitter_logo.png new file mode 100644 index 0000000000..59fd027c2a Binary files /dev/null and b/scripts/system/html/img/twitter_logo.png differ diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index f140c54e09..53f4d17930 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -10,117 +10,325 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var paths = [], idCounter = 0, imageCount; -function addImage(data) { - if (!data.localPath) { +var paths = []; +var idCounter = 0; +var imageCount = 0; +function showSetupInstructions() { + var snapshotImagesDiv = document.getElementById("snapshot-images"); + snapshotImagesDiv.className = "snapshotInstructions"; + snapshotImagesDiv.innerHTML = 'Snapshot Instructions' + + '
    ' + + '

    This app lets you take and share snaps and GIFs with your connections in High Fidelity.

    ' + + "

    Setup Instructions

    " + + "

    Before you can begin taking snaps, please choose where you'd like to save snaps on your computer:

    " + + '
    ' + + '
    ' + + '' + + '
    '; + document.getElementById("snap-button").disabled = true; +} +function showSetupComplete() { + var snapshotImagesDiv = document.getElementById("snapshot-images"); + snapshotImagesDiv.className = "snapshotInstructions"; + snapshotImagesDiv.innerHTML = 'Snapshot Instructions' + + '
    ' + + "

    You're all set!

    " + + '

    Try taking a snapshot by pressing the red button below.

    '; +} +function chooseSnapshotLocation() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "chooseSnapshotLocation" + })); +} +function clearImages() { + document.getElementById("snap-button").disabled = false; + var snapshotImagesDiv = document.getElementById("snapshot-images"); + snapshotImagesDiv.classList.remove("snapshotInstructions"); + while (snapshotImagesDiv.hasChildNodes()) { + snapshotImagesDiv.removeChild(snapshotImagesDiv.lastChild); + } + paths = []; + imageCount = 0; + idCounter = 0; +} +function addImage(image_data, isGifLoading, isShowingPreviousImages, canSharePreviousImages, hifiShareButtonsDisabled) { + if (!image_data.localPath) { return; } - var div = document.createElement("DIV"), - input = document.createElement("INPUT"), - label = document.createElement("LABEL"), - img = document.createElement("IMG"), - div2 = document.createElement("DIV"), - id = "p" + idCounter++; - img.id = id + "img"; - function toggle() { data.share = input.checked; } - div.style.height = "" + Math.floor(100 / imageCount) + "%"; + var id = "p" + idCounter++; + // imageContainer setup + var imageContainer = document.createElement("DIV"); + imageContainer.id = id; + imageContainer.style.width = "100%"; + imageContainer.style.height = "251px"; + imageContainer.style.display = "flex"; + imageContainer.style.justifyContent = "center"; + imageContainer.style.alignItems = "center"; + imageContainer.style.position = "relative"; + // img setup + var img = document.createElement("IMG"); + img.id = id + "img"; if (imageCount > 1) { img.setAttribute("class", "multiple"); } - img.src = data.localPath; - div.appendChild(img); - if (imageCount > 1) { // I'd rather use css, but the included stylesheet is quite particular. - // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. - label.setAttribute('for', id); // cannot do label.for = - input.id = id; - input.type = "checkbox"; - input.checked = false; - data.share = input.checked; - input.addEventListener('change', toggle); - div2.setAttribute("class", "property checkbox"); - div2.appendChild(input); - div2.appendChild(label); - div.appendChild(div2); - } else { - data.share = true; + img.src = image_data.localPath; + imageContainer.appendChild(img); + document.getElementById("snapshot-images").appendChild(imageContainer); + paths.push(image_data.localPath); + var isGif = img.src.split('.').pop().toLowerCase() === "gif"; + if (isGif) { + imageContainer.innerHTML += 'GIF'; + } + if (!isGifLoading && !isShowingPreviousImages) { + shareForUrl(id); + } else if (isShowingPreviousImages && canSharePreviousImages) { + appendShareBar(id, image_data.story_id, isGif, hifiShareButtonsDisabled) } - document.getElementById("snapshot-images").appendChild(div); - paths.push(data); } -function handleShareButtons(messageOptions) { - var openFeed = document.getElementById('openFeed'); - openFeed.checked = messageOptions.openFeedAfterShare; - openFeed.onchange = function () { +function appendShareBar(divID, story_id, isGif, hifiShareButtonsDisabled) { + var story_url = "https://highfidelity.com/user_stories/" + story_id; + var parentDiv = document.getElementById(divID); + parentDiv.setAttribute('data-story-id', story_id); + document.getElementById(divID).appendChild(createShareBar(divID, isGif, story_url, hifiShareButtonsDisabled)); + if (divID === "p0") { + selectImageToShare(divID, true); + } +} +function createShareBar(parentID, isGif, shareURL, hifiShareButtonsDisabled) { + var shareBar = document.createElement("div"); + shareBar.id = parentID + "shareBar"; + shareBar.className = "shareControls"; + var shareButtonsDivID = parentID + "shareButtonsDiv"; + var showShareButtonsButtonDivID = parentID + "showShareButtonsButtonDiv"; + var showShareButtonsButtonID = parentID + "showShareButtonsButton"; + var showShareButtonsLabelID = parentID + "showShareButtonsLabel"; + var blastToConnectionsButtonID = parentID + "blastToConnectionsButton"; + var shareWithEveryoneButtonID = parentID + "shareWithEveryoneButton"; + var facebookButtonID = parentID + "facebookButton"; + var twitterButtonID = parentID + "twitterButton"; + shareBar.innerHTML += '' + + '' + + '
    ' + + '' + + '' + + '
    ' + + '' + + '
    ' + + '
    '; + + // Add onclick handler to parent DIV's img to toggle share buttons + document.getElementById(parentID + 'img').onclick = function () { selectImageToShare(parentID, true) }; + + return shareBar; +} +function selectImageToShare(selectedID, isSelected) { + if (selectedID.id) { + selectedID = selectedID.id; // sometimes (?), `selectedID` is passed as an HTML object to these functions; we just want the ID + } + var imageContainer = document.getElementById(selectedID); + var image = document.getElementById(selectedID + 'img'); + var shareBar = document.getElementById(selectedID + "shareBar"); + var shareButtonsDiv = document.getElementById(selectedID + "shareButtonsDiv"); + var showShareButtonsButton = document.getElementById(selectedID + "showShareButtonsButton"); + + if (isSelected) { + showShareButtonsButton.onclick = function () { selectImageToShare(selectedID, false) }; + showShareButtonsButton.classList.remove("inactive"); + showShareButtonsButton.classList.add("active"); + + image.onclick = function () { selectImageToShare(selectedID, false) }; + imageContainer.style.outline = "4px solid #00b4ef"; + imageContainer.style.outlineOffset = "-4px"; + + shareBar.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; + + shareButtonsDiv.style.visibility = "visible"; + } else { + showShareButtonsButton.onclick = function () { selectImageToShare(selectedID, true) }; + showShareButtonsButton.classList.remove("active"); + showShareButtonsButton.classList.add("inactive"); + + image.onclick = function () { selectImageToShare(selectedID, true) }; + imageContainer.style.outline = "none"; + + shareBar.style.backgroundColor = "rgba(0, 0, 0, 0.0)"; + + shareButtonsDiv.style.visibility = "hidden"; + } +} +function shareForUrl(selectedID) { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "shareSnapshotForUrl", + data: paths[parseInt(selectedID.substring(1))] + })); +} +function blastToConnections(selectedID, isGif) { + selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID + + document.getElementById(selectedID + "blastToConnectionsButton").disabled = true; + document.getElementById(selectedID + "shareWithEveryoneButton").disabled = true; + + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "blastToConnections", + story_id: document.getElementById(selectedID).getAttribute("data-story-id"), + isGif: isGif + })); +} +function shareWithEveryone(selectedID, isGif) { + selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID + + document.getElementById(selectedID + "blastToConnectionsButton").disabled = true; + document.getElementById(selectedID + "shareWithEveryoneButton").disabled = true; + + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "shareSnapshotWithEveryone", + story_id: document.getElementById(selectedID).getAttribute("data-story-id"), + isGif: isGif + })); +} +function shareButtonClicked(selectedID) { + selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "shareButtonClicked", + story_id: document.getElementById(selectedID).getAttribute("data-story-id") + })); +} +function cancelSharing(selectedID) { + selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID + var shareBar = document.getElementById(selectedID + "shareBar"); + + shareBar.style.display = "inline"; +} + +function handleCaptureSetting(setting) { + var stillAndGif = document.getElementById('stillAndGif'); + var stillOnly = document.getElementById('stillOnly'); + stillAndGif.checked = setting; + stillOnly.checked = !setting; + + stillAndGif.onclick = function () { EventBridge.emitWebEvent(JSON.stringify({ type: "snapshot", - action: (openFeed.checked ? "setOpenFeedTrue" : "setOpenFeedFalse") + action: "captureStillAndGif" })); - }; - - if (!messageOptions.canShare) { - // this means you may or may not be logged in, but can't share - // because you are not in a public place. - document.getElementById("sharing").innerHTML = "

    Snapshots can be shared when they're taken in shareable places."; } + stillOnly.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "captureStillOnly" + })); + } + } window.onload = function () { - // Something like the following will allow testing in a browser. - //addImage({localPath: 'c:/Users/howar/OneDrive/Pictures/hifi-snap-by--on-2016-07-27_12-58-43.jpg'}); - //addImage({ localPath: 'http://lorempixel.com/1512/1680' }); + // Uncomment the line below to test functionality in a browser. + // See definition of "testInBrowser()" to modify tests. + //testInBrowser(true); openEventBridge(function () { // Set up a handler for receiving the data, and tell the .js we are ready to receive it. EventBridge.scriptEventReceived.connect(function (message) { + message = JSON.parse(message); + if (message.type !== "snapshot") { return; } + + switch (message.action) { + case 'showSetupInstructions': + showSetupInstructions(); + break; + case 'snapshotLocationChosen': + clearImages(); + showSetupComplete(); + break; + case 'clearPreviousImages': + clearImages(); + break; + case 'showPreviousImages': + clearImages(); + var messageOptions = message.options; + imageCount = message.image_data.length; + message.image_data.forEach(function (element, idx, array) { + addImage(element, true, true, message.canShare, message.image_data[idx].buttonDisabled); + }); + break; + case 'addImages': + // The last element of the message contents list contains a bunch of options, + // including whether or not we can share stuff + // The other elements of the list contain image paths. + var messageOptions = message.options; - // The last element of the message contents list contains a bunch of options, - // including whether or not we can share stuff - // The other elements of the list contain image paths. - var messageOptions = message.action.pop(); - handleShareButtons(messageOptions); + if (messageOptions.containsGif) { + if (messageOptions.processingGif) { + imageCount = message.image_data.length + 1; // "+1" for the GIF that'll finish processing soon + message.image_data.unshift({ localPath: messageOptions.loadingGifPath }); + message.image_data.forEach(function (element, idx, array) { + addImage(element, idx === 0, false, false); + }); + } else { + var gifPath = message.image_data[0].localPath; + var p0img = document.getElementById('p0img'); + p0img.src = gifPath; - if (messageOptions.containsGif) { - if (messageOptions.processingGif) { - imageCount = message.action.length + 1; // "+1" for the GIF that'll finish processing soon - message.action.unshift({ localPath: messageOptions.loadingGifPath }); - message.action.forEach(addImage); - document.getElementById('p0').disabled = true; - } else { - var gifPath = message.action[0].localPath; - document.getElementById('p0').disabled = false; - document.getElementById('p0img').src = gifPath; - paths[0].localPath = gifPath; - } - } else { - imageCount = message.action.length; - message.action.forEach(addImage); + paths[0] = gifPath; + shareForUrl("p0"); + } + } else { + imageCount = message.image_data.length; + message.image_data.forEach(function (element, idx, array) { + addImage(element, false, false, false); + }); + } + break; + case 'captureSettings': + handleCaptureSetting(message.setting); + break; + case 'snapshotUploadComplete': + var isGif = message.image_url.split('.').pop().toLowerCase() === "gif"; + appendShareBar(isGif || imageCount === 1 ? "p0" : "p1", message.story_id, isGif); + break; + default: + console.log("Unknown message action received in SnapshotReview.js."); + break; } }); + EventBridge.emitWebEvent(JSON.stringify({ type: "snapshot", action: "ready" })); - }); - + });; }; -// beware of bug: Cannot send objects at top level. (Nested in arrays is fine.) -function shareSelected() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "snapshot", - action: paths - })); -} -function doNotShare() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "snapshot", - action: [] - })); -} function snapshotSettings() { EventBridge.emitWebEvent(JSON.stringify({ type: "snapshot", action: "openSettings" })); } +function takeSnapshot() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "takeSnapshot" + })); +} + +function testInBrowser(isTestingSetupInstructions) { + if (isTestingSetupInstructions) { + showSetupInstructions(); + } else { + imageCount = 1; + //addImage({ localPath: 'http://lorempixel.com/553/255' }); + addImage({ localPath: 'C:/Users/valef/Desktop/hifi-snap-by-zfox-on-2017-04-26_10-26-53.gif' }, false, true, true, false); + } +} diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 0ffea0c568..47082e882f 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -1,4 +1,6 @@ "use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Window, Script, Controller, MyAvatar, AvatarList, Entities, Messages, Audio, SoundCache, Account, UserActivityLogger, Vec3, Quat, XMLHttpRequest, location, print*/ // // makeUserConnection.js // scripts/system @@ -9,11 +11,11 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -(function() { // BEGIN LOCAL_SCOPE +(function () { // BEGIN LOCAL_SCOPE var LABEL = "makeUserConnection"; var MAX_AVATAR_DISTANCE = 0.2; // m - var GRIP_MIN = 0.05; // goes from 0-1, so 5% pressed is pressed + var GRIP_MIN = 0.75; // goes from 0-1, so 75% pressed is pressed var MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; var STATES = { INACTIVE: 0, @@ -22,12 +24,13 @@ MAKING_CONNECTION: 3 }; var STATE_STRINGS = ["inactive", "waiting", "connecting", "makingConnection"]; + var HAND_STRING_PROPERTY = 'hand'; // Used in our message protocol. IWBNI we changed it to handString, but that would break compatability. var WAITING_INTERVAL = 100; // ms var CONNECTING_INTERVAL = 100; // ms var MAKING_CONNECTION_TIMEOUT = 800; // ms var CONNECTING_TIME = 1600; // ms var PARTICLE_RADIUS = 0.15; // m - var PARTICLE_ANGLE_INCREMENT = 360/45; // 1hz + var PARTICLE_ANGLE_INCREMENT = 360 / 45; // 1hz var HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/4beat_sweep.wav"; var SUCCESSFUL_HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/3rdbeat_success_bell.wav"; var PREFERRER_HAND_JOINT_POSTFIX_ORDER = ['Middle1', 'Index1', '']; @@ -39,7 +42,7 @@ var PARTICLE_EFFECT_PROPS = { "alpha": 0.8, "azimuthFinish": Math.PI, - "azimuthStart": -1*Math.PI, + "azimuthStart": -1 * Math.PI, "emitRate": 500, "emitSpeed": 0.0, "emitterShouldTrail": 1, @@ -56,10 +59,10 @@ "color": {"red": 255, "green": 255, "blue": 255}, "colorFinish": {"red": 0, "green": 164, "blue": 255}, "colorStart": {"red": 255, "green": 255, "blue": 255}, - "emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71}, + "emitOrientation": {"w": -0.71, "x": 0.0, "y": 0.0, "z": 0.71}, "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, - "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, + "dimensions": {"x": 0.05, "y": 0.05, "z": 0.05}, "type": "ParticleEffect" }; var MAKING_CONNECTION_PARTICLE_PROPS = { @@ -68,7 +71,7 @@ "alphaSpread": 0, "alphaFinish": 0, "azimuthFinish": Math.PI, - "azimuthStart": -1*Math.PI, + "azimuthStart": -1 * Math.PI, "emitRate": 2000, "emitSpeed": 0.0, "emitterShouldTrail": 1, @@ -86,14 +89,14 @@ "color": {"red": 200, "green": 170, "blue": 255}, "colorFinish": {"red": 0, "green": 134, "blue": 255}, "colorStart": {"red": 185, "green": 222, "blue": 255}, - "emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71}, + "emitOrientation": {"w": -0.71, "x": 0.0, "y": 0.0, "z": 0.71}, "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, - "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, + "dimensions": {"x": 0.05, "y": 0.05, "z": 0.05}, "type": "ParticleEffect" }; - var currentHand = undefined; + var currentHand; var currentHandJointIndex = -1; var state = STATES.INACTIVE; var connectingInterval; @@ -101,7 +104,6 @@ var makingConnectionTimeout; var animHandlerId; var connectingId; - var connectingHandString; var connectingHandJointIndex = -1; var waitingList = {}; var particleEffect; @@ -116,7 +118,7 @@ function debug() { var stateString = "<" + STATE_STRINGS[state] + ">"; - var connecting = "[" + connectingId + "/" + connectingHandString + "]"; + var connecting = "[" + connectingId + "/" + connectingHandJointIndex + "]"; print.apply(null, [].concat.apply([LABEL, stateString, JSON.stringify(waitingList), connecting], [].map.call(arguments, JSON.stringify))); } @@ -183,27 +185,19 @@ function handToString(hand) { if (hand === Controller.Standard.RightHand) { return "RightHand"; - } else if (hand === Controller.Standard.LeftHand) { + } + if (hand === Controller.Standard.LeftHand) { return "LeftHand"; } debug("handToString called without valid hand! value: ", hand); return ""; } - function stringToHand(hand) { - if (hand === "RightHand") { - return Controller.Standard.RightHand; - } else if (hand === "LeftHand") { - return Controller.Standard.LeftHand; - } - debug("stringToHand called with bad hand string:", hand); - return 0; - } - function handToHaptic(hand) { if (hand === Controller.Standard.RightHand) { return 1; - } else if (hand === Controller.Standard.LeftHand) { + } + if (hand === Controller.Standard.LeftHand) { return 0; } debug("handToHaptic called without a valid hand!"); @@ -229,13 +223,13 @@ } // This returns the ideal hand joint index for the avatar. - // [hand]middle1 -> [hand]index1 -> [hand] - function getIdealHandJointIndex(avatar, hand) { - debug("got hand " + hand + " for avatar " + avatar.sessionUUID); - var handString = handToString(hand); - for (var i = 0; i < PREFERRER_HAND_JOINT_POSTFIX_ORDER.length; i++) { - var jointName = handString + PREFERRER_HAND_JOINT_POSTFIX_ORDER[i]; - var jointIndex = avatar.getJointIndex(jointName); + // [handString]middle1 -> [handString]index1 -> [handString] + function getIdealHandJointIndex(avatar, handString) { + debug("get hand " + handString + " for avatar " + (avatar && avatar.sessionUUID)); + var suffixIndex, jointName, jointIndex; + for (suffixIndex = 0; suffixIndex < (avatar ? PREFERRER_HAND_JOINT_POSTFIX_ORDER.length : 0); suffixIndex++) { + jointName = handString + PREFERRER_HAND_JOINT_POSTFIX_ORDER[suffixIndex]; + jointIndex = avatar.getJointIndex(jointName); if (jointIndex !== -1) { debug('found joint ' + jointName + ' (' + jointIndex + ')'); return jointIndex; @@ -249,26 +243,39 @@ function getHandPosition(avatar, handJointIndex) { if (handJointIndex === -1) { debug("calling getHandPosition with no hand joint index! (returning avatar position but this is a BUG)"); - debug(new Error().stack); return avatar.position; } return avatar.getJointPosition(handJointIndex); } - function shakeHandsAnimation(animationProperties) { + var animationData = {}; + function updateAnimationData() { // all we are doing here is moving the right hand to a spot // that is in front of and a bit above the hips. Basing how // far in front as scaling with the avatar's height (say hips // to head distance) var headIndex = MyAvatar.getJointIndex("Head"); var offset = 0.5; // default distance of hand in front of you - var result = {}; if (headIndex) { offset = 0.8 * MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; } - result.rightHandPosition = Vec3.multiply(offset, {x: -0.25, y: 0.8, z: 1.3}); - result.rightHandRotation = Quat.fromPitchYawRollDegrees(90, 0, 90); - return result; + animationData.rightHandPosition = Vec3.multiply(offset, {x: -0.25, y: 0.8, z: 1.3}); + animationData.rightHandRotation = Quat.fromPitchYawRollDegrees(90, 0, 90); + } + function shakeHandsAnimation() { + return animationData; + } + function endHandshakeAnimation() { + if (animHandlerId) { + debug("removing animation"); + animHandlerId = MyAvatar.removeAnimationStateHandler(animHandlerId); + } + } + function startHandshakeAnimation() { + endHandshakeAnimation(); // just in case order of press/unpress is broken + debug("adding animation"); + updateAnimationData(); + animHandlerId = MyAvatar.addAnimationStateHandler(shakeHandsAnimation, []); } function positionFractionallyTowards(posA, posB, frac) { @@ -294,11 +301,11 @@ } } - function calcParticlePos(myHand, otherHand, otherOrientation, reset) { + function calcParticlePos(myHandPosition, otherHandPosition, otherOrientation, reset) { if (reset) { particleRotationAngle = 0.0; } - var position = positionFractionallyTowards(myHand, otherHand, 0.5); + var position = positionFractionallyTowards(myHandPosition, otherHandPosition, 0.5); particleRotationAngle += PARTICLE_ANGLE_INCREMENT; // about 0.5 hz var radius = Math.min(PARTICLE_RADIUS, PARTICLE_RADIUS * particleRotationAngle / 360); var axis = Vec3.mix(Quat.getFront(MyAvatar.orientation), Quat.inverse(Quat.getFront(otherOrientation)), 0.5); @@ -314,80 +321,78 @@ } var myHandPosition = getHandPosition(MyAvatar, currentHandJointIndex); - var otherHand; + var otherHandPosition; var otherOrientation; if (connectingId) { var other = AvatarList.getAvatar(connectingId); if (other) { otherOrientation = other.orientation; - otherHand = getHandPosition(other, connectingHandJointIndex); + otherHandPosition = getHandPosition(other, connectingHandJointIndex); } } switch (state) { - case STATES.WAITING: - // no visualization while waiting - deleteParticleEffect(); - deleteMakeConnectionParticleEffect(); - stopHandshakeSound(); - break; - case STATES.CONNECTING: - var particleProps = {}; - // put the position between the 2 hands, if we have a connectingId. This - // helps define the plane in which the particles move. - positionFractionallyTowards(myHandPosition, otherHand, 0.5); - // now manage the rest of the entity - if (!particleEffect) { - particleRotationAngle = 0.0; - particleEmitRate = 500; - particleProps = PARTICLE_EFFECT_PROPS; - particleProps.isEmitting = 0; - particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); - particleProps.parentID = MyAvatar.sessionUUID; - particleEffect = Entities.addEntity(particleProps, true); - } else { - particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); - particleProps.isEmitting = 1; - Entities.editEntity(particleEffect, particleProps); - } - if (!makingConnectionParticleEffect) { - var props = MAKING_CONNECTION_PARTICLE_PROPS; - props.parentID = MyAvatar.sessionUUID; - makingConnectionEmitRate = 2000; - props.emitRate = makingConnectionEmitRate; - props.position = myHandPosition; - makingConnectionParticleEffect = Entities.addEntity(props, true); - } else { - makingConnectionEmitRate *= 0.5; - Entities.editEntity(makingConnectionParticleEffect, { - emitRate: makingConnectionEmitRate, - position: myHandPosition, - isEmitting: true - }); - } - break; - case STATES.MAKING_CONNECTION: - particleEmitRate = Math.max(50, particleEmitRate * 0.5); - Entities.editEntity(makingConnectionParticleEffect, {emitRate: 0, isEmitting: 0, position: myHandPosition}); - Entities.editEntity(particleEffect, { - position: calcParticlePos(myHandPosition, otherHand, otherOrientation), - emitRate: particleEmitRate + case STATES.WAITING: + // no visualization while waiting + deleteParticleEffect(); + deleteMakeConnectionParticleEffect(); + stopHandshakeSound(); + break; + case STATES.CONNECTING: + var particleProps = {}; + // put the position between the 2 hands, if we have a connectingId. This + // helps define the plane in which the particles move. + positionFractionallyTowards(myHandPosition, otherHandPosition, 0.5); + // now manage the rest of the entity + if (!particleEffect) { + particleRotationAngle = 0.0; + particleEmitRate = 500; + particleProps = PARTICLE_EFFECT_PROPS; + particleProps.isEmitting = 0; + particleProps.position = calcParticlePos(myHandPosition, otherHandPosition, otherOrientation); + particleProps.parentID = MyAvatar.sessionUUID; + particleEffect = Entities.addEntity(particleProps, true); + } else { + particleProps.position = calcParticlePos(myHandPosition, otherHandPosition, otherOrientation); + particleProps.isEmitting = 1; + Entities.editEntity(particleEffect, particleProps); + } + if (!makingConnectionParticleEffect) { + var props = MAKING_CONNECTION_PARTICLE_PROPS; + props.parentID = MyAvatar.sessionUUID; + makingConnectionEmitRate = 2000; + props.emitRate = makingConnectionEmitRate; + props.position = myHandPosition; + makingConnectionParticleEffect = Entities.addEntity(props, true); + } else { + makingConnectionEmitRate *= 0.5; + Entities.editEntity(makingConnectionParticleEffect, { + emitRate: makingConnectionEmitRate, + position: myHandPosition, + isEmitting: true }); - break; - default: - debug("unexpected state", state); - break; + } + break; + case STATES.MAKING_CONNECTION: + particleEmitRate = Math.max(50, particleEmitRate * 0.5); + Entities.editEntity(makingConnectionParticleEffect, {emitRate: 0, isEmitting: 0, position: myHandPosition}); + Entities.editEntity(particleEffect, { + position: calcParticlePos(myHandPosition, otherHandPosition, otherOrientation), + emitRate: particleEmitRate + }); + break; + default: + debug("unexpected state", state); + break; } } - function isNearby(id, hand) { + function isNearby() { if (currentHand) { - var handPos = getHandPosition(MyAvatar, currentHandJointIndex); - var avatar = AvatarList.getAvatar(id); + var handPosition = getHandPosition(MyAvatar, currentHandJointIndex); + var avatar = AvatarList.getAvatar(connectingId); if (avatar) { - var otherHand = stringToHand(hand); - var otherHandJointIndex = getIdealHandJointIndex(avatar, otherHand); - var distance = Vec3.distance(getHandPosition(avatar, otherHandJointIndex), handPos); + var distance = Vec3.distance(getHandPosition(avatar, connectingHandJointIndex), handPosition); return (distance < MAX_AVATAR_DISTANCE); } } @@ -395,68 +400,90 @@ } function findNearestWaitingAvatar() { - var handPos = getHandPosition(MyAvatar, currentHandJointIndex); + var handPosition = getHandPosition(MyAvatar, currentHandJointIndex); var minDistance = MAX_AVATAR_DISTANCE; var nearestAvatar = {}; Object.keys(waitingList).forEach(function (identifier) { var avatar = AvatarList.getAvatar(identifier); if (avatar) { - var hand = stringToHand(waitingList[identifier]); - var handJointIndex = getIdealHandJointIndex(avatar, hand); - var distance = Vec3.distance(getHandPosition(avatar, handJointIndex), handPos); + var handJointIndex = waitingList[identifier]; + var distance = Vec3.distance(getHandPosition(avatar, handJointIndex), handPosition); if (distance < minDistance) { minDistance = distance; - nearestAvatar = {avatar: identifier, hand: hand, avatarObject: avatar}; + nearestAvatar = {avatarId: identifier, jointIndex: handJointIndex}; } } }); return nearestAvatar; } + function messageSend(message) { + Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); + } + function handStringMessageSend(message) { + message[HAND_STRING_PROPERTY] = handToString(currentHand); + messageSend(message); + } + function setupCandidate() { // find the closest in-range avatar, send connection request, and return true. (Otherwise falsey) + var nearestAvatar = findNearestWaitingAvatar(); + if (nearestAvatar.avatarId) { + connectingId = nearestAvatar.avatarId; + connectingHandJointIndex = nearestAvatar.jointIndex; + debug("sending connectionRequest to", connectingId); + handStringMessageSend({ + key: "connectionRequest", + id: connectingId + }); + return true; + } + } + function clearConnecting() { + connectingId = undefined; + connectingHandJointIndex = -1; + } + function lookForWaitingAvatar() { + // we started with nobody close enough, but maybe I've moved + // or they did. Note that 2 people doing this race, so stop + // as soon as you have a connectingId (which means you got their + // message before noticing they were in range in this loop) + // just in case we re-enter before stopping + stopWaiting(); + debug("started looking for waiting avatars"); + waitingInterval = Script.setInterval(function () { + if (state === STATES.WAITING && !connectingId) { + setupCandidate(); + } else { + // something happened, stop looking for avatars to connect + stopWaiting(); + debug("stopped looking for waiting avatars"); + } + }, WAITING_INTERVAL); + } + + var pollCount = 0, requestUrl = location.metaverseServerUrl + '/api/v1/user/connection_request'; // As currently implemented, we select the closest waiting avatar (if close enough) and send // them a connectionRequest. If nobody is close enough we send a waiting message, and wait for a // connectionRequest. If the 2 people who want to connect are both somewhat out of range when they // initiate the shake, they will race to see who sends the connectionRequest after noticing the - // waiting message. Either way, they will start connecting eachother at that point. + // waiting message. Either way, they will start connecting each other at that point. function startHandshake(fromKeyboard) { if (fromKeyboard) { - debug("adding animation"); - // just in case order of press/unpress is broken - if (animHandlerId) { - animHandlerId = MyAvatar.removeAnimationStateHandler(animHandlerId); - } - animHandlerId = MyAvatar.addAnimationStateHandler(shakeHandsAnimation, []); + startHandshakeAnimation(); } debug("starting handshake for", currentHand); pollCount = 0; state = STATES.WAITING; - connectingId = undefined; - connectingHandString = undefined; - connectingHandJointIndex = -1; + clearConnecting(); // just in case stopWaiting(); stopConnecting(); stopMakingConnection(); - - var nearestAvatar = findNearestWaitingAvatar(); - if (nearestAvatar.avatar) { - connectingId = nearestAvatar.avatar; - connectingHandString = handToString(nearestAvatar.hand); - connectingHandJointIndex = getIdealHandJointIndex(nearestAvatar.avatarObject, nearestAvatar.hand); - currentHandJointIndex = getIdealHandJointIndex(MyAvatar, currentHand); - debug("sending connectionRequest to", connectingId); - messageSend({ - key: "connectionRequest", - id: connectingId, - hand: handToString(currentHand) - }); - } else { + if (!setupCandidate()) { // send waiting message debug("sending waiting message"); - messageSend({ + handStringMessageSend({ key: "waiting", - hand: handToString(currentHand) }); lookForWaitingAvatar(); } @@ -474,9 +501,7 @@ // as we ignore the key release event when inactive. See updateTriggers // below. state = STATES.INACTIVE; - connectingId = undefined; - connectingHandString = undefined; - connectingHandJointIndex = -1; + clearConnecting(); stopWaiting(); stopConnecting(); stopMakingConnection(); @@ -486,10 +511,7 @@ key: "done" }); - if (animHandlerId) { - debug("removing animation"); - MyAvatar.removeAnimationStateHandler(animHandlerId); - } + endHandshakeAnimation(); // No-op if we were successful, but this way we ensure that failures and abandoned handshakes don't leave us // in a weird state. request({uri: requestUrl, method: 'DELETE'}, debug); @@ -500,19 +522,16 @@ debug("currentHand", currentHand, "ignoring messages from", hand); return; } - if (!currentHand) { - currentHand = hand; - currentHandJointIndex = getIdealHandJointIndex(MyAvatar, currentHand); - } + currentHand = hand; + currentHandJointIndex = getIdealHandJointIndex(MyAvatar, handToString(currentHand)); // Always, in case of changed skeleton. // ok now, we are either initiating or quitting... var isGripping = value > GRIP_MIN; if (isGripping) { debug("updateTriggers called - gripping", handToString(hand)); if (state !== STATES.INACTIVE) { return; - } else { - startHandshake(fromKeyboard); } + startHandshake(fromKeyboard); } else { // TODO: should we end handshake even when inactive? Ponder debug("updateTriggers called -- no longer gripping", handToString(hand)); @@ -524,47 +543,12 @@ } } - function messageSend(message) { - Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); - } - - function lookForWaitingAvatar() { - // we started with nobody close enough, but maybe I've moved - // or they did. Note that 2 people doing this race, so stop - // as soon as you have a connectingId (which means you got their - // message before noticing they were in range in this loop) - - // just in case we re-enter before stopping - stopWaiting(); - debug("started looking for waiting avatars"); - waitingInterval = Script.setInterval(function () { - if (state === STATES.WAITING && !connectingId) { - // find the closest in-range avatar, and send connection request - var nearestAvatar = findNearestWaitingAvatar(); - if (nearestAvatar.avatar) { - connectingId = nearestAvatar.avatar; - connectingHandString = handToString(nearestAvatar.hand); - debug("sending connectionRequest to", connectingId); - messageSend({ - key: "connectionRequest", - id: connectingId, - hand: handToString(currentHand) - }); - } - } else { - // something happened, stop looking for avatars to connect - stopWaiting(); - debug("stopped looking for waiting avatars"); - } - }, WAITING_INTERVAL); - } - /* There is a mini-state machine after entering STATES.makingConnection. We make a request (which might immediately succeed, fail, or neither. If we immediately fail, we tell the user. Otherwise, we wait MAKING_CONNECTION_TIMEOUT. At that time, we poll until success or fail. */ - var result, requestBody, pollCount = 0, requestUrl = location.metaverseServerUrl + '/api/v1/user/connection_request'; + var result, requestBody; function connectionRequestCompleted() { // Final result is in. Do effects. if (result.status === 'success') { // set earlier if (!successfulHandshakeInjector) { @@ -580,19 +564,44 @@ handToHaptic(currentHand)); // don't change state (so animation continues while gripped) // but do send a notification, by calling the slot that emits the signal for it - Window.makeConnection(true, result.connection.new_connection ? - "You and " + result.connection.username + " are now connected!" : result.connection.username); - UserActivityLogger.makeUserConnection(connectingId, true, result.connection.new_connection ? - "new connection" : "already connected"); + Window.makeConnection(true, + result.connection.new_connection ? + "You and " + result.connection.username + " are now connected!" : + result.connection.username); + UserActivityLogger.makeUserConnection(connectingId, + true, + result.connection.new_connection ? + "new connection" : + "already connected"); return; } // failed endHandshake(); debug("failing with result data", result); // IWBNI we also did some fail sound/visual effect. Window.makeConnection(false, result.connection); + if (Account.isLoggedIn()) { // Give extra failure info + request(location.metaverseServerUrl + '/api/v1/users/' + Account.username + '/location', function (error, response) { + var message = ''; + if (error || response.status !== 'success') { + message = 'Unable to get location.'; + } else if (!response.data || !response.data.location) { + message = "Unexpected location value: " + JSON.stringify(response); + } else if (response.data.location.node_id !== cleanId(MyAvatar.sessionUUID)) { + message = 'Session identification does not match database. Maybe you are logged in on another machine? That would prevent handshakes.' + JSON.stringify(response) + MyAvatar.sessionUUID; + } + if (message) { + Window.makeConnection(false, message); + } + debug("account location:", message || 'ok'); + }); + } UserActivityLogger.makeUserConnection(connectingId, false, result.connection); } - var POLL_INTERVAL_MS = 200, POLL_LIMIT = 5; + // This is a bit fragile - but to account for skew in when people actually create the + // connection request, I've upped this to 2 seconds (plus the round-trip times) + // TODO: keep track of when the person we are connecting with is done, and don't stop + // until say 1 second after that. + var POLL_INTERVAL_MS = 200, POLL_LIMIT = 10; function handleConnectionResponseAndMaybeRepeat(error, response) { // If response is 'pending', set a short timeout to try again. // If we fail other than pending, set result and immediately call connectionRequestCompleted. @@ -602,7 +611,7 @@ debug(response, 'pollCount', pollCount); if (pollCount++ >= POLL_LIMIT) { // server will expire, but let's not wait that long. debug('POLL LIMIT REACHED; TIMEOUT: expired message generated by CLIENT'); - result = {status: 'error', connection: 'expired'}; + result = {status: 'error', connection: 'No logged-in partner found.'}; connectionRequestCompleted(); } else { // poll Script.setTimeout(function () { @@ -632,8 +641,6 @@ } } - // this should be where we make the appropriate connection call. For now just make the - // visualization change. function makeConnection(id) { // send done to let the connection know you have made connection. messageSend({ @@ -643,8 +650,7 @@ state = STATES.MAKING_CONNECTION; - // continue the haptic background until the timeout fires. When we make calls, we will have an interval - // probably, in which we do this. + // continue the haptic background until the timeout fires. Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, MAKING_CONNECTION_TIMEOUT, handToHaptic(currentHand)); requestBody = {'node_id': cleanId(MyAvatar.sessionUUID), 'proposed_node_id': cleanId(id)}; // for use when repeating @@ -658,26 +664,27 @@ // This will immediately set response if successful (e.g., the other guy got his request in first), // or immediate failure, and will otherwise poll (using the requestBody we just set). - request({ // + request({ uri: requestUrl, method: 'POST', json: true, body: {'user_connection_request': requestBody} }, handleConnectionResponseAndMaybeRepeat); } + function setupConnecting(id, jointIndex) { + connectingId = id; + connectingHandJointIndex = jointIndex; + } // we change states, start the connectionInterval where we check // to be sure the hand is still close enough. If not, we terminate // the interval, go back to the waiting state. If we make it // the entire CONNECTING_TIME, we make the connection. - function startConnecting(id, hand) { + function startConnecting(id, jointIndex) { var count = 0; - debug("connecting", id, "hand", hand); + debug("connecting", id, "hand", jointIndex); // do we need to do this? - connectingId = id; - connectingHandString = hand; - connectingHandJointIndex = AvatarList.getAvatarIdentifiers().indexOf(connectingId) !== -1 ? - getIdealHandJointIndex(AvatarList.getAvatar(connectingId), stringToHand(connectingHandString)) : -1; + setupConnecting(id, jointIndex); state = STATES.CONNECTING; // play sound @@ -692,10 +699,9 @@ } // send message that we are connecting with them - messageSend({ + handStringMessageSend({ key: "connecting", - id: id, - hand: handToString(currentHand) + id: id }); Controller.triggerHapticPulse(HAPTIC_DATA.initial.strength, HAPTIC_DATA.initial.duration, handToHaptic(currentHand)); @@ -706,7 +712,7 @@ if (state !== STATES.CONNECTING) { debug("stopping connecting interval, state changed"); stopConnecting(); - } else if (!isNearby(id, hand)) { + } else if (!isNearby()) { // gotta go back to waiting debug(id, "moved, back to waiting"); stopConnecting(); @@ -714,7 +720,7 @@ key: "done" }); startHandshake(); - } else if (count > CONNECTING_TIME/CONNECTING_INTERVAL) { + } else if (count > CONNECTING_TIME / CONNECTING_INTERVAL) { debug("made connection with " + id); makeConnection(id); stopConnecting(); @@ -740,140 +746,120 @@ | ---------- (done) ---------> | */ function messageHandler(channel, messageString, senderID) { + var message = {}; + function exisitingOrSearchedJointIndex() { // If this is a new connectingId, we'll need to find the jointIndex + return connectingId ? connectingHandJointIndex : getIdealHandJointIndex(AvatarList.getAvatar(senderID), message[HAND_STRING_PROPERTY]); + } if (channel !== MESSAGE_CHANNEL) { return; } if (MyAvatar.sessionUUID === senderID) { // ignore my own return; } - var message = {}; try { message = JSON.parse(messageString); } catch (e) { debug(e); } switch (message.key) { - case "waiting": - // add this guy to waiting object. Any other message from this person will - // remove it from the list - waitingList[senderID] = message.hand; - break; - case "connectionRequest": - delete waitingList[senderID]; - if (state === STATES.WAITING && message.id === MyAvatar.sessionUUID && - (!connectingId || connectingId === senderID)) { - // you were waiting for a connection request, so send the ack. Or, you and the other - // guy raced and both send connectionRequests. Handle that too - connectingId = senderID; - connectingHandString = message.hand; - connectingHandJointIndex = AvatarList.getAvatarIdentifiers().indexOf(connectingId) !== -1 ? - getIdealHandJointIndex(AvatarList.getAvatar(connectingId), stringToHand(connectingHandString)) : -1; - messageSend({ - key: "connectionAck", - id: senderID, - hand: handToString(currentHand) - }); - } else if (state === STATES.WAITING && connectingId === senderID) { - // the person you are trying to connect sent a request to someone else. See the - // if statement above. So, don't cry, just start the handshake over again + case "waiting": + // add this guy to waiting object. Any other message from this person will remove it from the list + waitingList[senderID] = getIdealHandJointIndex(AvatarList.getAvatar(senderID), message[HAND_STRING_PROPERTY]); + break; + case "connectionRequest": + delete waitingList[senderID]; + if (state === STATES.WAITING && message.id === MyAvatar.sessionUUID && (!connectingId || connectingId === senderID)) { + // you were waiting for a connection request, so send the ack. Or, you and the other + // guy raced and both send connectionRequests. Handle that too + setupConnecting(senderID, exisitingOrSearchedJointIndex()); + handStringMessageSend({ + key: "connectionAck", + id: senderID, + }); + } else if (state === STATES.WAITING && connectingId === senderID) { + // the person you are trying to connect sent a request to someone else. See the + // if statement above. So, don't cry, just start the handshake over again + startHandshake(); + } + break; + case "connectionAck": + delete waitingList[senderID]; + if (state === STATES.WAITING && (!connectingId || connectingId === senderID)) { + if (message.id === MyAvatar.sessionUUID) { + stopWaiting(); + startConnecting(senderID, exisitingOrSearchedJointIndex()); + } else if (connectingId) { + // this is for someone else (we lost race in connectionRequest), + // so lets start over startHandshake(); } - break; - case "connectionAck": - delete waitingList[senderID]; - if (state === STATES.WAITING && (!connectingId || connectingId === senderID)) { - if (message.id === MyAvatar.sessionUUID) { - // start connecting... - connectingId = senderID; - connectingHandString = message.hand; - connectingHandJointIndex = AvatarList.getAvatarIdentifiers().indexOf(connectingId) !== -1 ? - getIdealHandJointIndex(AvatarList.getAvatar(connectingId), stringToHand(connectingHandString)) : -1; - stopWaiting(); - startConnecting(senderID, connectingHandString); - } else if (connectingId) { - // this is for someone else (we lost race in connectionRequest), - // so lets start over + } + // TODO: check to see if we are waiting for this but the person we are connecting sent it to + // someone else, and try again + break; + case "connecting": + delete waitingList[senderID]; + if (state === STATES.WAITING && senderID === connectingId) { + if (message.id !== MyAvatar.sessionUUID) { + // the person we were trying to connect is connecting to someone else + // so try again + startHandshake(); + break; + } + startConnecting(senderID, connectingHandJointIndex); + } + break; + case "done": + delete waitingList[senderID]; + if (state === STATES.CONNECTING && connectingId === senderID) { + // if they are done, and didn't connect us, terminate our + // connecting + if (message.connectionId !== MyAvatar.sessionUUID) { + stopConnecting(); + // now just call startHandshake. Should be ok to do so without a + // value for isKeyboard, as we should not change the animation + // state anyways (if any) + startHandshake(); + } + } else { + // if waiting or inactive, lets clear the connecting id. If in makingConnection, + // do nothing + if (state !== STATES.MAKING_CONNECTION && connectingId === senderID) { + clearConnecting(); + if (state !== STATES.INACTIVE) { startHandshake(); } } - // TODO: check to see if we are waiting for this but the person we are connecting sent it to - // someone else, and try again - break; - case "connecting": - delete waitingList[senderID]; - if (state === STATES.WAITING && senderID === connectingId) { - // temporary logging - if (connectingHandString !== message.hand) { - debug("connecting hand", connectingHandString, "not same as connecting hand in message", message.hand); - } - connectingHandString = message.hand; - if (message.id !== MyAvatar.sessionUUID) { - // the person we were trying to connect is connecting to someone else - // so try again - startHandshake(); - break; - } - startConnecting(senderID, message.hand); - } - break; - case "done": - delete waitingList[senderID]; - if (state === STATES.CONNECTING && connectingId === senderID) { - // if they are done, and didn't connect us, terminate our - // connecting - if (message.connectionId !== MyAvatar.sessionUUID) { - stopConnecting(); - // now just call startHandshake. Should be ok to do so without a - // value for isKeyboard, as we should not change the animation - // state anyways (if any) - startHandshake(); - } - } else { - // if waiting or inactive, lets clear the connecting id. If in makingConnection, - // do nothing - if (state !== STATES.MAKING_CONNECTION && connectingId === senderID) { - connectingId = undefined; - connectingHandString = undefined; - connectingHandJointIndex = -1; - if (state !== STATES.INACTIVE) { - startHandshake(); - } - } - } - break; - default: - debug("unknown message", message); - break; + } + break; + default: + debug("unknown message", message); + break; } } Messages.subscribe(MESSAGE_CHANNEL); Messages.messageReceived.connect(messageHandler); - function makeGripHandler(hand, animate) { // determine if we are gripping or un-gripping if (animate) { - return function(value) { + return function (value) { updateTriggers(value, true, hand); }; - - } else { - return function (value) { - updateTriggers(value, false, hand); - }; } + return function (value) { + updateTriggers(value, false, hand); + }; } function keyPressEvent(event) { - if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && - !event.isAlt) { + if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { updateTriggers(1.0, true, Controller.Standard.RightHand); } } function keyReleaseEvent(event) { - if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && - !event.isAlt) { + if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { updateTriggers(0.0, true, Controller.Standard.RightHand); } } diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 006ef3f90f..6429d6e0c6 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -1,5 +1,6 @@ "use strict"; - +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Script, Settings, Window, Controller, Overlays, SoundArray, LODManager, MyAvatar, Tablet, Camera, HMD, Menu, Quat, Vec3*/ // // notifications.js // Version 0.801 @@ -56,613 +57,614 @@ // } // } -/* global Script, Controller, Overlays, SoundArray, Quat, Vec3, MyAvatar, Menu, HMD, AudioDevice, LODManager, Settings, Camera */ -(function() { // BEGIN LOCAL_SCOPE +(function () { // BEGIN LOCAL_SCOPE -Script.include("./libraries/soundArray.js"); + Script.include("./libraries/soundArray.js"); -var width = 340.0; //width of notification overlay -var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window -var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window -var buttonLocationX = overlayLocationX + (width - 28.0); -var locationY = 20.0; // position down from top of interface window -var topMargin = 13.0; -var leftMargin = 10.0; -var textColor = { red: 228, green: 228, blue: 228}; // text color -var backColor = { red: 2, green: 2, blue: 2}; // background color was 38,38,38 -var backgroundAlpha = 0; -var fontSize = 12.0; -var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades -var PERSIST_TIME_3D = 15.0; -var persistTime = PERSIST_TIME_2D; -var frame = 0; -var ourWidth = Window.innerWidth; -var ourHeight = Window.innerHeight; -var ctrlIsPressed = false; -var ready = true; -var MENU_NAME = 'Tools > Notifications'; -var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds"; -var NOTIFICATION_MENU_ITEM_POST = " Notifications"; -var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; -var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; -var lodTextID = false; + var width = 340.0; //width of notification overlay + var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window + var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window + var buttonLocationX = overlayLocationX + (width - 28.0); + var locationY = 20.0; // position down from top of interface window + var topMargin = 13.0; + var leftMargin = 10.0; + var textColor = { red: 228, green: 228, blue: 228}; // text color + var backColor = { red: 2, green: 2, blue: 2}; // background color was 38,38,38 + var backgroundAlpha = 0; + var fontSize = 12.0; + var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades + var PERSIST_TIME_3D = 15.0; + var persistTime = PERSIST_TIME_2D; + var frame = 0; + var ctrlIsPressed = false; + var ready = true; + var MENU_NAME = 'Tools > Notifications'; + var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds"; + var NOTIFICATION_MENU_ITEM_POST = " Notifications"; + var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; + var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; + var lodTextID = false; -var NotificationType = { - UNKNOWN: 0, - SNAPSHOT: 1, - LOD_WARNING: 2, - CONNECTION_REFUSED: 3, - EDIT_ERROR: 4, - TABLET: 5, - CONNECTION: 6, - properties: [ - { text: "Snapshot" }, - { text: "Level of Detail" }, - { text: "Connection Refused" }, - { text: "Edit error" }, - { text: "Tablet" }, - { text: "Connection" } - ], - getTypeFromMenuItem: function(menuItemName) { - if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { - return NotificationType.UNKNOWN; - } - var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length); - for (var type in this.properties) { - if (this.properties[type].text === preMenuItemName) { - return parseInt(type) + 1; + var NotificationType = { + UNKNOWN: 0, + SNAPSHOT: 1, + LOD_WARNING: 2, + CONNECTION_REFUSED: 3, + EDIT_ERROR: 4, + TABLET: 5, + CONNECTION: 6, + properties: [ + { text: "Snapshot" }, + { text: "Level of Detail" }, + { text: "Connection Refused" }, + { text: "Edit error" }, + { text: "Tablet" }, + { text: "Connection" } + ], + getTypeFromMenuItem: function (menuItemName) { + var type; + if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { + return NotificationType.UNKNOWN; } + var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length); + for (type in this.properties) { + if (this.properties[type].text === preMenuItemName) { + return parseInt(type, 10) + 1; + } + } + return NotificationType.UNKNOWN; + }, + getMenuString: function (type) { + return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST; } - return NotificationType.UNKNOWN; - }, - getMenuString: function(type) { - return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST; - } -}; - -var randomSounds = new SoundArray({ localOnly: true }, true); -var numberOfSounds = 2; -for (var i = 1; i <= numberOfSounds; i++) { - randomSounds.addSound(Script.resolvePath("assets/sounds/notification-general"+ i + ".raw")); -} - -var notifications = []; -var buttons = []; -var times = []; -var heights = []; -var myAlpha = []; -var arrays = []; -var isOnHMD = false, - NOTIFICATIONS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. - NOTIFICATIONS_3D_DISTANCE = 0.6, // Horizontal distance from avatar position. - NOTIFICATIONS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. - NOTIFICATIONS_3D_YAW = 0.0, // Degrees relative to notifications direction. - NOTIFICATIONS_3D_PITCH = -60.0, // Degrees from vertical. - NOTIFICATION_3D_SCALE = 0.002, // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions. - NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE, // Need a little more room for button in 3D. - overlay3DDetails = []; - -// push data from above to the 2 dimensional array -function createArrays(notice, button, createTime, height, myAlpha) { - arrays.push([notice, button, createTime, height, myAlpha]); -} - -// This handles the final dismissal of a notification after fading -function dismiss(firstNoteOut, firstButOut, firstOut) { - if (firstNoteOut == lodTextID) { - lodTextID = false; - } - - Overlays.deleteOverlay(firstNoteOut); - Overlays.deleteOverlay(firstButOut); - notifications.splice(firstOut, 1); - buttons.splice(firstOut, 1); - times.splice(firstOut, 1); - heights.splice(firstOut, 1); - myAlpha.splice(firstOut, 1); - overlay3DDetails.splice(firstOut, 1); -} - -function fadeIn(noticeIn, buttonIn) { - var q = 0, - qFade, - pauseTimer = null; - - pauseTimer = Script.setInterval(function () { - q += 1; - qFade = q / 10.0; - Overlays.editOverlay(noticeIn, { alpha: qFade }); - Overlays.editOverlay(buttonIn, { alpha: qFade }); - if (q >= 9.0) { - Script.clearInterval(pauseTimer); - } - }, 10); -} - -// this fades the notification ready for dismissal, and removes it from the arrays -function fadeOut(noticeOut, buttonOut, arraysOut) { - var r = 9.0, - rFade, - pauseTimer = null; - - pauseTimer = Script.setInterval(function () { - r -= 1; - rFade = r / 10.0; - Overlays.editOverlay(noticeOut, { alpha: rFade }); - Overlays.editOverlay(buttonOut, { alpha: rFade }); - if (r < 0) { - dismiss(noticeOut, buttonOut, arraysOut); - arrays.splice(arraysOut, 1); - ready = true; - Script.clearInterval(pauseTimer); - } - }, 20); -} - -function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { - // Calculates overlay positions and orientations in avatar coordinates. - var noticeY, - originOffset, - notificationOrientation, - notificationPosition, - buttonPosition; - - // Notification plane positions - noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2; - notificationPosition = { x: 0, y: noticeY, z: 0 }; - buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 }; - - // Rotate plane - notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, - NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0); - notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition); - buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition); - - // Translate plane - originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0), - { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE }); - originOffset.y += NOTIFICATIONS_3D_ELEVATION; - notificationPosition = Vec3.sum(originOffset, notificationPosition); - buttonPosition = Vec3.sum(originOffset, buttonPosition); - - return { - notificationOrientation: notificationOrientation, - notificationPosition: notificationPosition, - buttonPosition: buttonPosition }; -} -// Pushes data to each array and sets up data for 2nd dimension array -// to handle auxiliary data not carried by the overlay class -// specifically notification "heights", "times" of creation, and . -function notify(notice, button, height, imageProperties, image) { - var notificationText, - noticeWidth, - noticeHeight, - positions, - last; + var randomSounds = new SoundArray({ localOnly: true }, true); + var numberOfSounds = 2; + var soundIndex; + for (soundIndex = 1; soundIndex <= numberOfSounds; soundIndex++) { + randomSounds.addSound(Script.resolvePath("assets/sounds/notification-general" + soundIndex + ".raw")); + } - if (isOnHMD) { - // Calculate 3D values from 2D overlay properties. + var notifications = []; + var buttons = []; + var times = []; + var heights = []; + var myAlpha = []; + var arrays = []; + var isOnHMD = false, + NOTIFICATIONS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. + NOTIFICATIONS_3D_DISTANCE = 0.6, // Horizontal distance from avatar position. + NOTIFICATIONS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. + NOTIFICATIONS_3D_YAW = 0.0, // Degrees relative to notifications direction. + NOTIFICATIONS_3D_PITCH = -60.0, // Degrees from vertical. + NOTIFICATION_3D_SCALE = 0.002, // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions. + NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE, // Need a little more room for button in 3D. + overlay3DDetails = []; - noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; - noticeHeight = notice.height * NOTIFICATION_3D_SCALE; + // push data from above to the 2 dimensional array + function createArrays(notice, button, createTime, height, myAlpha) { + arrays.push([notice, button, createTime, height, myAlpha]); + } - notice.size = { x: noticeWidth, y: noticeHeight }; - - positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); - - notice.parentID = MyAvatar.sessionUUID; - notice.parentJointIndex = -2; - - if (!image) { - notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; - notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; - notice.bottomMargin = 0; - notice.rightMargin = 0; - notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; - notice.isFacingAvatar = false; - - notificationText = Overlays.addOverlay("text3d", notice); - notifications.push(notificationText); - } else { - notifications.push(Overlays.addOverlay("image3d", notice)); + // This handles the final dismissal of a notification after fading + function dismiss(firstNoteOut, firstButOut, firstOut) { + if (firstNoteOut === lodTextID) { + lodTextID = false; } - button.url = button.imageURL; - button.scale = button.width * NOTIFICATION_3D_SCALE; - button.isFacingAvatar = false; - button.parentID = MyAvatar.sessionUUID; - button.parentJointIndex = -2; + Overlays.deleteOverlay(firstNoteOut); + Overlays.deleteOverlay(firstButOut); + notifications.splice(firstOut, 1); + buttons.splice(firstOut, 1); + times.splice(firstOut, 1); + heights.splice(firstOut, 1); + myAlpha.splice(firstOut, 1); + overlay3DDetails.splice(firstOut, 1); + } - buttons.push((Overlays.addOverlay("image3d", button))); - overlay3DDetails.push({ - notificationOrientation: positions.notificationOrientation, - notificationPosition: positions.notificationPosition, - buttonPosition: positions.buttonPosition, - width: noticeWidth, - height: noticeHeight - }); + function fadeIn(noticeIn, buttonIn) { + var q = 0, + qFade, + pauseTimer = null; + pauseTimer = Script.setInterval(function () { + q += 1; + qFade = q / 10.0; + Overlays.editOverlay(noticeIn, { alpha: qFade }); + Overlays.editOverlay(buttonIn, { alpha: qFade }); + if (q >= 9.0) { + Script.clearInterval(pauseTimer); + } + }, 10); + } - var defaultEyePosition, - avatarOrientation, - notificationPosition, + // this fades the notification ready for dismissal, and removes it from the arrays + function fadeOut(noticeOut, buttonOut, arraysOut) { + var r = 9.0, + rFade, + pauseTimer = null; + + pauseTimer = Script.setInterval(function () { + r -= 1; + rFade = Math.max(0.0, r / 10.0); + Overlays.editOverlay(noticeOut, { alpha: rFade }); + Overlays.editOverlay(buttonOut, { alpha: rFade }); + if (r <= 0) { + dismiss(noticeOut, buttonOut, arraysOut); + arrays.splice(arraysOut, 1); + ready = true; + Script.clearInterval(pauseTimer); + } + }, 20); + } + + function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { + // Calculates overlay positions and orientations in avatar coordinates. + var noticeY, + originOffset, notificationOrientation, + notificationPosition, buttonPosition; - if (isOnHMD && notifications.length > 0) { - // Update 3D overlays to maintain positions relative to avatar - defaultEyePosition = MyAvatar.getDefaultEyePosition(); - avatarOrientation = MyAvatar.orientation; + // Notification plane positions + noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2; + notificationPosition = { x: 0, y: noticeY, z: 0 }; + buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 }; - for (i = 0; i < notifications.length; i += 1) { - notificationPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, - overlay3DDetails[i].notificationPosition)); - notificationOrientation = Quat.multiply(avatarOrientation, - overlay3DDetails[i].notificationOrientation); - buttonPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, - overlay3DDetails[i].buttonPosition)); - Overlays.editOverlay(notifications[i], { position: notificationPosition, - rotation: notificationOrientation }); - Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation }); - } - } + // Rotate plane + notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, + NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0); + notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition); + buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition); - } else { - if (!image) { - notificationText = Overlays.addOverlay("text", notice); - notifications.push((notificationText)); - } else { - notifications.push(Overlays.addOverlay("image", notice)); - } - buttons.push(Overlays.addOverlay("image", button)); + // Translate plane + originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0), + { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE }); + originOffset.y += NOTIFICATIONS_3D_ELEVATION; + notificationPosition = Vec3.sum(originOffset, notificationPosition); + buttonPosition = Vec3.sum(originOffset, buttonPosition); + + return { + notificationOrientation: notificationOrientation, + notificationPosition: notificationPosition, + buttonPosition: buttonPosition + }; } - height = height + 1.0; - heights.push(height); - times.push(new Date().getTime() / 1000); - last = notifications.length - 1; - myAlpha.push(notifications[last].alpha); - createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); - fadeIn(notifications[last], buttons[last]); + // Pushes data to each array and sets up data for 2nd dimension array + // to handle auxiliary data not carried by the overlay class + // specifically notification "heights", "times" of creation, and . + function notify(notice, button, height, imageProperties, image) { + var notificationText, + noticeWidth, + noticeHeight, + positions, + last; - if (imageProperties && !image) { - var imageHeight = notice.width / imageProperties.aspectRatio; - notice = { - x: notice.x, - y: notice.y + height, - width: notice.width, - height: imageHeight, - subImage: { x: 0, y: 0 }, + if (isOnHMD) { + // Calculate 3D values from 2D overlay properties. + + noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; + noticeHeight = notice.height * NOTIFICATION_3D_SCALE; + + notice.size = { x: noticeWidth, y: noticeHeight }; + + positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); + + notice.parentID = MyAvatar.sessionUUID; + notice.parentJointIndex = -2; + + if (!image) { + notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; + notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; + notice.bottomMargin = 0; + notice.rightMargin = 0; + notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; + notice.isFacingAvatar = false; + + notificationText = Overlays.addOverlay("text3d", notice); + notifications.push(notificationText); + } else { + notifications.push(Overlays.addOverlay("image3d", notice)); + } + + button.url = button.imageURL; + button.scale = button.width * NOTIFICATION_3D_SCALE; + button.isFacingAvatar = false; + button.parentID = MyAvatar.sessionUUID; + button.parentJointIndex = -2; + + buttons.push((Overlays.addOverlay("image3d", button))); + overlay3DDetails.push({ + notificationOrientation: positions.notificationOrientation, + notificationPosition: positions.notificationPosition, + buttonPosition: positions.buttonPosition, + width: noticeWidth, + height: noticeHeight + }); + + + var defaultEyePosition, + avatarOrientation, + notificationPosition, + notificationOrientation, + buttonPosition, + notificationIndex; + + if (isOnHMD && notifications.length > 0) { + // Update 3D overlays to maintain positions relative to avatar + defaultEyePosition = MyAvatar.getDefaultEyePosition(); + avatarOrientation = MyAvatar.orientation; + + for (notificationIndex = 0; notificationIndex < notifications.length; notificationIndex += 1) { + notificationPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[notificationIndex].notificationPosition)); + notificationOrientation = Quat.multiply(avatarOrientation, + overlay3DDetails[notificationIndex].notificationOrientation); + buttonPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[notificationIndex].buttonPosition)); + Overlays.editOverlay(notifications[notificationIndex], { position: notificationPosition, + rotation: notificationOrientation }); + Overlays.editOverlay(buttons[notificationIndex], { position: buttonPosition, rotation: notificationOrientation }); + } + } + + } else { + if (!image) { + notificationText = Overlays.addOverlay("text", notice); + notifications.push(notificationText); + } else { + notifications.push(Overlays.addOverlay("image", notice)); + } + buttons.push(Overlays.addOverlay("image", button)); + } + + height = height + 1.0; + heights.push(height); + times.push(new Date().getTime() / 1000); + last = notifications.length - 1; + myAlpha.push(notifications[last].alpha); + createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); + fadeIn(notifications[last], buttons[last]); + + if (imageProperties && !image) { + var imageHeight = notice.width / imageProperties.aspectRatio; + notice = { + x: notice.x, + y: notice.y + height, + width: notice.width, + height: imageHeight, + subImage: { x: 0, y: 0 }, + color: { red: 255, green: 255, blue: 255}, + visible: true, + imageURL: imageProperties.path, + alpha: backgroundAlpha + }; + notify(notice, button, imageHeight, imageProperties, true); + } + + return notificationText; + } + + var CLOSE_NOTIFICATION_ICON = Script.resolvePath("assets/images/close-small-light.svg"); + + // This function creates and sizes the overlays + function createNotification(text, notificationType, imageProperties) { + var count = (text.match(/\n/g) || []).length, + breakPoint = 43.0, // length when new line is added + extraLine = 0, + breaks = 0, + height = 40.0, + stack = 0, + level, + noticeProperties, + bLevel, + buttonProperties, + i; + + if (text.length >= breakPoint) { + breaks = count; + } + extraLine = breaks * 16.0; + for (i = 0; i < heights.length; i += 1) { + stack = stack + heights[i]; + } + + level = (stack + 20.0); + height = height + extraLine; + + noticeProperties = { + x: overlayLocationX, + y: level, + width: width, + height: height, + color: textColor, + backgroundColor: backColor, + alpha: backgroundAlpha, + topMargin: topMargin, + leftMargin: leftMargin, + font: {size: fontSize}, + text: text + }; + + bLevel = level + 12.0; + buttonProperties = { + x: buttonLocationX, + y: bLevel, + width: 10.0, + height: 10.0, + subImage: { x: 0, y: 0, width: 10, height: 10 }, + imageURL: CLOSE_NOTIFICATION_ICON, color: { red: 255, green: 255, blue: 255}, visible: true, - imageURL: imageProperties.path, alpha: backgroundAlpha }; - notify(notice, button, imageHeight, imageProperties, true); + + if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && + Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { + randomSounds.playRandom(); + } + + return notify(noticeProperties, buttonProperties, height, imageProperties); } - return notificationText; -} - -var CLOSE_NOTIFICATION_ICON = Script.resolvePath("assets/images/close-small-light.svg"); - -// This function creates and sizes the overlays -function createNotification(text, notificationType, imageProperties) { - var count = (text.match(/\n/g) || []).length, - breakPoint = 43.0, // length when new line is added - extraLine = 0, - breaks = 0, - height = 40.0, - stack = 0, - level, - noticeProperties, - bLevel, - buttonProperties, - i; - - if (text.length >= breakPoint) { - breaks = count; - } - extraLine = breaks * 16.0; - for (i = 0; i < heights.length; i += 1) { - stack = stack + heights[i]; + function deleteNotification(index) { + var notificationTextID = notifications[index]; + if (notificationTextID === lodTextID) { + lodTextID = false; + } + Overlays.deleteOverlay(notificationTextID); + Overlays.deleteOverlay(buttons[index]); + notifications.splice(index, 1); + buttons.splice(index, 1); + times.splice(index, 1); + heights.splice(index, 1); + myAlpha.splice(index, 1); + overlay3DDetails.splice(index, 1); + arrays.splice(index, 1); } - level = (stack + 20.0); - height = height + extraLine; - noticeProperties = { - x: overlayLocationX, - y: level, - width: width, - height: height, - color: textColor, - backgroundColor: backColor, - alpha: backgroundAlpha, - topMargin: topMargin, - leftMargin: leftMargin, - font: {size: fontSize}, - text: text - }; - - bLevel = level + 12.0; - buttonProperties = { - x: buttonLocationX, - y: bLevel, - width: 10.0, - height: 10.0, - subImage: { x: 0, y: 0, width: 10, height: 10 }, - imageURL: CLOSE_NOTIFICATION_ICON, - color: { red: 255, green: 255, blue: 255}, - visible: true, - alpha: backgroundAlpha - }; - - if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && - Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { - randomSounds.playRandom(); - } - - return notify(noticeProperties, buttonProperties, height, imageProperties); -} - -function deleteNotification(index) { - var notificationTextID = notifications[index]; - if (notificationTextID == lodTextID) { - lodTextID = false; - } - Overlays.deleteOverlay(notificationTextID); - Overlays.deleteOverlay(buttons[index]); - notifications.splice(index, 1); - buttons.splice(index, 1); - times.splice(index, 1); - heights.splice(index, 1); - myAlpha.splice(index, 1); - overlay3DDetails.splice(index, 1); - arrays.splice(index, 1); -} - - -// Trims extra whitespace and breaks into lines of length no more than MAX_LENGTH, breaking at spaces. Trims extra whitespace. -var MAX_LENGTH = 42; -function wordWrap(string) { - var finishedLines = [], currentLine = ''; - string.split(/\s/).forEach(function (word) { - var tail = currentLine ? ' ' + word : word; - if ((currentLine.length + tail.length) <= MAX_LENGTH) { - currentLine += tail; - } else { + // Trims extra whitespace and breaks into lines of length no more than MAX_LENGTH, breaking at spaces. Trims extra whitespace. + var MAX_LENGTH = 42; + function wordWrap(string) { + var finishedLines = [], currentLine = ''; + string.split(/\s/).forEach(function (word) { + var tail = currentLine ? ' ' + word : word; + if ((currentLine.length + tail.length) <= MAX_LENGTH) { + currentLine += tail; + } else { + finishedLines.push(currentLine); + currentLine = word; + } + }); + if (currentLine) { finishedLines.push(currentLine); - currentLine = word; } - }); - if (currentLine) { - finishedLines.push(currentLine); - } - return finishedLines.join('\n'); -} - -function update() { - var nextOverlay, - noticeOut, - buttonOut, - arraysOut, - positions, - i, - j, - k; - - if (isOnHMD !== HMD.active) { - while (arrays.length > 0) { - deleteNotification(0); - } - isOnHMD = !isOnHMD; - persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D; - return; + return finishedLines.join('\n'); } - frame += 1; - if ((frame % 60.0) === 0) { // only update once a second - locationY = 20.0; - for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade - nextOverlay = Overlays.getOverlayAtPoint({ x: overlayLocationX, y: locationY }); - Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); - Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); - if (isOnHMD) { - positions = calculate3DOverlayPositions(overlay3DDetails[i].width, - overlay3DDetails[i].height, locationY); - overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; - overlay3DDetails[i].notificationPosition = positions.notificationPosition; - overlay3DDetails[i].buttonPosition = positions.buttonPosition; + function update() { + var noticeOut, + buttonOut, + arraysOut, + positions, + i, + j, + k; + + if (isOnHMD !== HMD.active) { + while (arrays.length > 0) { + deleteNotification(0); } - locationY = locationY + arrays[i][3]; + isOnHMD = !isOnHMD; + persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D; + return; } - } - // This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) - for (i = 0; i < arrays.length; i += 1) { - if (ready) { - j = arrays[i][2]; - k = j + persistTime; - if (k < (new Date().getTime() / 1000)) { - ready = false; - noticeOut = arrays[i][0]; - buttonOut = arrays[i][1]; - arraysOut = i; - fadeOut(noticeOut, buttonOut, arraysOut); + frame += 1; + if ((frame % 60.0) === 0) { // only update once a second + locationY = 20.0; + for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade + Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); + Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); + if (isOnHMD) { + positions = calculate3DOverlayPositions(overlay3DDetails[i].width, + overlay3DDetails[i].height, locationY); + overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; + overlay3DDetails[i].notificationPosition = positions.notificationPosition; + overlay3DDetails[i].buttonPosition = positions.buttonPosition; + } + locationY = locationY + arrays[i][3]; + } + } + + // This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) + for (i = 0; i < arrays.length; i += 1) { + if (ready) { + j = arrays[i][2]; + k = j + persistTime; + if (k < (new Date().getTime() / 1000)) { + ready = false; + noticeOut = arrays[i][0]; + buttonOut = arrays[i][1]; + arraysOut = i; + fadeOut(noticeOut, buttonOut, arraysOut); + } } } } -} -var STARTUP_TIMEOUT = 500, // ms - startingUp = true, - startupTimer = null; + var STARTUP_TIMEOUT = 500, // ms + startingUp = true, + startupTimer = null; -function finishStartup() { - startingUp = false; - Script.clearTimeout(startupTimer); -} + function finishStartup() { + startingUp = false; + Script.clearTimeout(startupTimer); + } -function isStartingUp() { - // Is starting up until get no checks that it is starting up for STARTUP_TIMEOUT - if (startingUp) { - if (startupTimer) { - Script.clearTimeout(startupTimer); + function isStartingUp() { + // Is starting up until get no checks that it is starting up for STARTUP_TIMEOUT + if (startingUp) { + if (startupTimer) { + Script.clearTimeout(startupTimer); + } + startupTimer = Script.setTimeout(finishStartup, STARTUP_TIMEOUT); } - startupTimer = Script.setTimeout(finishStartup, STARTUP_TIMEOUT); - } - return startingUp; -} - -function onDomainConnectionRefused(reason) { - createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); -} - -function onEditError(msg) { - createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); -} - -function onNotify(msg) { - createNotification(wordWrap(msg), NotificationType.UNKNOWN); // Needs a generic notification system for user feedback, thus using this -} - -function onSnapshotTaken(pathStillSnapshot, notify) { - if (notify) { - var imageProperties = { - path: "file:///" + pathStillSnapshot, - aspectRatio: Window.innerWidth / Window.innerHeight - }; - createNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), NotificationType.SNAPSHOT, imageProperties); - } -} - -function tabletNotification() { - createNotification("Tablet needs your attention", NotificationType.TABLET); -} - -function processingGif() { - createNotification("Processing GIF snapshot...", NotificationType.SNAPSHOT); -} - -function connectionAdded(connectionName) { - createNotification(connectionName, NotificationType.CONNECTION); -} - -function connectionError(error) { - createNotification(wordWrap("Error trying to make connection: " + error), NotificationType.CONNECTION); -} - -// handles mouse clicks on buttons -function mousePressEvent(event) { - var pickRay, - clickedOverlay, - i; - - if (isOnHMD) { - pickRay = Camera.computePickRay(event.x, event.y); - clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID; - } else { - clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + return startingUp; } - for (i = 0; i < buttons.length; i += 1) { - if (clickedOverlay === buttons[i]) { - deleteNotification(i); + function onDomainConnectionRefused(reason) { + createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); + } + + function onEditError(msg) { + createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); + } + + function onNotify(msg) { + createNotification(wordWrap(msg), NotificationType.UNKNOWN); // Needs a generic notification system for user feedback, thus using this + } + + function onSnapshotTaken(pathStillSnapshot, notify) { + if (notify) { + var imageProperties = { + path: "file:///" + pathStillSnapshot, + aspectRatio: Window.innerWidth / Window.innerHeight + }; + createNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), NotificationType.SNAPSHOT, imageProperties); } } -} -// Control key remains active only while key is held down -function keyReleaseEvent(key) { - if (key.key === 16777249) { - ctrlIsPressed = false; + function tabletNotification() { + createNotification("Tablet needs your attention", NotificationType.TABLET); } -} -// Triggers notification on specific key driven events -function keyPressEvent(key) { - if (key.key === 16777249) { - ctrlIsPressed = true; + function processingGif() { + createNotification("Processing GIF snapshot...", NotificationType.SNAPSHOT); } -} -function setup() { - Menu.addMenu(MENU_NAME); - var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING); - checked = checked === '' ? true : checked; - Menu.addMenuItem({ - menuName: MENU_NAME, - menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM, - isCheckable: true, - isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING) - }); - Menu.addSeparator(MENU_NAME, "Play sounds for:"); - for (var type in NotificationType.properties) { - checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type) + 1)); + function connectionAdded(connectionName) { + createNotification(connectionName, NotificationType.CONNECTION); + } + + function connectionError(error) { + createNotification(wordWrap("Error trying to make connection: " + error), NotificationType.CONNECTION); + } + + // handles mouse clicks on buttons + function mousePressEvent(event) { + var pickRay, + clickedOverlay, + i; + + if (isOnHMD) { + pickRay = Camera.computePickRay(event.x, event.y); + clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID; + } else { + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + } + + for (i = 0; i < buttons.length; i += 1) { + if (clickedOverlay === buttons[i]) { + deleteNotification(i); + } + } + } + + // Control key remains active only while key is held down + function keyReleaseEvent(key) { + if (key.key === 16777249) { + ctrlIsPressed = false; + } + } + + // Triggers notification on specific key driven events + function keyPressEvent(key) { + if (key.key === 16777249) { + ctrlIsPressed = true; + } + } + + function setup() { + var type; + Menu.addMenu(MENU_NAME); + var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING); checked = checked === '' ? true : checked; Menu.addMenuItem({ menuName: MENU_NAME, - menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST, + menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM, isCheckable: true, - isChecked: checked + isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING) }); + Menu.addSeparator(MENU_NAME, "Play sounds for:"); + for (type in NotificationType.properties) { + checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type, 10) + 1)); + checked = checked === '' ? true : checked; + Menu.addMenuItem({ + menuName: MENU_NAME, + menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST, + isCheckable: true, + isChecked: checked + }); + } } -} -// When our script shuts down, we should clean up all of our overlays -function scriptEnding() { - for (var i = 0; i < notifications.length; i++) { - Overlays.deleteOverlay(notifications[i]); - Overlays.deleteOverlay(buttons[i]); + // When our script shuts down, we should clean up all of our overlays + function scriptEnding() { + var notificationIndex; + for (notificationIndex = 0; notificationIndex < notifications.length; notificationIndex++) { + Overlays.deleteOverlay(notifications[notificationIndex]); + Overlays.deleteOverlay(buttons[notificationIndex]); + } + Menu.removeMenu(MENU_NAME); } - Menu.removeMenu(MENU_NAME); -} -function menuItemEvent(menuItem) { - if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) { - Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM)); - return; + function menuItemEvent(menuItem) { + if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM)); + return; + } + var notificationType = NotificationType.getTypeFromMenuItem(menuItem); + if (notificationType !== notificationType.UNKNOWN) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem)); + } } - var notificationType = NotificationType.getTypeFromMenuItem(menuItem); - if (notificationType !== notificationType.UNKNOWN) { - Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem)); - } -} -LODManager.LODDecreased.connect(function() { - var warningText = "\n" + - "Due to the complexity of the content, the \n" + - "level of detail has been decreased. " + - "You can now see: \n" + - LODManager.getLODFeedbackText(); + LODManager.LODDecreased.connect(function () { + var warningText = "\n" + + "Due to the complexity of the content, the \n" + + "level of detail has been decreased. " + + "You can now see: \n" + + LODManager.getLODFeedbackText(); - if (lodTextID === false) { - lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); - } else { - Overlays.editOverlay(lodTextID, { text: warningText }); - } -}); + if (lodTextID === false) { + lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); + } else { + Overlays.editOverlay(lodTextID, { text: warningText }); + } + }); -Controller.keyPressEvent.connect(keyPressEvent); -Controller.mousePressEvent.connect(mousePressEvent); -Controller.keyReleaseEvent.connect(keyReleaseEvent); -Script.update.connect(update); -Script.scriptEnding.connect(scriptEnding); -Menu.menuItemEvent.connect(menuItemEvent); -Window.domainConnectionRefused.connect(onDomainConnectionRefused); -Window.stillSnapshotTaken.connect(onSnapshotTaken); -Window.processingGifStarted.connect(processingGif); -Window.connectionAdded.connect(connectionAdded); -Window.connectionError.connect(connectionError); -Window.notifyEditError = onEditError; -Window.notify = onNotify; -Tablet.tabletNotification.connect(tabletNotification); -setup(); + Controller.keyPressEvent.connect(keyPressEvent); + Controller.mousePressEvent.connect(mousePressEvent); + Controller.keyReleaseEvent.connect(keyReleaseEvent); + Script.update.connect(update); + Script.scriptEnding.connect(scriptEnding); + Menu.menuItemEvent.connect(menuItemEvent); + Window.domainConnectionRefused.connect(onDomainConnectionRefused); + Window.stillSnapshotTaken.connect(onSnapshotTaken); + Window.processingGifStarted.connect(processingGif); + Window.connectionAdded.connect(connectionAdded); + Window.connectionError.connect(connectionError); + Window.announcement.connect(onNotify); + Window.notifyEditError = onEditError; + Window.notify = onNotify; + Tablet.tabletNotification.connect(tabletNotification); + setup(); }()); // END LOCAL_SCOPE diff --git a/scripts/system/pal.js b/scripts/system/pal.js index ae64065216..4a6b8d4142 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -723,7 +723,6 @@ function startup() { activeIcon: "icons/tablet-icons/people-a.svg", sortOrder: 7 }); - tablet.fromQml.connect(fromQml); button.clicked.connect(onTabletButtonClicked); tablet.screenChanged.connect(onTabletScreenChanged); Users.usernameFromIDReply.connect(usernameFromIDReply); @@ -789,8 +788,23 @@ function onTabletButtonClicked() { audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS); } } +var hasEventBridge = false; +function wireEventBridge(on) { + if (on) { + if (!hasEventBridge) { + tablet.fromQml.connect(fromQml); + hasEventBridge = true; + } + } else { + if (hasEventBridge) { + tablet.fromQml.disconnect(fromQml); + hasEventBridge = false; + } + } +} function onTabletScreenChanged(type, url) { + wireEventBridge(shouldActivateButton); // for toolbar mode: change button to active when window is first openend, false otherwise. button.editProperties({isActive: shouldActivateButton}); shouldActivateButton = false; diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 6039bc09c1..1cc24b8265 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -7,7 +7,7 @@ // Distributed under the Apache License, Version 2.0 // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* globals Tablet, Script, HMD, Settings, DialogsManager, Menu, Reticle, OverlayWebWindow, Desktop, Account, MyAvatar */ +/* globals Tablet, Script, HMD, Settings, DialogsManager, Menu, Reticle, OverlayWebWindow, Desktop, Account, MyAvatar, Snapshot */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE @@ -24,28 +24,79 @@ var buttonConnected = false; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ icon: "icons/tablet-icons/snap-i.svg", + activeIcon: "icons/tablet-icons/snap-a.svg", text: buttonName, sortOrder: 5 }); -function shouldOpenFeedAfterShare() { - var persisted = Settings.getValue('openFeedAfterShare', true); // might answer true, false, "true", or "false" - return persisted && (persisted !== 'false'); +var snapshotOptions; +var imageData = []; +var storyIDsToMaybeDelete = []; +var shareAfterLogin = false; +var snapshotToShareAfterLogin; +var METAVERSE_BASE = location.metaverseServerUrl; + +// It's totally unnecessary to return to C++ to perform many of these requests, such as DELETEing an old story, +// POSTING a new one, PUTTING a new audience, or GETTING story data. It's far more efficient to do all of that within JS +function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request. + var httpRequest = new XMLHttpRequest(), key; + // QT bug: apparently doesn't handle onload. Workaround using readyState. + httpRequest.onreadystatechange = function () { + var READY_STATE_DONE = 4; + var HTTP_OK = 200; + if (httpRequest.readyState >= READY_STATE_DONE) { + var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText, + response = !error && httpRequest.responseText, + contentType = !error && httpRequest.getResponseHeader('content-type'); + if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc. + try { + response = JSON.parse(response); + } catch (e) { + error = e; + } + } + callback(error, response); + } + }; + if (typeof options === 'string') { + options = { uri: options }; + } + if (options.url) { + options.uri = options.url; + } + if (!options.method) { + options.method = 'GET'; + } + if (options.body && (options.method === 'GET')) { // add query parameters + var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&'; + for (key in options.body) { + params.push(key + '=' + options.body[key]); + } + options.uri += appender + params.join('&'); + delete options.body; + } + if (options.json) { + options.headers = options.headers || {}; + options.headers["Content-type"] = "application/json"; + options.body = JSON.stringify(options.body); + } + for (key in options.headers || {}) { + httpRequest.setRequestHeader(key, options.headers[key]); + } + httpRequest.open(options.method, options.uri, true); + httpRequest.send(options.body); } -function showFeedWindow() { - if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar")) - || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))) { - tablet.loadQMLSource("TabletAddressDialog.qml"); + +function openLoginWindow() { + if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false)) + || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) { + Menu.triggerOption("Login / Sign Up"); } else { - tablet.initialScreen("TabletAddressDialog.qml"); + tablet.loadQMLOnTop("../../dialogs/TabletLoginDialog.qml"); HMD.openTablet(); } } -var outstanding; -var readyData; -var shareAfterLogin = false; -var snapshotToShareAfterLogin; function onMessage(message) { // Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following: // 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.) @@ -58,91 +109,257 @@ function onMessage(message) { } var isLoggedIn; - var needsLogin = false; switch (message.action) { - case 'ready': // Send it. + case 'ready': // DOM is ready and page has loaded tablet.emitScriptEvent(JSON.stringify({ type: "snapshot", - action: readyData + action: "captureSettings", + setting: Settings.getValue("alsoTakeAnimatedSnapshot", true) })); - outstanding = 0; + if (Snapshot.getSnapshotsLocation() !== "") { + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "showPreviousImages", + options: snapshotOptions, + image_data: imageData, + canShare: !isDomainOpen(Settings.getValue("previousSnapshotDomainID")) + })); + } else { + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "showSetupInstructions" + })); + Settings.setValue("previousStillSnapPath", ""); + Settings.setValue("previousStillSnapStoryID", ""); + Settings.setValue("previousStillSnapSharingDisabled", false); + Settings.setValue("previousAnimatedSnapPath", ""); + Settings.setValue("previousAnimatedSnapStoryID", ""); + Settings.setValue("previousAnimatedSnapSharingDisabled", false); + } + break; + case 'chooseSnapshotLocation': + var snapshotPath = Window.browseDir("Choose Snapshots Directory", "", ""); + + if (snapshotPath) { // not cancelled + Snapshot.setSnapshotsLocation(snapshotPath); + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "snapshotLocationChosen" + })); + } break; case 'openSettings': - if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar")) - || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))) { + if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false)) + || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) { Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "General Preferences"); } else { tablet.loadQMLOnTop("TabletGeneralPreferences.qml"); } break; - case 'setOpenFeedFalse': - Settings.setValue('openFeedAfterShare', false); + case 'captureStillAndGif': + print("Changing Snapshot Capture Settings to Capture Still + GIF"); + Settings.setValue("alsoTakeAnimatedSnapshot", true); break; - case 'setOpenFeedTrue': - Settings.setValue('openFeedAfterShare', true); + case 'captureStillOnly': + print("Changing Snapshot Capture Settings to Capture Still Only"); + Settings.setValue("alsoTakeAnimatedSnapshot", false); + break; + case 'takeSnapshot': + takeSnapshot(); + break; + case 'shareSnapshotForUrl': + isLoggedIn = Account.isLoggedIn(); + if (isLoggedIn) { + print('Sharing snapshot with audience "for_url":', message.data); + Window.shareSnapshot(message.data, message.href || href); + } else { + // TODO + } + break; + case 'blastToConnections': + isLoggedIn = Account.isLoggedIn(); + storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(message.story_id), 1); + if (message.isGif) { + Settings.setValue("previousAnimatedSnapSharingDisabled", true); + } else { + Settings.setValue("previousStillSnapSharingDisabled", true); + } + + if (isLoggedIn) { + print('Uploading new story for announcement!'); + + request({ + uri: METAVERSE_BASE + '/api/v1/user_stories/' + message.story_id, + method: 'GET' + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("ERROR getting details about existing snapshot story:", error || response.status); + return; + } else { + var requestBody = { + user_story: { + audience: "for_connections", + action: "announcement", + path: response.user_story.path, + place_name: response.user_story.place_name, + thumbnail_url: response.user_story.thumbnail_url, + // For historical reasons, the server doesn't take nested JSON objects. + // Thus, I'm required to STRINGIFY what should be a nested object. + details: JSON.stringify({ + shareable_url: response.user_story.details.shareable_url, + image_url: response.user_story.details.image_url + }) + } + } + request({ + uri: METAVERSE_BASE + '/api/v1/user_stories', + method: 'POST', + json: true, + body: requestBody + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("ERROR uploading announcement story: ", error || response.status); + if (message.isGif) { + Settings.setValue("previousAnimatedSnapSharingDisabled", false); + } else { + Settings.setValue("previousStillSnapSharingDisabled", false); + } + return; + } else { + print("SUCCESS uploading announcement story! Story ID:", response.user_story.id); + } + }); + } + }); + + } else { + openLoginWindow(); + } + break; + case 'shareSnapshotWithEveryone': + isLoggedIn = Account.isLoggedIn(); + storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(message.story_id), 1); + if (message.isGif) { + Settings.setValue("previousAnimatedSnapSharingDisabled", true); + } else { + Settings.setValue("previousStillSnapSharingDisabled", true); + } + + if (isLoggedIn) { + print('Modifying audience of story ID', message.story_id, "to 'for_feed'"); + var requestBody = { + audience: "for_feed" + } + + if (message.isAnnouncement) { + requestBody.action = "announcement"; + print('...Also announcing!'); + } + request({ + uri: METAVERSE_BASE + '/api/v1/user_stories/' + message.story_id, + method: 'PUT', + json: true, + body: requestBody + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("ERROR changing audience: ", error || response.status); + if (message.isGif) { + Settings.setValue("previousAnimatedSnapSharingDisabled", false); + } else { + Settings.setValue("previousStillSnapSharingDisabled", false); + } + return; + } else { + print("SUCCESS changing audience" + (message.isAnnouncement ? " and posting announcement!" : "!")); + } + }); + } else { + openLoginWindow(); + shareAfterLogin = true; + snapshotToShareAfterLogin = { path: message.data, href: message.href || href }; + } + break; + case 'shareButtonClicked': + print('Twitter or FB "Share" button clicked! Removing ID', message.story_id, 'from storyIDsToMaybeDelete[].'); + storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(message.story_id), 1); + print('storyIDsToMaybeDelete[] now:', JSON.stringify(storyIDsToMaybeDelete)); break; default: - //tablet.webEventReceived.disconnect(onMessage); // <<< It's probably this that's missing?! - HMD.closeTablet(); - isLoggedIn = Account.isLoggedIn(); - message.action.forEach(function (submessage) { - if (submessage.share && !isLoggedIn) { - needsLogin = true; - submessage.share = false; - shareAfterLogin = true; - snapshotToShareAfterLogin = {path: submessage.localPath, href: submessage.href || href}; - } - if (submessage.share) { - print('sharing', submessage.localPath); - outstanding = true; - Window.shareSnapshot(submessage.localPath, submessage.href || href); - } else { - print('not sharing', submessage.localPath); - } - - }); - if (outstanding && shouldOpenFeedAfterShare()) { - showFeedWindow(); - outstanding = false; - } - if (needsLogin) { // after the possible feed, so that the login is on top - var isLoggedIn = Account.isLoggedIn(); - - if (!isLoggedIn) { - if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar")) - || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))) { - Menu.triggerOption("Login / Sign Up"); - } else { - tablet.loadQMLOnTop("../../dialogs/TabletLoginDialog.qml"); - HMD.openTablet(); - } - } - } + print('Unknown message action received by snapshot.js!'); + break; } } var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html"); var isInSnapshotReview = false; -function confirmShare(data) { - tablet.gotoWebScreen(SNAPSHOT_REVIEW_URL); - readyData = data; - tablet.webEventReceived.connect(onMessage); - HMD.openTablet(); - isInSnapshotReview = true; +var shouldActivateButton = false; +function onButtonClicked() { + if (isInSnapshotReview){ + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + shouldActivateButton = true; + var previousStillSnapPath = Settings.getValue("previousStillSnapPath"); + var previousStillSnapStoryID = Settings.getValue("previousStillSnapStoryID"); + var previousStillSnapSharingDisabled = Settings.getValue("previousStillSnapSharingDisabled"); + var previousAnimatedSnapPath = Settings.getValue("previousAnimatedSnapPath"); + var previousAnimatedSnapStoryID = Settings.getValue("previousAnimatedSnapStoryID"); + var previousAnimatedSnapSharingDisabled = Settings.getValue("previousAnimatedSnapSharingDisabled"); + snapshotOptions = { + containsGif: previousAnimatedSnapPath !== "", + processingGif: false, + shouldUpload: false + } + imageData = []; + if (previousAnimatedSnapPath !== "") { + imageData.push({ localPath: previousAnimatedSnapPath, story_id: previousAnimatedSnapStoryID, buttonDisabled: previousAnimatedSnapSharingDisabled }); + } + if (previousStillSnapPath !== "") { + imageData.push({ localPath: previousStillSnapPath, story_id: previousStillSnapStoryID, buttonDisabled: previousStillSnapSharingDisabled }); + } + tablet.gotoWebScreen(SNAPSHOT_REVIEW_URL); + tablet.webEventReceived.connect(onMessage); + HMD.openTablet(); + isInSnapshotReview = true; + } } -function snapshotShared(errorMessage) { - if (!errorMessage) { - print('snapshot uploaded and shared'); +function snapshotUploaded(isError, reply) { + if (!isError) { + var replyJson = JSON.parse(reply); + var storyID = replyJson.user_story.id; + storyIDsToMaybeDelete.push(storyID); + var imageURL = replyJson.user_story.details.image_url; + var isGif = imageURL.split('.').pop().toLowerCase() === "gif"; + print('SUCCESS: Snapshot uploaded! Story with audience:for_url created! ID:', storyID); + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "snapshotUploadComplete", + story_id: storyID, + image_url: imageURL, + })); + if (isGif) { + Settings.setValue("previousAnimatedSnapStoryID", storyID); + } else { + Settings.setValue("previousStillSnapStoryID", storyID); + } } else { - print(errorMessage); - } - if ((--outstanding <= 0) && shouldOpenFeedAfterShare()) { - showFeedWindow(); + print(reply); } } var href, domainId; -function onClicked() { +function takeSnapshot() { + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "clearPreviousImages" + })); + Settings.setValue("previousStillSnapPath", ""); + Settings.setValue("previousStillSnapStoryID", ""); + Settings.setValue("previousStillSnapSharingDisabled", false); + Settings.setValue("previousAnimatedSnapPath", ""); + Settings.setValue("previousAnimatedSnapStoryID", ""); + Settings.setValue("previousAnimatedSnapSharingDisabled", false); + // Raising the desktop for the share dialog at end will interact badly with clearOverlayWhenMoving. // Turn it off now, before we start futzing with things (and possibly moving). clearOverlayWhenMoving = MyAvatar.getClearOverlayWhenMoving(); // Do not use Settings. MyAvatar keeps a separate copy. @@ -152,14 +369,25 @@ function onClicked() { // Even the domainId could change (e.g., if the user falls into a teleporter while recording). href = location.href; domainId = location.domainId; + Settings.setValue("previousSnapshotDomainID", domainId); + + maybeDeleteSnapshotStories(); // update button states - resetOverlays = Menu.isOptionChecked("Overlays"); // For completness. Certainly true if the button is visible to be clicke. + resetOverlays = Menu.isOptionChecked("Overlays"); // For completeness. Certainly true if the button is visible to be clicked. reticleVisible = Reticle.visible; Reticle.visible = false; - Window.stillSnapshotTaken.connect(stillSnapshotTaken); - Window.processingGifStarted.connect(processingGifStarted); - Window.processingGifCompleted.connect(processingGifCompleted); + + var includeAnimated = Settings.getValue("alsoTakeAnimatedSnapshot", true); + if (includeAnimated) { + Window.processingGifStarted.connect(processingGifStarted); + } else { + Window.stillSnapshotTaken.connect(stillSnapshotTaken); + } + if (buttonConnected) { + button.clicked.disconnect(onButtonClicked); + buttonConnected = false; + } // hide overlays if they are on if (resetOverlays) { @@ -170,13 +398,17 @@ function onClicked() { Script.setTimeout(function () { HMD.closeTablet(); Script.setTimeout(function () { - Window.takeSnapshot(false, true, 1.91); + Window.takeSnapshot(false, includeAnimated, 1.91); }, SNAPSHOT_DELAY); }, FINISH_SOUND_DELAY); } function isDomainOpen(id) { - var request = new XMLHttpRequest(); + print("Checking open status of domain with ID:", id); + if (!id) { + return false; + } + var options = [ 'now=' + new Date().toISOString(), 'include_actions=concurrency', @@ -184,15 +416,19 @@ function isDomainOpen(id) { 'restriction=open,hifi' // If we're sharing, we're logged in // If we're here, protocol matches, and it is online ]; - var url = location.metaverseServerUrl + "/api/v1/user_stories?" + options.join('&'); - request.open("GET", url, false); - request.send(); - if (request.status !== 200) { - return false; - } - var response = JSON.parse(request.response); // Not parsed for us. - return (response.status === 'success') && - response.total_entries; + var url = METAVERSE_BASE + "/api/v1/user_stories?" + options.join('&'); + + return request({ + uri: url, + method: 'GET' + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("ERROR getting open status of domain: ", error || response.status); + return false; + } else { + return response.total_entries; + } + }); } function stillSnapshotTaken(pathStillSnapshot, notify) { @@ -203,20 +439,30 @@ function stillSnapshotTaken(pathStillSnapshot, notify) { Menu.setIsOptionChecked("Overlays", true); } Window.stillSnapshotTaken.disconnect(stillSnapshotTaken); + if (!buttonConnected) { + button.clicked.connect(onButtonClicked); + buttonConnected = true; + } // A Snapshot Review dialog might be left open indefinitely after taking the picture, // during which time the user may have moved. So stash that info in the dialog so that // it records the correct href. (We can also stash in .jpegs, but not .gifs.) // last element in data array tells dialog whether we can share or not - var confirmShareContents = [ - { localPath: pathStillSnapshot, href: href }, - { - containsGif: false, - processingGif: false, - canShare: !!isDomainOpen(domainId), - openFeedAfterShare: shouldOpenFeedAfterShare() - }]; - confirmShare(confirmShareContents); + snapshotOptions = { + containsGif: false, + processingGif: false, + canShare: !isDomainOpen(domainId) + }; + imageData = [{ localPath: pathStillSnapshot, href: href }]; + Settings.setValue("previousStillSnapPath", pathStillSnapshot); + + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "addImages", + options: snapshotOptions, + image_data: imageData + })); + if (clearOverlayWhenMoving) { MyAvatar.setClearOverlayWhenMoving(true); // not until after the share dialog } @@ -225,8 +471,7 @@ function stillSnapshotTaken(pathStillSnapshot, notify) { function processingGifStarted(pathStillSnapshot) { Window.processingGifStarted.disconnect(processingGifStarted); - button.clicked.disconnect(onClicked); - buttonConnected = false; + Window.processingGifCompleted.connect(processingGifCompleted); // show hud Reticle.visible = reticleVisible; // show overlays if they were on @@ -234,16 +479,22 @@ function processingGifStarted(pathStillSnapshot) { Menu.setIsOptionChecked("Overlays", true); } - var confirmShareContents = [ - { localPath: pathStillSnapshot, href: href }, - { - containsGif: true, - processingGif: true, - loadingGifPath: Script.resolvePath(Script.resourcesPath() + 'icons/loadingDark.gif'), - canShare: !!isDomainOpen(domainId), - openFeedAfterShare: shouldOpenFeedAfterShare() - }]; - confirmShare(confirmShareContents); + snapshotOptions = { + containsGif: true, + processingGif: true, + loadingGifPath: Script.resolvePath(Script.resourcesPath() + 'icons/loadingDark.gif'), + canShare: !isDomainOpen(domainId) + }; + imageData = [{ localPath: pathStillSnapshot, href: href }]; + Settings.setValue("previousStillSnapPath", pathStillSnapshot); + + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "addImages", + options: snapshotOptions, + image_data: imageData + })); + if (clearOverlayWhenMoving) { MyAvatar.setClearOverlayWhenMoving(true); // not until after the share dialog } @@ -252,57 +503,72 @@ function processingGifStarted(pathStillSnapshot) { function processingGifCompleted(pathAnimatedSnapshot) { Window.processingGifCompleted.disconnect(processingGifCompleted); - button.clicked.connect(onClicked); - buttonConnected = true; + if (!buttonConnected) { + button.clicked.connect(onButtonClicked); + buttonConnected = true; + } - var confirmShareContents = [ - { localPath: pathAnimatedSnapshot, href: href }, - { - containsGif: true, - processingGif: false, - canShare: !!isDomainOpen(domainId), - openFeedAfterShare: shouldOpenFeedAfterShare() - }]; - readyData = confirmShareContents; + snapshotOptions = { + containsGif: true, + processingGif: false, + canShare: !isDomainOpen(domainId) + } + imageData = [{ localPath: pathAnimatedSnapshot, href: href }]; + Settings.setValue("previousAnimatedSnapPath", pathAnimatedSnapshot); tablet.emitScriptEvent(JSON.stringify({ type: "snapshot", - action: readyData + action: "addImages", + options: snapshotOptions, + image_data: imageData })); } - +function maybeDeleteSnapshotStories() { + storyIDsToMaybeDelete.forEach(function (element, idx, array) { + request({ + uri: METAVERSE_BASE + '/api/v1/user_stories/' + element, + method: 'DELETE' + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("ERROR deleting snapshot story: ", error || response.status); + return; + } else { + print("SUCCESS deleting snapshot story with ID", element); + } + }) + }); + storyIDsToMaybeDelete = []; +} function onTabletScreenChanged(type, url) { + button.editProperties({ isActive: shouldActivateButton }); + shouldActivateButton = false; if (isInSnapshotReview) { tablet.webEventReceived.disconnect(onMessage); isInSnapshotReview = false; } } -function onConnected() { +function onUsernameChanged() { if (shareAfterLogin && Account.isLoggedIn()) { - print('sharing', snapshotToShareAfterLogin.path); + print('Sharing snapshot after login:', snapshotToShareAfterLogin.path); Window.shareSnapshot(snapshotToShareAfterLogin.path, snapshotToShareAfterLogin.href); shareAfterLogin = false; - if (shouldOpenFeedAfterShare()) { - showFeedWindow(); - } } - } -button.clicked.connect(onClicked); +button.clicked.connect(onButtonClicked); buttonConnected = true; -Window.snapshotShared.connect(snapshotShared); +Window.snapshotShared.connect(snapshotUploaded); tablet.screenChanged.connect(onTabletScreenChanged); -Account.usernameChanged.connect(onConnected); +Account.usernameChanged.connect(onUsernameChanged); Script.scriptEnding.connect(function () { if (buttonConnected) { - button.clicked.disconnect(onClicked); + button.clicked.disconnect(onButtonClicked); buttonConnected = false; } if (tablet) { tablet.removeButton(button); } - Window.snapshotShared.disconnect(snapshotShared); + Window.snapshotShared.disconnect(snapshotUploaded); tablet.screenChanged.disconnect(onTabletScreenChanged); }); diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 84f7357b1a..fec7a6de90 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -1,4 +1,6 @@ "use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Window, Script, Tablet, HMD, Controller, Account, XMLHttpRequest, location, print*/ // // goto.js @@ -11,11 +13,106 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -(function() { // BEGIN LOCAL_SCOPE +(function () { // BEGIN LOCAL_SCOPE var gotoQmlSource = "TabletAddressDialog.qml"; var buttonName = "GOTO"; var onGotoScreen = false; var shouldActivateButton = false; + function ignore() { } + + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var NORMAL_ICON = "icons/tablet-icons/goto-i.svg"; + var NORMAL_ACTIVE = "icons/tablet-icons/goto-a.svg"; + var WAITING_ICON = "icons/tablet-icons/goto-msg.svg"; + var button = tablet.addButton({ + icon: NORMAL_ICON, + activeIcon: NORMAL_ACTIVE, + text: buttonName, + sortOrder: 8 + }); + function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request. + var httpRequest = new XMLHttpRequest(), key; + // QT bug: apparently doesn't handle onload. Workaround using readyState. + httpRequest.onreadystatechange = function () { + var READY_STATE_DONE = 4; + var HTTP_OK = 200; + if (httpRequest.readyState >= READY_STATE_DONE) { + var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText, + response = !error && httpRequest.responseText, + contentType = !error && httpRequest.getResponseHeader('content-type'); + if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc. + try { + response = JSON.parse(response); + } catch (e) { + error = e; + } + } + callback(error, response); + } + }; + if (typeof options === 'string') { + options = {uri: options}; + } + if (options.url) { + options.uri = options.url; + } + if (!options.method) { + options.method = 'GET'; + } + if (options.body && (options.method === 'GET')) { // add query parameters + var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&'; + for (key in options.body) { + params.push(key + '=' + options.body[key]); + } + options.uri += appender + params.join('&'); + delete options.body; + } + if (options.json) { + options.headers = options.headers || {}; + options.headers["Content-type"] = "application/json"; + options.body = JSON.stringify(options.body); + } + for (key in options.headers || {}) { + httpRequest.setRequestHeader(key, options.headers[key]); + } + httpRequest.open(options.method, options.uri, true); + httpRequest.send(options.body); + } + function fromQml(message) { + var response = {id: message.id, jsonrpc: "2.0"}; + switch (message.method) { + case 'request': + request(message.params, function (error, data) { + response.error = error; + response.result = data; + tablet.sendToQml(response); + }); + return; + default: + response.error = {message: 'Unrecognized message', data: message}; + } + tablet.sendToQml(response); + } + function messagesWaiting(isWaiting) { + button.editProperties({ + icon: isWaiting ? WAITING_ICON : NORMAL_ICON + // No need for a different activeIcon, because we issue messagesWaiting(false) when the button goes active anyway. + }); + } + var hasEventBridge = false; + function wireEventBridge(on) { + if (on) { + if (!hasEventBridge) { + tablet.fromQml.connect(fromQml); + hasEventBridge = true; + } + } else { + if (hasEventBridge) { + tablet.fromQml.disconnect(fromQml); + hasEventBridge = false; + } + } + } function onClicked() { if (onGotoScreen) { @@ -29,29 +126,68 @@ } function onScreenChanged(type, url) { + ignore(type); if (url === gotoQmlSource) { onGotoScreen = true; shouldActivateButton = true; button.editProperties({isActive: shouldActivateButton}); - } else { + wireEventBridge(true); + messagesWaiting(false); + } else { shouldActivateButton = false; onGotoScreen = false; button.editProperties({isActive: shouldActivateButton}); + wireEventBridge(false); } } - - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - var button = tablet.addButton({ - icon: "icons/tablet-icons/goto-i.svg", - activeIcon: "icons/tablet-icons/goto-a.svg", - text: buttonName, - sortOrder: 8 - }); - button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); + var stories = {}; + var DEBUG = false; + function pollForAnnouncements() { + var actions = DEBUG ? 'snapshot' : 'announcement'; + var count = DEBUG ? 10 : 100; + var options = [ + 'now=' + new Date().toISOString(), + 'include_actions=' + actions, + 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), + 'require_online=true', + 'protocol=' + encodeURIComponent(location.protocolVersion()), + 'per_page=' + count + ]; + var url = location.metaverseServerUrl + '/api/v1/user_stories?' + options.join('&'); + request({ + uri: url + }, function (error, data) { + if (error || (data.status !== 'success')) { + print("Error: unable to get", url, error || data.status); + return; + } + var didNotify = false; + data.user_stories.forEach(function (story) { + if (stories[story.id]) { // already seen + return; + } + stories[story.id] = story; + var message = story.username + " says something is happening in " + story.place_name + ". Open GOTO to join them."; + Window.displayAnnouncement(message); + didNotify = true; + }); + if (didNotify) { + messagesWaiting(true); + if (HMD.isHandControllerAvailable()) { + var STRENGTH = 1.0, DURATION_MS = 60, HAND = 2; // both hands + Controller.triggerHapticPulse(STRENGTH, DURATION_MS, HAND); + } + } + }); + } + var ANNOUNCEMENTS_POLL_TIME_MS = (DEBUG ? 10 : 60) * 1000; + var pollTimer = Script.setInterval(pollForAnnouncements, ANNOUNCEMENTS_POLL_TIME_MS); + Script.scriptEnding.connect(function () { + Script.clearInterval(pollTimer); button.clicked.disconnect(onClicked); tablet.removeButton(button); tablet.screenChanged.disconnect(onScreenChanged); diff --git a/tests/controllers/src/main.cpp b/tests/controllers/src/main.cpp index 2c8f361fac..e697bd501f 100644 --- a/tests/controllers/src/main.cpp +++ b/tests/controllers/src/main.cpp @@ -114,6 +114,12 @@ int main(int argc, char** argv) { last = now; InputCalibrationData calibrationData = { + glm::mat4(), + glm::mat4(), + glm::mat4(), + glm::mat4(), + glm::mat4(), + glm::mat4(), glm::mat4(), glm::mat4(), glm::mat4() @@ -130,6 +136,12 @@ int main(int argc, char** argv) { { InputCalibrationData calibrationData = { + glm::mat4(), + glm::mat4(), + glm::mat4(), + glm::mat4(), + glm::mat4(), + glm::mat4(), glm::mat4(), glm::mat4(), glm::mat4()