diff --git a/CMakeLists.txt b/CMakeLists.txt index bb26afad75..c8710eed05 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,7 +81,11 @@ if (ANDROID) set(GLES_OPTION ON) set(PLATFORM_QT_COMPONENTS AndroidExtras WebView) add_definitions(-DHIFI_ANDROID_APP=\"${HIFI_ANDROID_APP}\") - if (${HIFI_ANDROID_APP} STREQUAL "questInterface") + if ( + (${HIFI_ANDROID_APP} STREQUAL "questInterface") OR + (${HIFI_ANDROID_APP} STREQUAL "questFramePlayer") OR + (${HIFI_ANDROID_APP} STREQUAL "framePlayer") + ) # We know the quest hardware has this extension, so we can force the use of instanced stereo add_definitions(-DHAVE_EXT_clip_cull_distance) # We can also use multiview stereo techniques @@ -89,9 +93,15 @@ if (ANDROID) add_definitions(-DHAVE_OVR_multiview) # We can also use our own foveated textures add_definitions(-DHAVE_QCOM_texture_foveated) + + # if set, the application itself or some library it depends on MUST implement + # `DisplayPluginList getDisplayPlugins()` and `InputPluginList getInputPlugins()` + add_definitions(-DCUSTOM_INPUT_PLUGINS) + add_definitions(-DCUSTOM_DISPLAY_PLUGINS) + set(PLATFORM_PLUGIN_LIBRARIES oculusMobile oculusMobilePlugin) endif() else () - set(PLATFORM_QT_COMPONENTS WebEngine) + set(PLATFORM_QT_COMPONENTS WebEngine Xml) endif () if (USE_GLES AND (NOT ANDROID)) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 500772c1b5..801f28c6f5 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -38,6 +38,19 @@ const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer"; // FIXME - what we'd actually like to do is send to users at ~50% of their present rate down to 30hz. Assume 90 for now. const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45; +const QRegularExpression AvatarMixer::suffixedNamePattern { R"(^\s*(.+)\s*_(\d)+\s*$)" }; + +// Lexicographic comparison: +bool AvatarMixer::SessionDisplayName::operator<(const SessionDisplayName& rhs) const { + if (_baseName < rhs._baseName) { + return true; + } else if (rhs._baseName < _baseName) { + return false; + } else { + return _suffix < rhs._suffix; + } +} + AvatarMixer::AvatarMixer(ReceivedMessage& message) : ThreadedAssignment(message), _slavePool(&_slaveSharedData) @@ -313,27 +326,40 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) { bool sendIdentity = false; if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) { AvatarData& avatar = nodeData->getAvatar(); - const QString& existingBaseDisplayName = nodeData->getBaseDisplayName(); - if (--_sessionDisplayNames[existingBaseDisplayName].second <= 0) { - _sessionDisplayNames.remove(existingBaseDisplayName); + const QString& existingBaseDisplayName = nodeData->getAvatar().getSessionDisplayName(); + if (!existingBaseDisplayName.isEmpty()) { + SessionDisplayName existingDisplayName { existingBaseDisplayName }; + + auto suffixMatch = suffixedNamePattern.match(existingBaseDisplayName); + if (suffixMatch.hasMatch()) { + existingDisplayName._baseName = suffixMatch.captured(1); + existingDisplayName._suffix = suffixMatch.captured(2).toInt(); + } + _sessionDisplayNames.erase(existingDisplayName); } QString baseName = avatar.getDisplayName().trimmed(); const QRegularExpression curses { "fuck|shit|damn|cock|cunt" }; // POC. We may eventually want something much more elaborate (subscription?). baseName = baseName.replace(curses, "*"); // Replace rather than remove, so that people have a clue that the person's a jerk. - const QRegularExpression trailingDigits { "\\s*(_\\d+\\s*)?(\\s*\\n[^$]*)?$" }; // trailing whitespace "_123" and any subsequent lines + static const QRegularExpression trailingDigits { R"(\s*(_\d+\s*)?(\s*\n[^$]*)?$)" }; // trailing whitespace "_123" and any subsequent lines baseName = baseName.remove(trailingDigits); if (baseName.isEmpty()) { baseName = "anonymous"; } - QPair& soFar = _sessionDisplayNames[baseName]; // Inserts and answers 0, 0 if not already present, which is what we want. - int& highWater = soFar.first; - nodeData->setBaseDisplayName(baseName); - QString sessionDisplayName = (highWater > 0) ? baseName + "_" + QString::number(highWater) : baseName; + SessionDisplayName newDisplayName { baseName }; + auto nameIter = _sessionDisplayNames.lower_bound(newDisplayName); + if (nameIter != _sessionDisplayNames.end() && nameIter->_baseName == baseName) { + // Existing instance(s) of name; find first free suffix + while (*nameIter == newDisplayName && ++newDisplayName._suffix && ++nameIter != _sessionDisplayNames.end()) + ; + } + + _sessionDisplayNames.insert(newDisplayName); + QString sessionDisplayName = (newDisplayName._suffix > 0) ? baseName + "_" + QString::number(newDisplayName._suffix) : baseName; avatar.setSessionDisplayName(sessionDisplayName); - highWater++; - soFar.second++; // refcount + nodeData->setBaseDisplayName(baseName); + nodeData->flagIdentityChange(); nodeData->setAvatarSessionDisplayNameMustChange(false); sendIdentity = true; @@ -409,10 +435,19 @@ void AvatarMixer::handleAvatarKilled(SharedNodePointer avatarNode) { { // decrement sessionDisplayNames table and possibly remove QMutexLocker nodeDataLocker(&avatarNode->getLinkedData()->getMutex()); AvatarMixerClientData* nodeData = dynamic_cast(avatarNode->getLinkedData()); - const QString& baseDisplayName = nodeData->getBaseDisplayName(); - // No sense guarding against very rare case of a node with no entry, as this will work without the guard and do one less lookup in the common case. - if (--_sessionDisplayNames[baseDisplayName].second <= 0) { - _sessionDisplayNames.remove(baseDisplayName); + const QString& displayName = nodeData->getAvatar().getSessionDisplayName(); + SessionDisplayName exitingDisplayName { displayName }; + + auto suffixMatch = suffixedNamePattern.match(displayName); + if (suffixMatch.hasMatch()) { + exitingDisplayName._baseName = suffixMatch.captured(1); + exitingDisplayName._suffix = suffixMatch.captured(2).toInt(); + } + auto displayNameIter = _sessionDisplayNames.find(exitingDisplayName); + if (displayNameIter == _sessionDisplayNames.end()) { + qCDebug(avatars) << "Exiting avatar displayname" << displayName << "not found"; + } else { + _sessionDisplayNames.erase(displayNameIter); } } diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 764656a2d5..2992e19b8f 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -15,6 +15,7 @@ #ifndef hifi_AvatarMixer_h #define hifi_AvatarMixer_h +#include #include #include @@ -88,7 +89,24 @@ private: RateCounter<> _broadcastRate; p_high_resolution_clock::time_point _lastDebugMessage; - QHash> _sessionDisplayNames; + + // Pair of basename + uniquifying integer suffix. + struct SessionDisplayName { + explicit SessionDisplayName(QString baseName = QString(), int suffix = 0) : + _baseName(baseName), + _suffix(suffix) { } + // Does lexicographic ordering: + bool operator<(const SessionDisplayName& rhs) const; + bool operator==(const SessionDisplayName& rhs) const { + return _baseName == rhs._baseName && _suffix == rhs._suffix; + } + + QString _baseName; + int _suffix; + }; + static const QRegularExpression suffixedNamePattern; + + std::set _sessionDisplayNames; quint64 _displayNameManagementElapsedTime { 0 }; // total time spent in broadcastAvatarData/display name management... since last stats window quint64 _ignoreCalculationElapsedTime { 0 }; diff --git a/cmake/macros/FixupNitpick.cmake b/cmake/macros/FixupNitpick.cmake new file mode 100644 index 0000000000..8477b17823 --- /dev/null +++ b/cmake/macros/FixupNitpick.cmake @@ -0,0 +1,36 @@ +# +# FixupNitpick.cmake +# cmake/macros +# +# Copyright 2019 High Fidelity, Inc. +# Created by Nissim Hadar on January 14th, 2016 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# + +macro(fixup_nitpick) + if (APPLE) + string(REPLACE " " "\\ " ESCAPED_BUNDLE_NAME ${NITPICK_BUNDLE_NAME}) + string(REPLACE " " "\\ " ESCAPED_INSTALL_PATH ${NITPICK_INSTALL_DIR}) + set(_NITPICK_INSTALL_PATH "${ESCAPED_INSTALL_PATH}/${ESCAPED_BUNDLE_NAME}.app") + + find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS "${QT_DIR}/bin" NO_DEFAULT_PATH) + + if (NOT MACDEPLOYQT_COMMAND AND (PRODUCTION_BUILD OR PR_BUILD)) + message(FATAL_ERROR "Could not find macdeployqt at ${QT_DIR}/bin.\ + It is required to produce a relocatable nitpick application.\ + Check that the environment variable QT_DIR points to your Qt installation.\ + ") + endif () + + install(CODE " + execute_process(COMMAND ${MACDEPLOYQT_COMMAND}\ + \${CMAKE_INSTALL_PREFIX}/${_NITPICK_INSTALL_PATH}/\ + -verbose=2 -qmldir=${CMAKE_SOURCE_DIR}/interface/resources/qml/\ + )" + COMPONENT ${CLIENT_COMPONENT} + ) + + endif () +endmacro() diff --git a/cmake/macros/SetPackagingParameters.cmake b/cmake/macros/SetPackagingParameters.cmake index 1b7243d4f2..3ebc44e931 100644 --- a/cmake/macros/SetPackagingParameters.cmake +++ b/cmake/macros/SetPackagingParameters.cmake @@ -77,6 +77,9 @@ macro(SET_PACKAGING_PARAMETERS) add_definitions(-DDEV_BUILD) endif () + set(NITPICK_BUNDLE_NAME "nitpick") + set(NITPICK_ICON_PREFIX "nitpick") + string(TIMESTAMP BUILD_TIME "%d/%m/%Y") # if STABLE_BUILD is 1, PRODUCTION_BUILD must be 1 and @@ -140,8 +143,9 @@ macro(SET_PACKAGING_PARAMETERS) set(DMG_SUBFOLDER_ICON "${HF_CMAKE_DIR}/installer/install-folder.rsrc") - set(CONSOLE_INSTALL_DIR ${DMG_SUBFOLDER_NAME}) + set(CONSOLE_INSTALL_DIR ${DMG_SUBFOLDER_NAME}) set(INTERFACE_INSTALL_DIR ${DMG_SUBFOLDER_NAME}) + set(NITPICK_INSTALL_DIR ${DMG_SUBFOLDER_NAME}) if (CLIENT_ONLY) set(CONSOLE_EXEC_NAME "Console.app") @@ -159,11 +163,14 @@ macro(SET_PACKAGING_PARAMETERS) set(INTERFACE_INSTALL_APP_PATH "${CONSOLE_INSTALL_DIR}/${INTERFACE_BUNDLE_NAME}.app") set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.icns") + set(NITPICK_ICON_FILENAME "${NITPICK_ICON_PREFIX}.icns") else () if (WIN32) set(CONSOLE_INSTALL_DIR "server-console") + set(NITPICK_INSTALL_DIR "nitpick") else () set(CONSOLE_INSTALL_DIR ".") + set(NITPICK_INSTALL_DIR ".") endif () set(COMPONENT_INSTALL_DIR ".") @@ -173,6 +180,7 @@ macro(SET_PACKAGING_PARAMETERS) if (WIN32) set(INTERFACE_EXEC_PREFIX "interface") set(INTERFACE_ICON_FILENAME "${INTERFACE_ICON_PREFIX}.ico") + set(NITPICK_ICON_FILENAME "${NITPICK_ICON_PREFIX}.ico") set(CONSOLE_EXEC_NAME "server-console.exe") diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 4eee6242ff..2cc3302af7 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -217,8 +217,8 @@ link_hifi_libraries( ui-plugins display-plugins input-plugins # Platform specific GL libraries ${PLATFORM_GL_BACKEND} - # Plaform specific display plugins libraries - ${PLATFORM_DISPLAY_PLUGINS} + # Plaform specific input & display plugin libraries + ${PLATFORM_PLUGIN_LIBRARIES} shaders ) diff --git a/interface/resources/images/unsupportedImage.png b/interface/resources/images/unsupportedImage.png new file mode 100644 index 0000000000..87d238b67c Binary files /dev/null and b/interface/resources/images/unsupportedImage.png differ diff --git a/interface/resources/meshes/defaultAvatar_full.fst b/interface/resources/meshes/defaultAvatar_full.fst index b47faeb8a8..aa679e319a 100644 --- a/interface/resources/meshes/defaultAvatar_full.fst +++ b/interface/resources/meshes/defaultAvatar_full.fst @@ -10,10 +10,6 @@ joint = jointRoot = Hips joint = jointLeftHand = LeftHand joint = jointRightHand = RightHand joint = jointHead = Head -freeJoint = LeftArm -freeJoint = LeftForeArm -freeJoint = RightArm -freeJoint = RightForeArm bs = JawOpen = mouth_Open = 1 bs = LipsFunnel = Oo = 1 bs = BrowsU_L = brow_Up = 1 diff --git a/interface/resources/qml/+webengine/QmlWebWindow.qml b/interface/resources/qml/+webengine/QmlWebWindow.qml new file mode 100644 index 0000000000..2e3718f6f5 --- /dev/null +++ b/interface/resources/qml/+webengine/QmlWebWindow.qml @@ -0,0 +1,108 @@ +// +// QmlWebWindow.qml +// +// Created by Bradley Austin Davis on 17 Dec 2015 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtWebEngine 1.1 +import QtWebChannel 1.0 + +import "qrc:////qml//windows" as Windows +import controlsUit 1.0 as Controls +import stylesUit 1.0 + +Windows.ScrollingWindow { + id: root + HifiConstants { id: hifi } + title: "WebWindow" + resizable: true + shown: false + // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer + destroyOnCloseButton: false + property alias source: webview.url + property alias scriptUrl: webview.userScriptUrl + + // This is for JS/QML communication, which is unused in a WebWindow, + // but not having this here results in spurious warnings about a + // missing signal + signal sendToScript(var message); + + signal moved(vector2d position); + signal resized(size size); + + function notifyMoved() { + moved(Qt.vector2d(x, y)); + } + + function notifyResized() { + resized(Qt.size(width, height)); + } + + onXChanged: notifyMoved(); + onYChanged: notifyMoved(); + + onWidthChanged: notifyResized(); + onHeightChanged: notifyResized(); + + onShownChanged: { + keyboardEnabled = HMD.active; + } + + Item { + width: pane.contentWidth + implicitHeight: pane.scrollHeight + + Controls.WebView { + id: webview + url: "about:blank" + anchors.fill: parent + focus: true + profile: HFWebEngineProfile; + + property string userScriptUrl: "" + + // Create a global EventBridge object for raiseAndLowerKeyboard. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // Detect when may want to raise and lower keyboard. + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + // User script. + WebEngineScript { + id: userScript + sourceUrl: webview.userScriptUrl + injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. + worldId: WebEngineScript.MainWorld + } + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] + + function onWebEventReceived(event) { + if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") { + ApplicationInterface.addAssetToWorldFromURL(event.slice(18)); + } + } + + Component.onCompleted: { + webChannel.registerObject("eventBridge", eventBridge); + webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); + eventBridge.webEventReceived.connect(onWebEventReceived); + } + } + } +} diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index d639ed15d6..a40168039e 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -9,8 +9,6 @@ // import QtQuick 2.5 -import QtWebView 1.1 -import QtWebChannel 1.0 import "windows" as Windows import controlsUit 1.0 as Controls @@ -60,22 +58,9 @@ Windows.ScrollingWindow { Controls.WebView { id: webview url: "about:blank" + property string userScriptUrl: "" anchors.fill: parent focus: true - - property string userScriptUrl: "" - - function onWebEventReceived(event) { - if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") { - ApplicationInterface.addAssetToWorldFromURL(event.slice(18)); - } - } - - Component.onCompleted: { - webChannel.registerObject("eventBridge", eventBridge); - webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); - eventBridge.webEventReceived.connect(onWebEventReceived); - } } } } diff --git a/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml b/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml new file mode 100644 index 0000000000..c2e2ca4b40 --- /dev/null +++ b/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml @@ -0,0 +1,24 @@ +// +// WebSpinner.qml +// +// Created by David Rowe on 23 May 2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtWebEngine 1.5 + +AnimatedImage { + property WebEngineView webview: parent + source: "qrc:////icons//loader-snake-64-w.gif" + visible: webview.loading && /^(http.*|)$/i.test(webview.url.toString()) + playing: visible + z: 10000 + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } +} diff --git a/interface/resources/qml/controlsUit/WebSpinner.qml b/interface/resources/qml/controlsUit/WebSpinner.qml index 8f78a1e273..bcf415e0c0 100644 --- a/interface/resources/qml/controlsUit/WebSpinner.qml +++ b/interface/resources/qml/controlsUit/WebSpinner.qml @@ -9,11 +9,15 @@ // import QtQuick 2.5 -import QtWebView 1.1 -AnimatedImage { - property WebView webview: parent - source: "../../icons/loader-snake-64-w.gif" +Image { + Item { + id: webView + property bool loading: false + property string url: "" + } + + source: "qrc:////images//unsupportedImage.png" visible: webview.loading && /^(http.*|)$/i.test(webview.url.toString()) playing: visible z: 10000 diff --git a/interface/resources/qml/hifi/+webengine/DesktopWebEngine.qml b/interface/resources/qml/hifi/+webengine/DesktopWebEngine.qml new file mode 100644 index 0000000000..56cc38254f --- /dev/null +++ b/interface/resources/qml/hifi/+webengine/DesktopWebEngine.qml @@ -0,0 +1,44 @@ +import QtQuick 2.7 +import QtWebEngine 1.5 + +Item { + id: root + + property bool webViewProfileSetup: false + property string currentUrl: "" + property string downloadUrl: "" + property string adaptedPath: "" + property string tempDir: "" + function setupWebEngineSettings() { + WebEngine.settings.javascriptCanOpenWindows = true; + WebEngine.settings.javascriptCanAccessClipboard = false; + WebEngine.settings.spatialNavigationEnabled = false; + WebEngine.settings.localContentCanAccessRemoteUrls = true; + } + + + function initWebviewProfileHandlers(profile) { + downloadUrl = currentUrl; + if (webViewProfileSetup) return; + webViewProfileSetup = true; + + profile.downloadRequested.connect(function(download){ + adaptedPath = File.convertUrlToPath(downloadUrl); + tempDir = File.getTempDir(); + download.path = tempDir + "/" + adaptedPath; + download.accept(); + if (download.state === WebEngineDownloadItem.DownloadInterrupted) { + console.log("download failed to complete"); + } + }) + + profile.downloadFinished.connect(function(download){ + if (download.state === WebEngineDownloadItem.DownloadCompleted) { + File.runUnzip(download.path, downloadUrl, autoAdd); + } else { + console.log("The download was corrupted, state: " + download.state); + } + autoAdd = false; + }) + } +} diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 3a235c3377..a97d94d91c 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -1,5 +1,4 @@ import QtQuick 2.7 -import QtWebView 1.1 import Qt.labs.settings 1.0 as QtSettings import QtQuick.Controls 2.3 @@ -87,15 +86,21 @@ OriginalDesktop.Desktop { return map; })({}); + Component.onCompleted: { + webEngineConfig.setupWebEngineSettings(); + } + // Accept a download through the webview - property bool webViewProfileSetup: false - property string currentUrl: "" - property string downloadUrl: "" - property string adaptedPath: "" - property string tempDir: "" + property alias webViewProfileSetup: webEngineConfig.webViewProfileSetup + property alias currentUrl: webEngineConfig.currentUrl + property alias downloadUrl: webEngineConfig.downloadUrl + property alias adaptedPath: webEngineConfig.adaptedPath + property alias tempDir: webEngineConfig.tempDir + property var initWebviewProfileHandlers: webEngineConfig.initWebviewProfileHandlers property bool autoAdd: false - function initWebviewProfileHandlers(profile) { + DesktopWebEngine { + id: webEngineConfig } function setAutoAdd(auto) { diff --git a/interface/resources/qml/hifi/DesktopWebEngine.qml b/interface/resources/qml/hifi/DesktopWebEngine.qml new file mode 100644 index 0000000000..58c6244e7e --- /dev/null +++ b/interface/resources/qml/hifi/DesktopWebEngine.qml @@ -0,0 +1,17 @@ +import QtQuick 2.7 + +Item { + id: root + + property bool webViewProfileSetup: false + property string currentUrl: "" + property string downloadUrl: "" + property string adaptedPath: "" + property string tempDir: "" + function setupWebEngineSettings() { + } + + + function initWebviewProfileHandlers(profile) { + } +} diff --git a/interface/resources/qml/hifi/WebBrowser.qml b/interface/resources/qml/hifi/WebBrowser.qml deleted file mode 100644 index 6012e9be40..0000000000 --- a/interface/resources/qml/hifi/WebBrowser.qml +++ /dev/null @@ -1,358 +0,0 @@ - -// -// WebBrowser.qml -// -// -// Created by Vlad Stelmahovsky on 06/22/2017 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.7 -import QtQuick.Controls 2.2 as QQControls -import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.0 - -import QtWebView 1.1 -import QtWebChannel 1.0 - -import stylesUit 1.0 -import controlsUit 1.0 as HifiControls -import "../windows" -import "../controls" - -import HifiWeb 1.0 - -Rectangle { - id: root; - - HifiConstants { id: hifi; } - - property string title: ""; - signal sendToScript(var message); - property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false - property bool keyboardRaised: false - property bool punctuationMode: false - property var suggestionsList: [] - readonly property string searchUrlTemplate: "https://www.google.com/search?client=hifibrowser&q="; - - - WebBrowserSuggestionsEngine { - id: searchEngine - - onSuggestions: { - if (suggestions.length > 0) { - suggestionsList = [] - suggestionsList.push(addressBarInput.text); //do not overwrite edit text - for(var i = 0; i < suggestions.length; i++) { - suggestionsList.push(suggestions[i]); - } - addressBar.model = suggestionsList - if (!addressBar.popup.visible) { - addressBar.popup.open(); - } - } - } - } - - Timer { - id: suggestionRequestTimer - interval: 200 - repeat: false - onTriggered: { - if (addressBar.editText !== "") { - searchEngine.querySuggestions(addressBarInput.text); - } - } - } - - color: hifi.colors.baseGray; - - function goTo(url) { - //must be valid attempt to open an site with dot - var urlNew = url - if (url.indexOf(".") > 0) { - if (url.indexOf("http") < 0) { - urlNew = "http://" + url; - } - } else { - urlNew = searchUrlTemplate + url - } - - addressBar.model = [] - //need to rebind if binfing was broken by selecting from suggestions - addressBar.editText = Qt.binding( function() { return webStack.currentItem.webEngineView.url; }); - webStack.currentItem.webEngineView.url = urlNew - suggestionRequestTimer.stop(); - addressBar.popup.close(); - } - - Column { - spacing: 2 - width: parent.width; - - RowLayout { - id: addressBarRow - width: parent.width; - height: 48 - - HifiControls.WebGlyphButton { - enabled: webStack.currentItem.webEngineView.canGoBack || webStack.depth > 1 - glyph: hifi.glyphs.backward; - anchors.verticalCenter: parent.verticalCenter; - size: 38; - onClicked: { - if (webStack.currentItem.webEngineView.canGoBack) { - webStack.currentItem.webEngineView.goBack(); - } else if (webStack.depth > 1) { - webStack.pop(); - } - } - } - - HifiControls.WebGlyphButton { - enabled: webStack.currentItem.webEngineView.canGoForward - glyph: hifi.glyphs.forward; - anchors.verticalCenter: parent.verticalCenter; - size: 38; - onClicked: { - webStack.currentItem.webEngineView.goForward(); - } - } - - QQControls.ComboBox { - id: addressBar - - //selectByMouse: true - focus: true - - editable: true - //flat: true - indicator: Item {} - background: Item {} - onActivated: { - goTo(textAt(index)); - } - - onHighlightedIndexChanged: { - if (highlightedIndex >= 0) { - addressBar.editText = textAt(highlightedIndex) - } - } - - popup.height: webStack.height - - onFocusChanged: { - if (focus) { - addressBarInput.selectAll(); - } - } - - contentItem: QQControls.TextField { - id: addressBarInput - leftPadding: 26 - rightPadding: hifi.dimensions.controlLineHeight + 5 - text: addressBar.editText - placeholderText: qsTr("Enter URL") - font: addressBar.font - selectByMouse: true - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - onFocusChanged: { - if (focus) { - selectAll(); - } - } - - Keys.onDeletePressed: { - addressBarInput.text = "" - } - - Keys.onPressed: { - if (event.key === Qt.Key_Return) { - goTo(addressBarInput.text); - event.accepted = true; - } - } - - Image { - anchors.verticalCenter: parent.verticalCenter; - x: 5 - z: 2 - id: faviconImage - width: 16; height: 16 - sourceSize: Qt.size(width, height) - source: webStack.currentItem.webEngineView.icon - } - - HifiControls.WebGlyphButton { - glyph: webStack.currentItem.WebView.Loading ? hifi.glyphs.closeSmall : hifi.glyphs.reloadSmall; - anchors.verticalCenter: parent.verticalCenter; - width: hifi.dimensions.controlLineHeight - z: 2 - x: addressBarInput.width - implicitWidth - onClicked: { - if (webStack.currentItem.WebView.Loading) { - webStack.currentItem.webEngineView.stop(); - } else { - webStack.currentItem.reloadTimer.start(); - } - } - } - } - - Component.onCompleted: ScriptDiscoveryService.scriptsModelFilter.filterRegExp = new RegExp("^.*$", "i"); - - Keys.onPressed: { - if (event.key === Qt.Key_Return) { - goTo(addressBarInput.text); - event.accepted = true; - } - } - - onEditTextChanged: { - if (addressBar.editText !== "" && addressBar.editText !== webStack.currentItem.webEngineView.url.toString()) { - suggestionRequestTimer.restart(); - } else { - addressBar.model = [] - addressBar.popup.close(); - } - - } - - Layout.fillWidth: true - editText: webStack.currentItem.webEngineView.url - onAccepted: goTo(addressBarInput.text); - } - - HifiControls.WebGlyphButton { - checkable: true - checked: webStack.currentItem.webEngineView.audioMuted - glyph: checked ? hifi.glyphs.unmuted : hifi.glyphs.muted - anchors.verticalCenter: parent.verticalCenter; - width: hifi.dimensions.controlLineHeight - onClicked: { - webStack.currentItem.webEngineView.audioMuted = !webStack.currentItem.webEngineView.audioMuted - } - } - } - - QQControls.ProgressBar { - id: loadProgressBar - background: Rectangle { - implicitHeight: 2 - color: "#6A6A6A" - } - - contentItem: Item { - implicitHeight: 2 - - Rectangle { - width: loadProgressBar.visualPosition * parent.width - height: parent.height - color: "#00B4EF" - } - } - - width: parent.width; - from: 0 - to: 100 - value: webStack.currentItem.WebView.LoadProgress - height: 2 - } - - Component { - id: webViewComponent - Rectangle { - property alias webEngineView: webEngineView - property alias reloadTimer: reloadTimer - - property var request: null - - property bool isDialog: QQControls.StackView.index > 0 - property real margins: isDialog ? 10 : 0 - - color: "#d1d1d1" - - QQControls.StackView.onActivated: { - addressBar.editText = Qt.binding( function() { return webStack.currentItem.webEngineView.url; }); - } - - onRequestChanged: { - if (isDialog && request !== null && request !== undefined) {//is Dialog ? - request.openIn(webEngineView); - } - } - - HifiControls.BaseWebView { - id: webEngineView - anchors.fill: parent - anchors.margins: parent.margins - - layer.enabled: parent.isDialog - layer.effect: DropShadow { - verticalOffset: 8 - horizontalOffset: 8 - color: "#330066ff" - samples: 10 - spread: 0.5 - } - - focus: true - objectName: "tabletWebEngineView" - - profile.httpUserAgent: "Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/13.0 Firefox/13.0" - - property string userScriptUrl: "" - - onLoadingChanged: { - if (!loading) { - addressBarInput.cursorPosition = 0 //set input field cursot to beginning - suggestionRequestTimer.stop(); - addressBar.popup.close(); - } - } - - onLinkHovered: { - //TODO: change cursor shape? - } - - Component.onCompleted: { - webChannel.registerObject("eventBridge", eventBridge); - webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); - } - } - Timer { - id: reloadTimer - interval: 0 - running: false - repeat: false - onTriggered: webEngineView.reload() - } - } - } - - QQControls.StackView { - id: webStack - width: parent.width; - property real webViewHeight: root.height - loadProgressBar.height - 48 - 4 - height: keyboardEnabled && keyboardRaised ? webViewHeight - keyboard.height : webViewHeight - - Component.onCompleted: webStack.push(webViewComponent, {"webEngineView.url": "https://www.highfidelity.com"}); - } - } - - - 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/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index fee37ca1c1..39f75a9182 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -16,7 +16,13 @@ import TabletScriptingInterface 1.0 Rectangle { readonly property var level: AudioScriptingInterface.inputLevel; - + + property bool gated: false; + Component.onCompleted: { + AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); + AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); + } + property bool standalone: false; property var dragTarget: null; @@ -77,6 +83,7 @@ Rectangle { readonly property string gutter: "#575757"; readonly property string greenStart: "#39A38F"; readonly property string greenEnd: "#1FC6A6"; + readonly property string yellow: "#C0C000"; readonly property string red: colors.muted; readonly property string fill: "#55000000"; readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; @@ -189,7 +196,7 @@ Rectangle { Rectangle { // mask id: mask; - width: parent.width * level; + width: gated ? 0 : parent.width * level; radius: 5; anchors { bottom: parent.bottom; @@ -212,18 +219,42 @@ Rectangle { color: colors.greenStart; } GradientStop { - position: 0.8; + position: 0.5; color: colors.greenEnd; } - GradientStop { - position: 0.81; - color: colors.red; - } GradientStop { position: 1; - color: colors.red; + color: colors.yellow; } } } + + Rectangle { + id: gatedIndicator; + visible: gated && !AudioScriptingInterface.clipping + + radius: 4; + width: 2 * radius; + height: 2 * radius; + color: "#0080FF"; + anchors { + right: parent.left; + verticalCenter: parent.verticalCenter; + } + } + + Rectangle { + id: clippingIndicator; + visible: AudioScriptingInterface.clipping + + radius: 4; + width: 2 * radius; + height: 2 * radius; + color: colors.red; + anchors { + left: parent.right; + verticalCenter: parent.verticalCenter; + } + } } } diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 351c43286c..0d42cb599e 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -38,8 +38,8 @@ Rectangle { property bool keyboardEnabled: HMD.active property bool keyboardRaised: false property string searchScopeString: "Featured" - property bool isLoggedIn: false; - property bool supports3DHTML: true; + property bool isLoggedIn: false + property bool supports3DHTML: true anchors.fill: (typeof parent === undefined) ? undefined : parent @@ -49,7 +49,7 @@ Rectangle { licenseInfo.visible = false; marketBrowseModel.getFirstPage(); { - if(root.searchString !== undefined && root.searchString !== "") { + if(root.searchString !== undefined && root.searchString !== "") { root.searchScopeString = "Search Results: \"" + root.searchString + "\""; } else if (root.categoryString !== "") { root.searchScopeString = root.categoryString; @@ -498,7 +498,7 @@ Rectangle { "", "", root.sortString, - false, + WalletScriptingInterface.limitedCommerce, marketBrowseModel.currentPageToRetrieve, marketBrowseModel.itemsPerPage ); diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index 24ef528673..0a57e56099 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -14,8 +14,6 @@ import Hifi 1.0 as Hifi import QtQuick 2.9 import QtQuick.Controls 2.2 -import QtGraphicalEffects 1.0 -import QtWebEngine 1.5 import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit import "../../../controls" as HifiControls @@ -424,13 +422,13 @@ Rectangle { elide: Text.ElideRight size: 14 - color: model.link ? hifi.colors.blueHighlight : hifi.colors.baseGray + color: (model.link && root.supports3DHTML)? hifi.colors.blueHighlight : hifi.colors.baseGray verticalAlignment: Text.AlignVCenter MouseArea { anchors.fill: parent onClicked: { - if (model.link) { + if (model.link && root.supports3DHTML) { sendToScript({method: 'marketplace_open_link', link: model.link}); } } @@ -571,12 +569,14 @@ Rectangle { text: root.description size: 14 color: hifi.colors.lightGray - linkColor: hifi.colors.blueHighlight + linkColor: root.supports3DHTML ? hifi.colors.blueHighlight : hifi.colors.lightGray verticalAlignment: Text.AlignVCenter textFormat: Text.RichText wrapMode: Text.Wrap onLinkActivated: { - sendToScript({method: 'marketplace_open_link', link: link}); + if (root.supports3DHTML) { + sendToScript({method: 'marketplace_open_link', link: link}); + } } onHeightChanged: { footer.evalHeight(); } diff --git a/interface/resources/qml/hifi/tablet/BlocksWebView.qml b/interface/resources/qml/hifi/tablet/BlocksWebView.qml index 0267724ca1..eaed88ba01 100644 --- a/interface/resources/qml/hifi/tablet/BlocksWebView.qml +++ b/interface/resources/qml/hifi/tablet/BlocksWebView.qml @@ -1,6 +1,4 @@ import QtQuick 2.0 -import QtWebView 1.1 - import "../../controls" as Controls Controls.TabletWebView { diff --git a/interface/resources/qml/hifi/tablet/TabletMenu.qml b/interface/resources/qml/hifi/tablet/TabletMenu.qml index 2b5e26ab4d..7170fcdf5f 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenu.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenu.qml @@ -2,7 +2,6 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import QtQuick.Controls 1.4 import QtQml 2.2 -import QtWebChannel 1.0 import "." import stylesUit 1.0 diff --git a/interface/resources/qml/hifi/tablet/TabletWebView.qml b/interface/resources/qml/hifi/tablet/TabletWebView.qml index 9dcfdda5be..9eba7824e0 100644 --- a/interface/resources/qml/hifi/tablet/TabletWebView.qml +++ b/interface/resources/qml/hifi/tablet/TabletWebView.qml @@ -1,5 +1,4 @@ import QtQuick 2.0 - import "../../controls" as Controls Controls.TabletWebScreen { diff --git a/interface/resources/qml/hifi/tablet/WindowWebView.qml b/interface/resources/qml/hifi/tablet/WindowWebView.qml index 3dfe48a4e4..632ab712cb 100644 --- a/interface/resources/qml/hifi/tablet/WindowWebView.qml +++ b/interface/resources/qml/hifi/tablet/WindowWebView.qml @@ -1,5 +1,4 @@ import QtQuick 2.0 - import "../../controls" as Controls Controls.WebView { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d9eae20752..0073e16051 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4980,10 +4980,10 @@ void Application::idle() { // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing // details if we're in ExtraDebugging mode. However, the ::update() and its subcomponents will show their timing // details normally. -#ifndef Q_OS_ANDROID - bool showWarnings = getLogger()->extraDebugging(); -#else +#ifdef Q_OS_ANDROID bool showWarnings = false; +#else + bool showWarnings = getLogger()->extraDebugging(); #endif PerformanceWarning warn(showWarnings, "idle()"); @@ -8328,7 +8328,7 @@ void Application::toggleLogDialog() { bool keepOnTop =_keepLogWindowOnTop.get(); #ifdef Q_OS_WIN _logDialog = new LogDialog(keepOnTop ? qApp->getWindow() : nullptr, getLogger()); -#else +#elif !defined(Q_OS_ANDROID) _logDialog = new LogDialog(nullptr, getLogger()); if (keepOnTop) { diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index 84325da473..db74b34d91 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -294,13 +294,6 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename } mapping.insert(JOINT_FIELD, joints); - - if (!mapping.contains(FREE_JOINT_FIELD)) { - mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); - } // If there are no blendshape mappings, and we detect that this is likely a mixamo file, // then we can add the default mixamo to "faceshift" mappings diff --git a/interface/src/ModelPropertiesDialog.cpp b/interface/src/ModelPropertiesDialog.cpp index 1bdb170b60..d67341990d 100644 --- a/interface/src/ModelPropertiesDialog.cpp +++ b/interface/src/ModelPropertiesDialog.cpp @@ -58,11 +58,6 @@ _hfmModel(hfmModel) form->addRow("Left Hand Joint:", _leftHandJoint = createJointBox()); form->addRow("Right Hand Joint:", _rightHandJoint = createJointBox()); - form->addRow("Free Joints:", _freeJoints = new QVBoxLayout()); - QPushButton* newFreeJoint = new QPushButton("New Free Joint"); - _freeJoints->addWidget(newFreeJoint); - connect(newFreeJoint, SIGNAL(clicked(bool)), SLOT(createNewFreeJoint())); - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Reset); connect(buttons, SIGNAL(accepted()), SLOT(accept())); @@ -102,11 +97,6 @@ QVariantHash ModelPropertiesDialog::getMapping() const { insertJointMapping(joints, "jointLeftHand", _leftHandJoint->currentText()); insertJointMapping(joints, "jointRightHand", _rightHandJoint->currentText()); - mapping.remove(FREE_JOINT_FIELD); - for (int i = 0; i < _freeJoints->count() - 1; i++) { - QComboBox* box = static_cast(_freeJoints->itemAt(i)->widget()->layout()->itemAt(0)->widget()); - mapping.insertMulti(FREE_JOINT_FIELD, box->currentText()); - } mapping.insert(JOINT_FIELD, joints); return mapping; @@ -133,16 +123,6 @@ void ModelPropertiesDialog::reset() { setJointText(_headJoint, jointHash.value("jointHead").toString()); setJointText(_leftHandJoint, jointHash.value("jointLeftHand").toString()); setJointText(_rightHandJoint, jointHash.value("jointRightHand").toString()); - - while (_freeJoints->count() > 1) { - delete _freeJoints->itemAt(0)->widget(); - } - foreach (const QVariant& joint, _originalMapping.values(FREE_JOINT_FIELD)) { - QString jointName = joint.toString(); - if (_hfmModel.jointIndices.contains(jointName)) { - createNewFreeJoint(jointName); - } - } } void ModelPropertiesDialog::chooseTextureDirectory() { @@ -176,20 +156,6 @@ void ModelPropertiesDialog::updatePivotJoint() { _pivotJoint->setEnabled(!_pivotAboutCenter->isChecked()); } -void ModelPropertiesDialog::createNewFreeJoint(const QString& joint) { - QWidget* freeJoint = new QWidget(); - QHBoxLayout* freeJointLayout = new QHBoxLayout(); - freeJointLayout->setContentsMargins(QMargins()); - freeJoint->setLayout(freeJointLayout); - QComboBox* jointBox = createJointBox(false); - jointBox->setCurrentText(joint); - freeJointLayout->addWidget(jointBox, 1); - QPushButton* deleteJoint = new QPushButton("Delete"); - freeJointLayout->addWidget(deleteJoint); - freeJoint->connect(deleteJoint, SIGNAL(clicked(bool)), SLOT(deleteLater())); - _freeJoints->insertWidget(_freeJoints->count() - 1, freeJoint); -} - QComboBox* ModelPropertiesDialog::createJointBox(bool withNone) const { QComboBox* box = new QComboBox(); if (withNone) { diff --git a/interface/src/ModelPropertiesDialog.h b/interface/src/ModelPropertiesDialog.h index 7019d239ff..8cf9bd5248 100644 --- a/interface/src/ModelPropertiesDialog.h +++ b/interface/src/ModelPropertiesDialog.h @@ -39,7 +39,6 @@ private slots: void chooseTextureDirectory(); void chooseScriptDirectory(); void updatePivotJoint(); - void createNewFreeJoint(const QString& joint = QString()); private: QComboBox* createJointBox(bool withNone = true) const; @@ -66,7 +65,6 @@ private: QComboBox* _headJoint = nullptr; QComboBox* _leftHandJoint = nullptr; QComboBox* _rightHandJoint = nullptr; - QVBoxLayout* _freeJoints = nullptr; }; #endif // hifi_ModelPropertiesDialog_h diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 5fb9c9a0ee..a1826076fa 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -569,7 +569,7 @@ void AvatarActionHold::lateAvatarUpdate(const AnimPose& prePhysicsRoomPose, cons } btTransform worldTrans = rigidBody->getWorldTransform(); - AnimPose worldBodyPose(glm::vec3(1), bulletToGLM(worldTrans.getRotation()), bulletToGLM(worldTrans.getOrigin())); + AnimPose worldBodyPose(1.0f, bulletToGLM(worldTrans.getRotation()), bulletToGLM(worldTrans.getOrigin())); // transform the body transform into sensor space with the prePhysics sensor-to-world matrix. // then transform it back into world uisng the postAvatarUpdate sensor-to-world matrix. diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 1eb87c16f0..0b33220c01 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -652,28 +652,25 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic PROFILE_RANGE(simulation_physics, __FUNCTION__); - float distance = (float)INT_MAX; // with FLT_MAX bullet rayTest does not return results + float bulletDistance = (float)INT_MAX; // with FLT_MAX bullet rayTest does not return results glm::vec3 rayDirection = glm::normalize(ray.direction); - std::vector physicsResults = _myAvatar->getCharacterController()->rayTest(glmToBullet(ray.origin), glmToBullet(rayDirection), distance, QVector()); + std::vector physicsResults = _myAvatar->getCharacterController()->rayTest(glmToBullet(ray.origin), glmToBullet(rayDirection), bulletDistance, QVector()); if (physicsResults.size() > 0) { glm::vec3 rayDirectionInv = { rayDirection.x != 0.0f ? 1.0f / rayDirection.x : INFINITY, rayDirection.y != 0.0f ? 1.0f / rayDirection.y : INFINITY, rayDirection.z != 0.0f ? 1.0f / rayDirection.z : INFINITY }; - MyCharacterController::RayAvatarResult rayAvatarResult; - AvatarPointer avatar = nullptr; - - BoxFace face = BoxFace::UNKNOWN_FACE; - glm::vec3 surfaceNormal; - QVariantMap extraInfo; - for (auto &hit : physicsResults) { auto avatarID = hit._intersectWithAvatar; if ((avatarsToInclude.size() > 0 && !avatarsToInclude.contains(avatarID)) || (avatarsToDiscard.size() > 0 && avatarsToDiscard.contains(avatarID))) { continue; } - + + MyCharacterController::RayAvatarResult rayAvatarResult; + BoxFace face = BoxFace::UNKNOWN_FACE; + QVariantMap extraInfo; + AvatarPointer avatar = nullptr; if (_myAvatar->getSessionUUID() != avatarID) { auto avatarMap = getHashCopy(); AvatarHash::iterator itr = avatarMap.find(avatarID); @@ -683,46 +680,45 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic } else { avatar = _myAvatar; } + if (!hit._isBound) { rayAvatarResult = hit; } else if (avatar) { auto &multiSpheres = avatar->getMultiSphereShapes(); if (multiSpheres.size() > 0) { - std::vector boxHits; + MyCharacterController::RayAvatarResult boxHit; + boxHit._distance = FLT_MAX; + for (size_t i = 0; i < hit._boundJoints.size(); i++) { assert(hit._boundJoints[i] < multiSpheres.size()); auto &mSphere = multiSpheres[hit._boundJoints[i]]; if (mSphere.isValid()) { float boundDistance = FLT_MAX; - BoxFace face; - glm::vec3 surfaceNormal; + BoxFace boundFace = BoxFace::UNKNOWN_FACE; + glm::vec3 boundSurfaceNormal; auto &bbox = mSphere.getBoundingBox(); - if (bbox.findRayIntersection(ray.origin, rayDirection, rayDirectionInv, boundDistance, face, surfaceNormal)) { - MyCharacterController::RayAvatarResult boxHit; - boxHit._distance = boundDistance; - boxHit._intersect = true; - boxHit._intersectionNormal = surfaceNormal; - boxHit._intersectionPoint = ray.origin + boundDistance * rayDirection; - boxHit._intersectWithAvatar = avatarID; - boxHit._intersectWithJoint = mSphere.getJointIndex(); - boxHits.push_back(boxHit); + if (bbox.findRayIntersection(ray.origin, rayDirection, rayDirectionInv, boundDistance, boundFace, boundSurfaceNormal)) { + if (boundDistance < boxHit._distance) { + boxHit._intersect = true; + boxHit._intersectWithAvatar = avatarID; + boxHit._intersectWithJoint = mSphere.getJointIndex(); + boxHit._distance = boundDistance; + boxHit._intersectionPoint = ray.origin + boundDistance * rayDirection; + boxHit._intersectionNormal = boundSurfaceNormal; + face = boundFace; + } } } } - if (boxHits.size() > 0) { - if (boxHits.size() > 1) { - std::sort(boxHits.begin(), boxHits.end(), [](const MyCharacterController::RayAvatarResult& hitA, - const MyCharacterController::RayAvatarResult& hitB) { - return hitA._distance < hitB._distance; - }); - } - rayAvatarResult = boxHits[0]; + if (boxHit._distance < FLT_MAX) { + rayAvatarResult = boxHit; } } } - if (pickAgainstMesh) { + + if (rayAvatarResult._intersect && pickAgainstMesh) { glm::vec3 localRayOrigin = avatar->worldToJointPoint(ray.origin, rayAvatarResult._intersectWithJoint); - glm::vec3 localRayPoint = avatar->worldToJointPoint(ray.origin + rayDirection, rayAvatarResult._intersectWithJoint); + glm::vec3 localRayPoint = avatar->worldToJointPoint(ray.origin + rayAvatarResult._distance * rayDirection, rayAvatarResult._intersectWithJoint); auto avatarOrientation = avatar->getWorldOrientation(); auto avatarPosition = avatar->getWorldPosition(); @@ -732,31 +728,37 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic auto defaultFrameRayOrigin = jointPosition + jointOrientation * localRayOrigin; auto defaultFrameRayPoint = jointPosition + jointOrientation * localRayPoint; - auto defaultFrameRayDirection = defaultFrameRayPoint - defaultFrameRayOrigin; + auto defaultFrameRayDirection = glm::normalize(defaultFrameRayPoint - defaultFrameRayOrigin); - if (avatar->getSkeletonModel()->findRayIntersectionAgainstSubMeshes(defaultFrameRayOrigin, defaultFrameRayDirection, distance, face, surfaceNormal, extraInfo, true, false)) { - auto newDistance = glm::length(vec3FromVariant(extraInfo["worldIntersectionPoint"]) - defaultFrameRayOrigin); - rayAvatarResult._distance = newDistance; - rayAvatarResult._intersectionPoint = ray.origin + newDistance * rayDirection; - rayAvatarResult._intersectionNormal = surfaceNormal; - extraInfo["worldIntersectionPoint"] = vec3toVariant(rayAvatarResult._intersectionPoint); - break; + float subMeshDistance = FLT_MAX; + BoxFace subMeshFace = BoxFace::UNKNOWN_FACE; + glm::vec3 subMeshSurfaceNormal; + QVariantMap subMeshExtraInfo; + if (avatar->getSkeletonModel()->findRayIntersectionAgainstSubMeshes(defaultFrameRayOrigin, defaultFrameRayDirection, subMeshDistance, subMeshFace, subMeshSurfaceNormal, subMeshExtraInfo, true, false)) { + rayAvatarResult._distance = subMeshDistance; + rayAvatarResult._intersectionPoint = ray.origin + subMeshDistance * rayDirection; + rayAvatarResult._intersectionNormal = subMeshSurfaceNormal; + face = subMeshFace; + extraInfo = subMeshExtraInfo; + } else { + rayAvatarResult._intersect = false; } - } else if (rayAvatarResult._intersect){ + } + + if (rayAvatarResult._intersect) { + result.intersects = true; + result.avatarID = rayAvatarResult._intersectWithAvatar; + result.distance = rayAvatarResult._distance; + result.face = face; + result.intersection = ray.origin + rayAvatarResult._distance * rayDirection; + result.surfaceNormal = rayAvatarResult._intersectionNormal; + result.jointIndex = rayAvatarResult._intersectWithJoint; + result.extraInfo = extraInfo; break; } } - if (rayAvatarResult._intersect) { - result.intersects = true; - result.avatarID = rayAvatarResult._intersectWithAvatar; - result.distance = rayAvatarResult._distance; - result.surfaceNormal = rayAvatarResult._intersectionNormal; - result.jointIndex = rayAvatarResult._intersectWithJoint; - result.intersection = ray.origin + rayAvatarResult._distance * rayDirection; - result.extraInfo = extraInfo; - result.face = face; - } } + return result; } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index b198edc245..6fef47da8e 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -915,14 +915,9 @@ void MyAvatar::simulate(float deltaTime, bool inView) { auto entityTreeRenderer = qApp->getEntities(); EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; if (entityTree) { - bool zoneAllowsFlying = true; - bool collisionlessAllowed = true; + std::pair zoneInteractionProperties; entityTree->withWriteLock([&] { - std::shared_ptr zone = entityTreeRenderer->myAvatarZone(); - if (zone) { - zoneAllowsFlying = zone->getFlyingAllowed(); - collisionlessAllowed = zone->getGhostingAllowed(); - } + zoneInteractionProperties = entityTreeRenderer->getZoneInteractionProperties(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); forEachDescendant([&](SpatiallyNestablePointer object) { // we need to update attached queryAACubes in our own local tree so point-select always works @@ -935,6 +930,8 @@ void MyAvatar::simulate(float deltaTime, bool inView) { }); }); bool isPhysicsEnabled = qApp->isPhysicsEnabled(); + bool zoneAllowsFlying = zoneInteractionProperties.first; + bool collisionlessAllowed = zoneInteractionProperties.second; _characterController.setFlyingAllowed((zoneAllowsFlying && _enableFlying) || !isPhysicsEnabled); _characterController.setCollisionlessAllowed(collisionlessAllowed); } @@ -2983,7 +2980,7 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { auto animSkeleton = _skeletonModel->getRig().getAnimSkeleton(); // the rig is in the skeletonModel frame - AnimPose xform(glm::vec3(1), _skeletonModel->getRotation(), _skeletonModel->getTranslation()); + AnimPose xform(1.0f, _skeletonModel->getRotation(), _skeletonModel->getTranslation()); if (_enableDebugDrawDefaultPose && animSkeleton) { glm::vec4 gray(0.2f, 0.2f, 0.2f, 0.2f); @@ -3028,7 +3025,7 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { updateHoldActions(_prePhysicsRoomPose, postUpdateRoomPose); if (_enableDebugDrawDetailedCollision) { - AnimPose rigToWorldPose(glm::vec3(1.0f), getWorldOrientation() * Quaternions::Y_180, getWorldPosition()); + AnimPose rigToWorldPose(1.0f, getWorldOrientation() * Quaternions::Y_180, getWorldPosition()); const int NUM_DEBUG_COLORS = 8; const glm::vec4 DEBUG_COLORS[NUM_DEBUG_COLORS] = { glm::vec4(1.0f, 1.0f, 1.0f, 1.0f), @@ -4824,7 +4821,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat swingTwistDecomposition(hipsinWorldSpace, avatarUpWorld, resultingSwingInWorld, resultingTwistInWorld); // remove scale present from sensorToWorldMatrix - followWorldPose.scale() = glm::vec3(1.0f); + followWorldPose.scale() = 1.0f; if (isActive(Rotation)) { //use the hmd reading for the hips follow diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 26d69841d0..253cc891ee 100755 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -41,7 +41,7 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { if (myAvatar->isJointPinned(hipsIndex)) { Transform avatarTransform = myAvatar->getTransform(); AnimPose result = AnimPose(worldToSensorMat * avatarTransform.getMatrix() * Matrices::Y_180); - result.scale() = glm::vec3(1.0f, 1.0f, 1.0f); + result.scale() = 1.0f; return result; } @@ -108,7 +108,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { Rig::ControllerParameters params; - AnimPose avatarToRigPose(glm::vec3(1.0f), Quaternions::Y_180, glm::vec3(0.0f)); + AnimPose avatarToRigPose(1.0f, Quaternions::Y_180, glm::vec3(0.0f)); glm::mat4 rigToAvatarMatrix = Matrices::Y_180; glm::mat4 avatarToWorldMatrix = createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()); @@ -127,7 +127,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { // preMult 180 is necessary to convert from avatar to rig coordinates. // postMult 180 is necessary to convert head from -z forward to z forward. glm::quat headRot = Quaternions::Y_180 * head->getFinalOrientationInLocalFrame() * Quaternions::Y_180; - params.primaryControllerPoses[Rig::PrimaryControllerType_Head] = AnimPose(glm::vec3(1.0f), headRot, glm::vec3(0.0f)); + params.primaryControllerPoses[Rig::PrimaryControllerType_Head] = AnimPose(1.0f, headRot, glm::vec3(0.0f)); params.primaryControllerFlags[Rig::PrimaryControllerType_Head] = 0; } diff --git a/interface/src/commerce/QmlMarketplace.cpp b/interface/src/commerce/QmlMarketplace.cpp index 07a9e570bd..23ba418a2d 100644 --- a/interface/src/commerce/QmlMarketplace.cpp +++ b/interface/src/commerce/QmlMarketplace.cpp @@ -72,20 +72,17 @@ void QmlMarketplace::getMarketplaceItems( void QmlMarketplace::getMarketplaceItem(const QString& marketplaceItemId) { QString endpoint = QString("items/") + marketplaceItemId; - QUrlQuery request; - send(endpoint, "getMarketplaceItemSuccess", "getMarketplaceItemFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::Optional, request); + send(endpoint, "getMarketplaceItemSuccess", "getMarketplaceItemFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::Optional); } void QmlMarketplace::marketplaceItemLike(const QString& marketplaceItemId, const bool like) { QString endpoint = QString("items/") + marketplaceItemId + "/like"; - QUrlQuery request; - send(endpoint, "marketplaceItemLikeSuccess", "marketplaceItemLikeFailure", like ? QNetworkAccessManager::PostOperation : QNetworkAccessManager::DeleteOperation, AccountManagerAuth::Required, request); + send(endpoint, "marketplaceItemLikeSuccess", "marketplaceItemLikeFailure", like ? QNetworkAccessManager::PostOperation : QNetworkAccessManager::DeleteOperation, AccountManagerAuth::Required); } void QmlMarketplace::getMarketplaceCategories() { QString endpoint = "categories"; - QUrlQuery request; - send(endpoint, "getMarketplaceCategoriesSuccess", "getMarketplaceCategoriesFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::None, request); + send(endpoint, "getMarketplaceCategoriesSuccess", "getMarketplaceCategoriesFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::None); } @@ -94,14 +91,13 @@ void QmlMarketplace::send(const QString& endpoint, const QString& success, const const QString URL = "/api/v1/marketplace/"; JSONCallbackParameters callbackParams(this, success, fail); - accountManager->sendRequest(URL + endpoint, + accountManager->sendRequest(URL + endpoint + "?" + request.toString(), authType, method, callbackParams, QByteArray(), NULL, - QVariantMap(), - request); + QVariantMap()); } diff --git a/interface/src/commerce/QmlMarketplace.h b/interface/src/commerce/QmlMarketplace.h index f954198371..5794d4f53c 100644 --- a/interface/src/commerce/QmlMarketplace.h +++ b/interface/src/commerce/QmlMarketplace.h @@ -60,7 +60,12 @@ signals: void marketplaceItemLikeResult(QJsonObject result); private: - void send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, const QUrlQuery & request); + void send(const QString& endpoint, + const QString& success, + const QString& fail, + QNetworkAccessManager::Operation method, + AccountManagerAuth::Type authType, + const QUrlQuery& request = QUrlQuery()); QJsonObject apiResponse(const QString& label, QNetworkReply* reply); QJsonObject failResponse(const QString& label, QNetworkReply* reply); }; diff --git a/interface/src/octree/OctreePacketProcessor.cpp b/interface/src/octree/OctreePacketProcessor.cpp index c6e0e50bc6..62ef9cd875 100644 --- a/interface/src/octree/OctreePacketProcessor.cpp +++ b/interface/src/octree/OctreePacketProcessor.cpp @@ -44,7 +44,7 @@ void OctreePacketProcessor::processPacket(QSharedPointer messag qDebug("OctreePacketProcessor::processPacket() packets to process=%d", packetsToProcessCount()); } #endif - + bool wasStatsPacket = false; PacketType octreePacketType = message->getType(); diff --git a/interface/src/raypick/RayPick.cpp b/interface/src/raypick/RayPick.cpp index 24ba4435e2..d476357bab 100644 --- a/interface/src/raypick/RayPick.cpp +++ b/interface/src/raypick/RayPick.cpp @@ -56,7 +56,8 @@ PickResultPointer RayPick::getOverlayIntersection(const PickRay& pick) { } PickResultPointer RayPick::getAvatarIntersection(const PickRay& pick) { - RayToAvatarIntersectionResult avatarRes = DependencyManager::get()->findRayIntersectionVector(pick, getIncludeItemsAs(), getIgnoreItemsAs(), true); + bool precisionPicking = !(getFilter().isCoarse() || DependencyManager::get()->getForceCoarsePicking()); + RayToAvatarIntersectionResult avatarRes = DependencyManager::get()->findRayIntersectionVector(pick, getIncludeItemsAs(), getIgnoreItemsAs(), precisionPicking); if (avatarRes.intersects) { return std::make_shared(IntersectionType::AVATAR, avatarRes.avatarID, avatarRes.distance, avatarRes.intersection, pick, avatarRes.surfaceNormal, avatarRes.extraInfo); } else { diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 524170c7a5..fb64dbe098 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -15,6 +15,7 @@ #include "Application.h" #include "AudioClient.h" +#include "AudioHelpers.h" #include "ui/AvatarInputs.h" using namespace scripting; @@ -26,26 +27,9 @@ QString Audio::HMD { "VR" }; Setting::Handle enableNoiseReductionSetting { QStringList { Audio::AUDIO, "NoiseReduction" }, true }; float Audio::loudnessToLevel(float loudness) { - const float LOG2 = log(2.0f); - const float METER_LOUDNESS_SCALE = 2.8f / 5.0f; - const float LOG2_LOUDNESS_FLOOR = 11.0f; - - float level = 0.0f; - - loudness += 1.0f; - float log2loudness = logf(loudness) / LOG2; - - if (log2loudness <= LOG2_LOUDNESS_FLOOR) { - level = (log2loudness / LOG2_LOUDNESS_FLOOR) * METER_LOUDNESS_SCALE; - } else { - level = (log2loudness - (LOG2_LOUDNESS_FLOOR - 1.0f)) * METER_LOUDNESS_SCALE; - } - - if (level > 1.0f) { - level = 1.0; - } - - return level; + float level = 6.02059991f * fastLog2f(loudness); // level in dBFS + level = (level + 48.0f) * (1/39.0f); // map [-48, -9] dBFS to [0, 1] + return glm::clamp(level, 0.0f, 1.0f); } Audio::Audio() : _devices(_contextIsHMD) { @@ -150,18 +134,33 @@ float Audio::getInputLevel() const { }); } -void Audio::onInputLoudnessChanged(float loudness) { +bool Audio::isClipping() const { + return resultWithReadLock([&] { + return _isClipping; + }); +} + +void Audio::onInputLoudnessChanged(float loudness, bool isClipping) { float level = loudnessToLevel(loudness); - bool changed = false; + bool levelChanged = false; + bool isClippingChanged = false; + withWriteLock([&] { if (_inputLevel != level) { _inputLevel = level; - changed = true; + levelChanged = true; + } + if (_isClipping != isClipping) { + _isClipping = isClipping; + isClippingChanged = true; } }); - if (changed) { + if (levelChanged) { emit inputLevelChanged(level); } + if (isClippingChanged) { + emit clippingChanged(isClipping); + } } QString Audio::getContext() const { diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 4c4bf6dd60..63758d0633 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -41,6 +41,7 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { * above the noise floor. * @property {number} inputLevel - The loudness of the audio input, range 0.0 (no sound) – * 1.0 (the onset of clipping). Read-only. + * @property {boolean} clipping - true if the audio input is clipping, otherwise false. * @property {number} inputVolume - Adjusts the volume of the input audio; range 0.01.0. * If set to a value, the resulting value depends on the input device: for example, the volume can't be changed on some * devices, and others might only support values of 0.0 and 1.0. @@ -58,6 +59,7 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool noiseReduction READ noiseReductionEnabled WRITE enableNoiseReduction NOTIFY noiseReductionChanged) Q_PROPERTY(float inputVolume READ getInputVolume WRITE setInputVolume NOTIFY inputVolumeChanged) Q_PROPERTY(float inputLevel READ getInputLevel NOTIFY inputLevelChanged) + Q_PROPERTY(bool clipping READ isClipping NOTIFY clippingChanged) Q_PROPERTY(QString context READ getContext NOTIFY contextChanged) Q_PROPERTY(AudioDevices* devices READ getDevices NOTIFY nop) @@ -74,6 +76,7 @@ public: bool noiseReductionEnabled() const; float getInputVolume() const; float getInputLevel() const; + bool isClipping() const; QString getContext() const; void showMicMeter(bool show); @@ -217,6 +220,14 @@ signals: */ void inputLevelChanged(float level); + /**jsdoc + * Triggered when the clipping state of the input audio changes. + * @function Audio.clippingChanged + * @param {boolean} isClipping - true if the audio input is clipping, otherwise false. + * @returns {Signal} + */ + void clippingChanged(bool isClipping); + /**jsdoc * Triggered when the current context of the audio changes. * @function Audio.contextChanged @@ -237,7 +248,7 @@ private slots: void setMuted(bool muted); void enableNoiseReduction(bool enable); void setInputVolume(float volume); - void onInputLoudnessChanged(float loudness); + void onInputLoudnessChanged(float loudness, bool isClipping); protected: // Audio must live on a separate thread from AudioClient to avoid deadlocks @@ -247,6 +258,7 @@ private: float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; + bool _isClipping { false }; bool _isMuted { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _contextIsHMD { false }; diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 1adc04ee1b..71b876ff8c 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -140,10 +140,10 @@ void AnimClip::copyFromNetworkAnim() { postRot = animSkeleton.getPostRotationPose(animJoint); // cancel out scale - preRot.scale() = glm::vec3(1.0f); - postRot.scale() = glm::vec3(1.0f); + preRot.scale() = 1.0f; + postRot.scale() = 1.0f; - AnimPose rot(glm::vec3(1.0f), hfmAnimRot, glm::vec3()); + AnimPose rot(1.0f, hfmAnimRot, glm::vec3()); // adjust translation offsets, so large translation animatons on the reference skeleton // will be adjusted when played on a skeleton with short limbs. @@ -155,7 +155,7 @@ void AnimClip::copyFromNetworkAnim() { boneLengthScale = glm::length(relDefaultPose.trans()) / glm::length(hfmZeroTrans); } - AnimPose trans = AnimPose(glm::vec3(1.0f), glm::quat(), relDefaultPose.trans() + boneLengthScale * (hfmAnimTrans - hfmZeroTrans)); + AnimPose trans = AnimPose(1.0f, glm::quat(), relDefaultPose.trans() + boneLengthScale * (hfmAnimTrans - hfmZeroTrans)); _anim[frame][skeletonJoint] = trans * preRot * rot * postRot; } diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index a1809f3438..7af9e81889 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -552,7 +552,7 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const AnimPose accum = absolutePoses[_hipsIndex]; AnimPose baseParentPose = absolutePoses[_hipsIndex]; for (int i = (int)chainDepth - 1; i >= 0; i--) { - accum = accum * AnimPose(glm::vec3(1.0f), jointChainInfoOut.jointInfoVec[i].rot, jointChainInfoOut.jointInfoVec[i].trans); + accum = accum * AnimPose(1.0f, jointChainInfoOut.jointInfoVec[i].rot, jointChainInfoOut.jointInfoVec[i].trans); postAbsPoses[i] = accum; if (jointChainInfoOut.jointInfoVec[i].jointIndex == topJointIndex) { topChainIndex = i; @@ -734,7 +734,7 @@ void AnimInverseKinematics::computeAndCacheSplineJointInfosForIKTarget(const Ani glm::mat3 m(u, v, glm::cross(u, v)); glm::quat rot = glm::normalize(glm::quat_cast(m)); - AnimPose pose(glm::vec3(1.0f), rot, spline(t)); + AnimPose pose(1.0f, rot, spline(t)); AnimPose offsetPose = pose.inverse() * defaultPose; SplineJointInfo splineJointInfo = { index, ratio, offsetPose }; @@ -767,7 +767,7 @@ void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, co const int baseIndex = _hipsIndex; // build spline from tip to base - AnimPose tipPose = AnimPose(glm::vec3(1.0f), target.getRotation(), target.getTranslation()); + AnimPose tipPose = AnimPose(1.0f, target.getRotation(), target.getTranslation()); AnimPose basePose = absolutePoses[baseIndex]; CubicHermiteSplineFunctorWithArcLength spline; if (target.getIndex() == _headIndex) { @@ -815,7 +815,7 @@ void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, co glm::mat3 m(u, v, glm::cross(u, v)); glm::quat rot = glm::normalize(glm::quat_cast(m)); - AnimPose desiredAbsPose = AnimPose(glm::vec3(1.0f), rot, trans) * splineJointInfo.offsetPose; + AnimPose desiredAbsPose = AnimPose(1.0f, rot, trans) * splineJointInfo.offsetPose; // apply flex coefficent AnimPose flexedAbsPose; @@ -965,7 +965,7 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars } _relativePoses[_hipsIndex] = parentAbsPose.inverse() * absPose; - _relativePoses[_hipsIndex].scale() = glm::vec3(1.0f); + _relativePoses[_hipsIndex].scale() = 1.0f; } // if there is an active jointChainInfo for the hips store the post shifted hips into it. @@ -1753,7 +1753,7 @@ void AnimInverseKinematics::setSecondaryTargets(const AnimContext& context) { AnimPose rigToGeometryPose = AnimPose(glm::inverse(context.getGeometryToRigMatrix())); for (auto& iter : _secondaryTargetsInRigFrame) { AnimPose absPose = rigToGeometryPose * iter.second; - absPose.scale() = glm::vec3(1.0f); + absPose.scale() = 1.0f; AnimPose parentAbsPose; int parentIndex = _skeleton->getParentIndex(iter.first); @@ -1825,7 +1825,7 @@ void AnimInverseKinematics::debugDrawSpineSplines(const AnimContext& context, co const int baseIndex = _hipsIndex; // build spline - AnimPose tipPose = AnimPose(glm::vec3(1.0f), target.getRotation(), target.getTranslation()); + AnimPose tipPose = AnimPose(1.0f, target.getRotation(), target.getTranslation()); AnimPose basePose = _skeleton->getAbsolutePose(baseIndex, _relativePoses); CubicHermiteSplineFunctorWithArcLength spline; diff --git a/libraries/animation/src/AnimManipulator.cpp b/libraries/animation/src/AnimManipulator.cpp index 1146cbb19a..c75c9865bb 100644 --- a/libraries/animation/src/AnimManipulator.cpp +++ b/libraries/animation/src/AnimManipulator.cpp @@ -172,5 +172,5 @@ AnimPose AnimManipulator::computeRelativePoseFromJointVar(const AnimVariantMap& break; } - return AnimPose(glm::vec3(1), relRot, relTrans); + return AnimPose(1.0f, relRot, relTrans); } diff --git a/libraries/animation/src/AnimPoleVectorConstraint.cpp b/libraries/animation/src/AnimPoleVectorConstraint.cpp index f017fe2348..96e4e67261 100644 --- a/libraries/animation/src/AnimPoleVectorConstraint.cpp +++ b/libraries/animation/src/AnimPoleVectorConstraint.cpp @@ -95,7 +95,7 @@ const AnimPoseVec& AnimPoleVectorConstraint::evaluate(const AnimVariantMap& anim AnimPose tipPose = ikChain.getAbsolutePoseFromJointIndex(_tipJointIndex); // Look up refVector from animVars, make sure to convert into geom space. - glm::vec3 refVector = midPose.xformVectorFast(_referenceVector); + glm::vec3 refVector = midPose.xformVector(_referenceVector); float refVectorLength = glm::length(refVector); glm::vec3 axis = basePose.trans() - tipPose.trans(); diff --git a/libraries/animation/src/AnimPose.cpp b/libraries/animation/src/AnimPose.cpp index d77514e691..366d863c3d 100644 --- a/libraries/animation/src/AnimPose.cpp +++ b/libraries/animation/src/AnimPose.cpp @@ -14,15 +14,14 @@ #include #include "AnimUtil.h" -const AnimPose AnimPose::identity = AnimPose(glm::vec3(1.0f), - glm::quat(), - glm::vec3(0.0f)); +const AnimPose AnimPose::identity = AnimPose(1.0f, glm::quat(), glm::vec3(0.0f)); AnimPose::AnimPose(const glm::mat4& mat) { static const float EPSILON = 0.0001f; - _scale = extractScale(mat); + glm::vec3 scale = extractScale(mat); // quat_cast doesn't work so well with scaled matrices, so cancel it out. - glm::mat4 tmp = glm::scale(mat, 1.0f / _scale); + glm::mat4 tmp = glm::scale(mat, 1.0f / scale); + _scale = extractUniformScale(scale); _rot = glm::quat_cast(tmp); float lengthSquared = glm::length2(_rot); if (glm::abs(lengthSquared - 1.0f) > EPSILON) { @@ -40,29 +39,22 @@ glm::vec3 AnimPose::xformPoint(const glm::vec3& rhs) const { return *this * rhs; } -// really slow, but accurate for transforms with non-uniform scale glm::vec3 AnimPose::xformVector(const glm::vec3& rhs) const { - glm::vec3 xAxis = _rot * glm::vec3(_scale.x, 0.0f, 0.0f); - glm::vec3 yAxis = _rot * glm::vec3(0.0f, _scale.y, 0.0f); - glm::vec3 zAxis = _rot * glm::vec3(0.0f, 0.0f, _scale.z); - glm::mat3 mat(xAxis, yAxis, zAxis); - glm::mat3 transInvMat = glm::inverse(glm::transpose(mat)); - return transInvMat * rhs; -} - -// faster, but does not handle non-uniform scale correctly. -glm::vec3 AnimPose::xformVectorFast(const glm::vec3& rhs) const { return _rot * (_scale * rhs); } AnimPose AnimPose::operator*(const AnimPose& rhs) const { - glm::mat4 result; - glm_mat4u_mul(*this, rhs, result); - return AnimPose(result); + float scale = _scale * rhs._scale; + glm::quat rot = _rot * rhs._rot; + glm::vec3 trans = _trans + (_rot * (_scale * rhs._trans)); + return AnimPose(scale, rot, trans); } AnimPose AnimPose::inverse() const { - return AnimPose(glm::inverse(static_cast(*this))); + float invScale = 1.0f / _scale; + glm::quat invRot = glm::inverse(_rot); + glm::vec3 invTrans = invScale * (invRot * -_trans); + return AnimPose(invScale, invRot, invTrans); } // mirror about x-axis without applying negative scale. @@ -71,11 +63,10 @@ AnimPose AnimPose::mirror() const { } AnimPose::operator glm::mat4() const { - glm::vec3 xAxis = _rot * glm::vec3(_scale.x, 0.0f, 0.0f); - glm::vec3 yAxis = _rot * glm::vec3(0.0f, _scale.y, 0.0f); - glm::vec3 zAxis = _rot * glm::vec3(0.0f, 0.0f, _scale.z); - return glm::mat4(glm::vec4(xAxis, 0.0f), glm::vec4(yAxis, 0.0f), - glm::vec4(zAxis, 0.0f), glm::vec4(_trans, 1.0f)); + glm::vec3 xAxis = _rot * glm::vec3(_scale, 0.0f, 0.0f); + glm::vec3 yAxis = _rot * glm::vec3(0.0f, _scale, 0.0f); + glm::vec3 zAxis = _rot * glm::vec3(0.0f, 0.0f, _scale); + return glm::mat4(glm::vec4(xAxis, 0.0f), glm::vec4(yAxis, 0.0f), glm::vec4(zAxis, 0.0f), glm::vec4(_trans, 1.0f)); } void AnimPose::blend(const AnimPose& srcPose, float alpha) { diff --git a/libraries/animation/src/AnimPose.h b/libraries/animation/src/AnimPose.h index 1558a6b881..4d6dee1987 100644 --- a/libraries/animation/src/AnimPose.h +++ b/libraries/animation/src/AnimPose.h @@ -21,14 +21,13 @@ class AnimPose { public: AnimPose() {} explicit AnimPose(const glm::mat4& mat); - explicit AnimPose(const glm::quat& rotIn) : _scale(1.0f), _rot(rotIn), _trans(0.0f) {} - AnimPose(const glm::quat& rotIn, const glm::vec3& transIn) : _scale(1.0f), _rot(rotIn), _trans(transIn) {} - AnimPose(const glm::vec3& scaleIn, const glm::quat& rotIn, const glm::vec3& transIn) : _scale(scaleIn), _rot(rotIn), _trans(transIn) {} + explicit AnimPose(const glm::quat& rotIn) : _rot(rotIn), _trans(0.0f), _scale(1.0f) {} + AnimPose(const glm::quat& rotIn, const glm::vec3& transIn) : _rot(rotIn), _trans(transIn), _scale(1.0f) {} + AnimPose(float scaleIn, const glm::quat& rotIn, const glm::vec3& transIn) : _rot(rotIn), _trans(transIn), _scale(scaleIn) {} static const AnimPose identity; glm::vec3 xformPoint(const glm::vec3& rhs) const; glm::vec3 xformVector(const glm::vec3& rhs) const; // really slow, but accurate for transforms with non-uniform scale - glm::vec3 xformVectorFast(const glm::vec3& rhs) const; // faster, but does not handle non-uniform scale correctly. glm::vec3 operator*(const glm::vec3& rhs) const; // same as xformPoint AnimPose operator*(const AnimPose& rhs) const; @@ -37,8 +36,8 @@ public: AnimPose mirror() const; operator glm::mat4() const; - const glm::vec3& scale() const { return _scale; } - glm::vec3& scale() { return _scale; } + float scale() const { return _scale; } + float& scale() { return _scale; } const glm::quat& rot() const { return _rot; } glm::quat& rot() { return _rot; } @@ -50,13 +49,13 @@ public: private: friend QDebug operator<<(QDebug debug, const AnimPose& pose); - glm::vec3 _scale { 1.0f }; glm::quat _rot; glm::vec3 _trans; + float _scale { 1.0f }; // uniform scale only. }; inline QDebug operator<<(QDebug debug, const AnimPose& pose) { - debug << "AnimPose, trans = (" << pose.trans().x << pose.trans().y << pose.trans().z << "), rot = (" << pose.rot().x << pose.rot().y << pose.rot().z << pose.rot().w << "), scale = (" << pose.scale().x << pose.scale().y << pose.scale().z << ")"; + debug << "AnimPose, trans = (" << pose.trans().x << pose.trans().y << pose.trans().z << "), rot = (" << pose.rot().x << pose.rot().y << pose.rot().z << pose.rot().w << "), scale =" << pose.scale(); return debug; } diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index cc48308f17..03e3ac6ebd 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -76,7 +76,7 @@ int AnimSkeleton::getChainDepth(int jointIndex) const { int index = jointIndex; do { chainDepth++; - index = _joints[index].parentIndex; + index = _parentIndices[index]; } while (index != -1); return chainDepth; } else { @@ -102,17 +102,12 @@ const AnimPose& AnimSkeleton::getPostRotationPose(int jointIndex) const { return _relativePostRotationPoses[jointIndex]; } -int AnimSkeleton::getParentIndex(int jointIndex) const { - return _joints[jointIndex].parentIndex; -} - std::vector AnimSkeleton::getChildrenOfJoint(int jointIndex) const { // Children and grandchildren, etc. std::vector result; if (jointIndex != -1) { - for (int i = jointIndex + 1; i < (int)_joints.size(); i++) { - if (_joints[i].parentIndex == jointIndex - || (std::find(result.begin(), result.end(), _joints[i].parentIndex) != result.end())) { + for (int i = jointIndex + 1; i < (int)_parentIndices.size(); i++) { + if (_parentIndices[i] == jointIndex || (std::find(result.begin(), result.end(), _parentIndices[i]) != result.end())) { result.push_back(i); } } @@ -128,7 +123,7 @@ AnimPose AnimSkeleton::getAbsolutePose(int jointIndex, const AnimPoseVec& relati if (jointIndex < 0 || jointIndex >= (int)relativePoses.size() || jointIndex >= _jointsSize) { return AnimPose::identity; } else { - return getAbsolutePose(_joints[jointIndex].parentIndex, relativePoses) * relativePoses[jointIndex]; + return getAbsolutePose(_parentIndices[jointIndex], relativePoses) * relativePoses[jointIndex]; } } @@ -136,7 +131,7 @@ void AnimSkeleton::convertRelativePosesToAbsolute(AnimPoseVec& poses) const { // poses start off relative and leave in absolute frame int lastIndex = std::min((int)poses.size(), _jointsSize); for (int i = 0; i < lastIndex; ++i) { - int parentIndex = _joints[i].parentIndex; + int parentIndex = _parentIndices[i]; if (parentIndex != -1) { poses[i] = poses[parentIndex] * poses[i]; } @@ -147,7 +142,7 @@ void AnimSkeleton::convertAbsolutePosesToRelative(AnimPoseVec& poses) const { // poses start off absolute and leave in relative frame int lastIndex = std::min((int)poses.size(), _jointsSize); for (int i = lastIndex - 1; i >= 0; --i) { - int parentIndex = _joints[i].parentIndex; + int parentIndex = _parentIndices[i]; if (parentIndex != -1) { poses[i] = poses[parentIndex].inverse() * poses[i]; } @@ -158,7 +153,7 @@ void AnimSkeleton::convertAbsoluteRotationsToRelative(std::vector& ro // poses start off absolute and leave in relative frame int lastIndex = std::min((int)rotations.size(), _jointsSize); for (int i = lastIndex - 1; i >= 0; --i) { - int parentIndex = _joints[i].parentIndex; + int parentIndex = _parentIndices[i]; if (parentIndex != -1) { rotations[i] = glm::inverse(rotations[parentIndex]) * rotations[i]; } @@ -197,6 +192,14 @@ void AnimSkeleton::mirrorAbsolutePoses(AnimPoseVec& poses) const { void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints, const QMap jointOffsets) { _joints = joints; + + // build a seperate vector of parentIndices for cache coherency + // AnimSkeleton::getParentIndex is called very frequently in tight loops. + _parentIndices.reserve(_joints.size()); + for (auto& joint : _joints) { + _parentIndices.push_back(joint.parentIndex); + } + _jointsSize = (int)joints.size(); // build a cache of bind poses @@ -289,8 +292,6 @@ void AnimSkeleton::dump(bool verbose) const { qCDebug(animation) << " relDefaultPose =" << getRelativeDefaultPose(i); if (verbose) { qCDebug(animation) << " hfmJoint ="; - qCDebug(animation) << " isFree =" << _joints[i].isFree; - qCDebug(animation) << " freeLineage =" << _joints[i].freeLineage; qCDebug(animation) << " parentIndex =" << _joints[i].parentIndex; qCDebug(animation) << " translation =" << _joints[i].translation; qCDebug(animation) << " preTransform =" << _joints[i].preTransform; diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 14f39eedbc..0eefbf973e 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -43,7 +43,10 @@ public: // get post transform which might include FBX offset transformations const AnimPose& getPostRotationPose(int jointIndex) const; - int getParentIndex(int jointIndex) const; + int getParentIndex(int jointIndex) const { + return _parentIndices[jointIndex]; + } + std::vector getChildrenOfJoint(int jointIndex) const; AnimPose getAbsolutePose(int jointIndex, const AnimPoseVec& relativePoses) const; @@ -69,6 +72,7 @@ protected: void buildSkeletonFromJoints(const std::vector& joints, const QMap jointOffsets); std::vector _joints; + std::vector _parentIndices; int _jointsSize { 0 }; AnimPoseVec _relativeDefaultPoses; AnimPoseVec _absoluteDefaultPoses; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 7842ec0804..d09de36a14 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -39,14 +39,13 @@ static int nextRigId = 1; static std::map rigRegistry; static std::mutex rigRegistryMutex; +static bool isEqual(const float p, float q) { + const float EPSILON = 0.00001f; + return fabsf(p - q) <= EPSILON; +} + static bool isEqual(const glm::vec3& u, const glm::vec3& v) { - const float EPSILON = 0.0001f; - float uLen = glm::length(u); - if (uLen == 0.0f) { - return glm::length(v) <= EPSILON; - } else { - return (glm::length(u - v) / uLen) <= EPSILON; - } + return isEqual(u.x, v.x) && isEqual(u.y, v.y) && isEqual(u.z, v.z); } static bool isEqual(const glm::quat& p, const glm::quat& q) { @@ -494,10 +493,8 @@ std::shared_ptr Rig::getAnimInverseKinematicsNode() const std::shared_ptr result; if (_animNode) { _animNode->traverse([&](AnimNode::Pointer node) { - // only report clip nodes as valid roles. - auto ikNode = std::dynamic_pointer_cast(node); - if (ikNode) { - result = ikNode; + if (node->getType() == AnimNodeType::InverseKinematics) { + result = std::dynamic_pointer_cast(node); return false; } else { return true; @@ -1329,7 +1326,7 @@ static bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& sh } if (slabCount == (DOP14_COUNT / 2) && minDisplacementLen != FLT_MAX) { // we are within the k-dop so push the point along the minimum displacement found - displacementOut = shapePose.xformVectorFast(minDisplacement); + displacementOut = shapePose.xformVector(minDisplacement); return true; } else { // point is outside of kdop @@ -1338,7 +1335,7 @@ static bool findPointKDopDisplacement(const glm::vec3& point, const AnimPose& sh } else { // point is directly on top of shapeInfo.avgPoint. // push the point out along the x axis. - displacementOut = shapePose.xformVectorFast(shapeInfo.points[0]); + displacementOut = shapePose.xformVector(shapeInfo.points[0]); return true; } } diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index b2cba2351f..60a95ff58a 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -170,6 +170,57 @@ static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { } } +static float computeLoudness(int16_t* samples, int numSamples, int numChannels, bool& isClipping) { + + const int32_t CLIPPING_THRESHOLD = 32392; // -0.1 dBFS + const int32_t CLIPPING_DETECTION = 3; // consecutive samples over threshold + + float scale = numSamples ? 1.0f / (numSamples * 32768.0f) : 0.0f; + + int32_t loudness = 0; + isClipping = false; + + if (numChannels == 2) { + int32_t oversLeft = 0; + int32_t oversRight = 0; + + for (int i = 0; i < numSamples/2; i++) { + int32_t left = std::abs((int32_t)samples[2*i+0]); + int32_t right = std::abs((int32_t)samples[2*i+1]); + + loudness += left; + loudness += right; + + if (left > CLIPPING_THRESHOLD) { + isClipping |= (++oversLeft >= CLIPPING_DETECTION); + } else { + oversLeft = 0; + } + if (right > CLIPPING_THRESHOLD) { + isClipping |= (++oversRight >= CLIPPING_DETECTION); + } else { + oversRight = 0; + } + } + } else { + int32_t overs = 0; + + for (int i = 0; i < numSamples; i++) { + int32_t sample = std::abs((int32_t)samples[i]); + + loudness += sample; + + if (sample > CLIPPING_THRESHOLD) { + isClipping |= (++overs >= CLIPPING_DETECTION); + } else { + overs = 0; + } + } + } + + return (float)loudness * scale; +} + static inline float convertToFloat(int16_t sample) { return (float)sample * (1 / 32768.0f); } @@ -1075,45 +1126,25 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { void AudioClient::handleAudioInput(QByteArray& audioBuffer) { if (!_audioPaused) { - if (_muted) { - _lastInputLoudness = 0.0f; - _timeSinceLastClip = 0.0f; - } else { + + bool audioGateOpen = false; + + if (!_muted) { int16_t* samples = reinterpret_cast(audioBuffer.data()); int numSamples = audioBuffer.size() / AudioConstants::SAMPLE_SIZE; int numFrames = numSamples / (_isStereoInput ? AudioConstants::STEREO : AudioConstants::MONO); if (_isNoiseGateEnabled) { // The audio gate includes DC removal - _audioGate->render(samples, samples, numFrames); + audioGateOpen = _audioGate->render(samples, samples, numFrames); } else { - _audioGate->removeDC(samples, samples, numFrames); - } - - int32_t loudness = 0; - assert(numSamples < 65536); // int32_t loudness cannot overflow - bool didClip = false; - for (int i = 0; i < numSamples; ++i) { - const int32_t CLIPPING_THRESHOLD = (int32_t)(AudioConstants::MAX_SAMPLE_VALUE * 0.9f); - int32_t sample = std::abs((int32_t)samples[i]); - loudness += sample; - didClip |= (sample > CLIPPING_THRESHOLD); - } - _lastInputLoudness = (float)loudness / numSamples; - - if (didClip) { - _timeSinceLastClip = 0.0f; - } else if (_timeSinceLastClip >= 0.0f) { - _timeSinceLastClip += (float)numSamples / (float)AudioConstants::SAMPLE_RATE; + audioGateOpen = _audioGate->removeDC(samples, samples, numFrames); } emit inputReceived(audioBuffer); } - emit inputLoudnessChanged(_lastInputLoudness); - - // state machine to detect gate opening and closing - bool audioGateOpen = (_lastInputLoudness != 0.0f); + // detect gate opening and closing bool openedInLastBlock = !_audioGateOpen && audioGateOpen; // the gate just opened bool closedInLastBlock = _audioGateOpen && !audioGateOpen; // the gate just closed _audioGateOpen = audioGateOpen; @@ -1186,10 +1217,27 @@ void AudioClient::handleMicAudioInput() { static int16_t networkAudioSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; while (_inputRingBuffer.samplesAvailable() >= inputSamplesRequired) { - if (_muted) { - _inputRingBuffer.shiftReadPosition(inputSamplesRequired); - } else { - _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); + + _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); + + // detect loudness and clipping on the raw input + bool isClipping = false; + float inputLoudness = computeLoudness(inputAudioSamples.get(), inputSamplesRequired, _inputFormat.channelCount(), isClipping); + + float tc = (inputLoudness > _lastInputLoudness) ? 0.378f : 0.967f; // 10ms attack, 300ms release @ 100Hz + inputLoudness += tc * (_lastInputLoudness - inputLoudness); + _lastInputLoudness = inputLoudness; + + if (isClipping) { + _timeSinceLastClip = 0.0f; + } else if (_timeSinceLastClip >= 0.0f) { + _timeSinceLastClip += AudioConstants::NETWORK_FRAME_SECS; + } + isClipping = (_timeSinceLastClip >= 0.0f) && (_timeSinceLastClip < 2.0f); // 2 second hold time + + emit inputLoudnessChanged(_lastInputLoudness, isClipping); + + if (!_muted) { possibleResampling(_inputToNetworkResampler, inputAudioSamples.get(), networkAudioSamples, inputSamplesRequired, numNetworkSamples, diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index f3e1ad9a52..94ed2ce132 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -248,7 +248,7 @@ signals: void noiseReductionChanged(bool noiseReductionEnabled); void mutedByMixer(); void inputReceived(const QByteArray& inputSamples); - void inputLoudnessChanged(float loudness); + void inputLoudnessChanged(float loudness, bool isClipping); void outputBytesToNetwork(int numBytes); void inputBytesFromNetwork(int numBytes); void noiseGateOpened(); diff --git a/libraries/audio/src/AudioGate.cpp b/libraries/audio/src/AudioGate.cpp index e9cdf832d2..0df46ac532 100644 --- a/libraries/audio/src/AudioGate.cpp +++ b/libraries/audio/src/AudioGate.cpp @@ -138,8 +138,8 @@ public: int32_t hysteresis(int32_t peak); int32_t envelope(int32_t attn); - virtual void process(int16_t* input, int16_t* output, int numFrames) = 0; - virtual void removeDC(int16_t* input, int16_t* output, int numFrames) = 0; + virtual bool process(int16_t* input, int16_t* output, int numFrames) = 0; + virtual bool removeDC(int16_t* input, int16_t* output, int numFrames) = 0; }; GateImpl::GateImpl(int sampleRate) { @@ -403,14 +403,15 @@ public: GateMono(int sampleRate) : GateImpl(sampleRate) {} // mono input/output (in-place is allowed) - void process(int16_t* input, int16_t* output, int numFrames) override; - void removeDC(int16_t* input, int16_t* output, int numFrames) override; + bool process(int16_t* input, int16_t* output, int numFrames) override; + bool removeDC(int16_t* input, int16_t* output, int numFrames) override; }; template -void GateMono::process(int16_t* input, int16_t* output, int numFrames) { +bool GateMono::process(int16_t* input, int16_t* output, int numFrames) { clearHistogram(); + int32_t mask = 0; for (int n = 0; n < numFrames; n++) { @@ -453,15 +454,21 @@ void GateMono::process(int16_t* input, int16_t* output, int numFrames) { x = MULQ31(x, attn); // store 16-bit output - output[n] = (int16_t)saturateQ30(x); + x = saturateQ30(x); + output[n] = (int16_t)x; + + mask |= x; } // update adaptive threshold processHistogram(numFrames); + return mask != 0; } template -void GateMono::removeDC(int16_t* input, int16_t* output, int numFrames) { +bool GateMono::removeDC(int16_t* input, int16_t* output, int numFrames) { + + int32_t mask = 0; for (int n = 0; n < numFrames; n++) { @@ -471,8 +478,13 @@ void GateMono::removeDC(int16_t* input, int16_t* output, int numFrames) { _dc.process(x); // store 16-bit output - output[n] = (int16_t)saturateQ30(x); + x = saturateQ30(x); + output[n] = (int16_t)x; + + mask |= x; } + + return mask != 0; } // @@ -489,14 +501,15 @@ public: GateStereo(int sampleRate) : GateImpl(sampleRate) {} // interleaved stereo input/output (in-place is allowed) - void process(int16_t* input, int16_t* output, int numFrames) override; - void removeDC(int16_t* input, int16_t* output, int numFrames) override; + bool process(int16_t* input, int16_t* output, int numFrames) override; + bool removeDC(int16_t* input, int16_t* output, int numFrames) override; }; template -void GateStereo::process(int16_t* input, int16_t* output, int numFrames) { +bool GateStereo::process(int16_t* input, int16_t* output, int numFrames) { clearHistogram(); + int32_t mask = 0; for (int n = 0; n < numFrames; n++) { @@ -541,16 +554,23 @@ void GateStereo::process(int16_t* input, int16_t* output, int numFrames) { x1 = MULQ31(x1, attn); // store 16-bit output - output[2*n+0] = (int16_t)saturateQ30(x0); - output[2*n+1] = (int16_t)saturateQ30(x1); + x0 = saturateQ30(x0); + x1 = saturateQ30(x1); + output[2*n+0] = (int16_t)x0; + output[2*n+1] = (int16_t)x1; + + mask |= (x0 | x1); } // update adaptive threshold processHistogram(numFrames); + return mask != 0; } template -void GateStereo::removeDC(int16_t* input, int16_t* output, int numFrames) { +bool GateStereo::removeDC(int16_t* input, int16_t* output, int numFrames) { + + int32_t mask = 0; for (int n = 0; n < numFrames; n++) { @@ -561,9 +581,15 @@ void GateStereo::removeDC(int16_t* input, int16_t* output, int numFrames) { _dc.process(x0, x1); // store 16-bit output - output[2*n+0] = (int16_t)saturateQ30(x0); - output[2*n+1] = (int16_t)saturateQ30(x1); + x0 = saturateQ30(x0); + x1 = saturateQ30(x1); + output[2*n+0] = (int16_t)x0; + output[2*n+1] = (int16_t)x1; + + mask |= (x0 | x1); } + + return mask != 0; } // @@ -580,14 +606,15 @@ public: GateQuad(int sampleRate) : GateImpl(sampleRate) {} // interleaved quad input/output (in-place is allowed) - void process(int16_t* input, int16_t* output, int numFrames) override; - void removeDC(int16_t* input, int16_t* output, int numFrames) override; + bool process(int16_t* input, int16_t* output, int numFrames) override; + bool removeDC(int16_t* input, int16_t* output, int numFrames) override; }; template -void GateQuad::process(int16_t* input, int16_t* output, int numFrames) { +bool GateQuad::process(int16_t* input, int16_t* output, int numFrames) { clearHistogram(); + int32_t mask = 0; for (int n = 0; n < numFrames; n++) { @@ -636,18 +663,27 @@ void GateQuad::process(int16_t* input, int16_t* output, int numFrames) { x3 = MULQ31(x3, attn); // store 16-bit output - output[4*n+0] = (int16_t)saturateQ30(x0); - output[4*n+1] = (int16_t)saturateQ30(x1); - output[4*n+2] = (int16_t)saturateQ30(x2); - output[4*n+3] = (int16_t)saturateQ30(x3); + x0 = saturateQ30(x0); + x1 = saturateQ30(x1); + x2 = saturateQ30(x2); + x3 = saturateQ30(x3); + output[4*n+0] = (int16_t)x0; + output[4*n+1] = (int16_t)x1; + output[4*n+2] = (int16_t)x2; + output[4*n+3] = (int16_t)x3; + + mask |= (x0 | x1 | x2 | x3); } // update adaptive threshold processHistogram(numFrames); + return mask != 0; } template -void GateQuad::removeDC(int16_t* input, int16_t* output, int numFrames) { +bool GateQuad::removeDC(int16_t* input, int16_t* output, int numFrames) { + + int32_t mask = 0; for (int n = 0; n < numFrames; n++) { @@ -660,11 +696,19 @@ void GateQuad::removeDC(int16_t* input, int16_t* output, int numFrames) { _dc.process(x0, x1, x2, x3); // store 16-bit output - output[4*n+0] = (int16_t)saturateQ30(x0); - output[4*n+1] = (int16_t)saturateQ30(x1); - output[4*n+2] = (int16_t)saturateQ30(x2); - output[4*n+3] = (int16_t)saturateQ30(x3); + x0 = saturateQ30(x0); + x1 = saturateQ30(x1); + x2 = saturateQ30(x2); + x3 = saturateQ30(x3); + output[4*n+0] = (int16_t)x0; + output[4*n+1] = (int16_t)x1; + output[4*n+2] = (int16_t)x2; + output[4*n+3] = (int16_t)x3; + + mask |= (x0 | x1 | x2 | x3); } + + return mask != 0; } // @@ -721,12 +765,12 @@ AudioGate::~AudioGate() { delete _impl; } -void AudioGate::render(int16_t* input, int16_t* output, int numFrames) { - _impl->process(input, output, numFrames); +bool AudioGate::render(int16_t* input, int16_t* output, int numFrames) { + return _impl->process(input, output, numFrames); } -void AudioGate::removeDC(int16_t* input, int16_t* output, int numFrames) { - _impl->removeDC(input, output, numFrames); +bool AudioGate::removeDC(int16_t* input, int16_t* output, int numFrames) { + return _impl->removeDC(input, output, numFrames); } void AudioGate::setThreshold(float threshold) { diff --git a/libraries/audio/src/AudioGate.h b/libraries/audio/src/AudioGate.h index d4ae3c5fe8..6fc7ca83df 100644 --- a/libraries/audio/src/AudioGate.h +++ b/libraries/audio/src/AudioGate.h @@ -18,9 +18,12 @@ public: AudioGate(int sampleRate, int numChannels); ~AudioGate(); - // interleaved int16_t input/output (in-place is allowed) - void render(int16_t* input, int16_t* output, int numFrames); - void removeDC(int16_t* input, int16_t* output, int numFrames); + // + // Process interleaved int16_t input/output (in-place is allowed). + // Returns true when output is non-zero. + // + bool render(int16_t* input, int16_t* output, int numFrames); + bool removeDC(int16_t* input, int16_t* output, int numFrames); void setThreshold(float threshold); void setRelease(float release); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index b8626c813e..daef0e411a 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1736,15 +1736,17 @@ void Avatar::computeShapeInfo(ShapeInfo& shapeInfo) { void Avatar::computeDetailedShapeInfo(ShapeInfo& shapeInfo, int jointIndex) { if (jointIndex > -1 && jointIndex < (int)_multiSphereShapes.size()) { auto& data = _multiSphereShapes[jointIndex].getSpheresData(); - std::vector positions; - std::vector radiuses; - positions.reserve(data.size()); - radiuses.reserve(data.size()); - for (auto& sphere : data) { - positions.push_back(sphere._position); - radiuses.push_back(sphere._radius); + if (data.size() > 0) { + std::vector positions; + std::vector radiuses; + positions.reserve(data.size()); + radiuses.reserve(data.size()); + for (auto& sphere : data) { + positions.push_back(sphere._position); + radiuses.push_back(sphere._radius); + } + shapeInfo.setMultiSphere(positions, radiuses); } - shapeInfo.setMultiSphere(positions, radiuses); } } @@ -1975,12 +1977,12 @@ float Avatar::getUnscaledEyeHeightFromSkeleton() const { auto& rig = _skeletonModel->getRig(); // Normally the model offset transform will contain the avatar scale factor, we explicitly remove it here. - AnimPose modelOffsetWithoutAvatarScale(glm::vec3(1.0f), rig.getModelOffsetPose().rot(), rig.getModelOffsetPose().trans()); + AnimPose modelOffsetWithoutAvatarScale(1.0f, rig.getModelOffsetPose().rot(), rig.getModelOffsetPose().trans()); AnimPose geomToRigWithoutAvatarScale = modelOffsetWithoutAvatarScale * rig.getGeometryOffsetPose(); // This factor can be used to scale distances in the geometry frame into the unscaled rig frame. // Typically it will be the unit conversion from cm to m. - float scaleFactor = geomToRigWithoutAvatarScale.scale().x; // in practice this always a uniform scale factor. + float scaleFactor = geomToRigWithoutAvatarScale.scale(); int headTopJoint = rig.indexOfJoint("HeadTop_End"); int headJoint = rig.indexOfJoint("Head"); diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index 7f2dbda3de..ea71ff128c 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -249,14 +249,6 @@ bool SkeletonModel::getRightHandPosition(glm::vec3& position) const { return getJointPositionInWorldFrame(getRightHandJointIndex(), position); } -bool SkeletonModel::getLeftShoulderPosition(glm::vec3& position) const { - return getJointPositionInWorldFrame(getLastFreeJointIndex(getLeftHandJointIndex()), position); -} - -bool SkeletonModel::getRightShoulderPosition(glm::vec3& position) const { - return getJointPositionInWorldFrame(getLastFreeJointIndex(getRightHandJointIndex()), position); -} - bool SkeletonModel::getHeadPosition(glm::vec3& headPosition) const { return isActive() && getJointPositionInWorldFrame(_rig.indexOfJoint("Head"), headPosition); } diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h index ef0e1e0fae..99f6632306 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h @@ -57,17 +57,9 @@ public: /// \return true whether or not the position was found bool getRightHandPosition(glm::vec3& position) const; - /// Gets the position of the left shoulder. - /// \return whether or not the left shoulder joint was found - bool getLeftShoulderPosition(glm::vec3& position) const; - /// Returns the extended length from the left hand to its last free ancestor. float getLeftArmLength() const; - /// Gets the position of the right shoulder. - /// \return whether or not the right shoulder joint was found - bool getRightShoulderPosition(glm::vec3& position) const; - /// Returns the position of the head joint. /// \return whether or not the head was found bool getHeadPosition(glm::vec3& headPosition) const; diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index bcbe5308c7..3e188afbdf 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -107,11 +107,10 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { if (initialSend || *simpleIt == Updated) { if (traitType == AvatarTraits::SkeletonModelURL) { - bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); - // keep track of our skeleton version in case we get an override back _currentSkeletonVersion = _currentTraitVersion; } + bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); } ++simpleIt; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 44025fc8f4..2b3a915235 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -1289,6 +1289,17 @@ CalculateEntityLoadingPriority EntityTreeRenderer::_calculateEntityLoadingPriori return 0.0f; }; +std::pair EntityTreeRenderer::getZoneInteractionProperties() { + for (auto& zone : _layeredZones) { + // Only domain entities control flying allowed and ghosting allowed + if (zone.zone && zone.zone->isDomainEntity()) { + return { zone.zone->getFlyingAllowed(), zone.zone->getGhostingAllowed() }; + } + } + + return { true, true }; +} + bool EntityTreeRenderer::wantsKeyboardFocus(const EntityItemID& id) const { auto renderable = renderableForEntityId(id); if (!renderable) { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index d9f594a20b..725416e2cc 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -105,7 +105,7 @@ public: // For Scene.shouldRenderEntities QList& getEntitiesLastInScene() { return _entityIDsLastInScene; } - std::shared_ptr myAvatarZone() { return _layeredZones.getZone(); } + std::pair getZoneInteractionProperties(); bool wantsKeyboardFocus(const EntityItemID& id) const; QObject* getEventHandler(const EntityItemID& id); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 7e01af04dd..fea46d7a6e 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -959,23 +959,6 @@ QStringList RenderableModelEntityItem::getJointNames() const { return result; } -void RenderableModelEntityItem::setAnimationURL(const QString& url) { - QString oldURL = getAnimationURL(); - ModelEntityItem::setAnimationURL(url); - if (oldURL != getAnimationURL()) { - _needsAnimationReset = true; - } -} - -bool RenderableModelEntityItem::needsAnimationReset() const { - return _needsAnimationReset; -} - -QString RenderableModelEntityItem::getAnimationURLAndReset() { - _needsAnimationReset = false; - return getAnimationURL(); -} - scriptable::ScriptableModelBase render::entities::ModelEntityRenderer::getScriptableModel() { auto model = resultWithReadLock([this]{ return _model; }); @@ -1264,7 +1247,7 @@ bool ModelEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPoin return false; } - if (_lastTextures != entity->getTextures()) { + if (_textures != entity->getTextures()) { return true; } @@ -1418,15 +1401,14 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce entity->_originalTexturesRead = true; } - if (_lastTextures != entity->getTextures()) { + if (_textures != entity->getTextures()) { + QVariantMap newTextures; withWriteLock([&] { _texturesLoaded = false; - _lastTextures = entity->getTextures(); + _textures = entity->getTextures(); + newTextures = parseTexturesToMap(_textures, entity->_originalTextures); }); - auto newTextures = parseTexturesToMap(_lastTextures, entity->_originalTextures); - if (newTextures != model->getTextures()) { - model->setTextures(newTextures); - } + model->setTextures(newTextures); } if (entity->_needsJointSimulation) { entity->copyAnimationJointDataToModel(); @@ -1494,11 +1476,17 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce if (_animating) { DETAILED_PROFILE_RANGE(simulation_physics, "Animate"); - if (_animation && entity->needsAnimationReset()) { - //(_animation->getURL().toString() != entity->getAnimationURL())) { // bad check - // the joints have been mapped before but we have a new animation to load - _animation.reset(); - _jointMappingCompleted = false; + auto animationURL = entity->getAnimationURL(); + bool animationChanged = _animationURL != animationURL; + if (animationChanged) { + _animationURL = animationURL; + + if (_animation) { + //(_animation->getURL().toString() != entity->getAnimationURL())) { // bad check + // the joints have been mapped before but we have a new animation to load + _animation.reset(); + _jointMappingCompleted = false; + } } if (!_jointMappingCompleted) { @@ -1563,7 +1551,7 @@ void ModelEntityRenderer::mapJoints(const TypedEntityPointer& entity, const Mode } if (!_animation) { - _animation = DependencyManager::get()->getAnimation(entity->getAnimationURLAndReset()); + _animation = DependencyManager::get()->getAnimation(_animationURL); } if (_animation && _animation->isLoaded()) { diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 16c3664f28..9adff9ca01 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -113,10 +113,6 @@ public: virtual int getJointIndex(const QString& name) const override; virtual QStringList getJointNames() const override; - void setAnimationURL(const QString& url) override; - bool needsAnimationReset() const; - QString getAnimationURLAndReset(); - private: bool needsUpdateModelBounds() const; void autoResizeJointArrays(); @@ -131,7 +127,6 @@ private: bool _originalTexturesRead { false }; bool _dimensionsInitialized { true }; bool _needsJointSimulation { false }; - bool _needsAnimationReset { false }; }; namespace render { namespace entities { @@ -181,7 +176,7 @@ private: bool _hasModel { false }; ModelPointer _model; - QString _lastTextures; + QString _textures; bool _texturesLoaded { false }; int _lastKnownCurrentFrame { -1 }; #ifdef MODEL_ENTITY_USE_FADE_EFFECT @@ -190,12 +185,12 @@ private: const void* _collisionMeshKey { nullptr }; - // used on client side + QUrl _parsedModelURL; bool _jointMappingCompleted { false }; QVector _jointMapping; // domain is index into model-joints, range is index into animation-joints AnimationPointer _animation; - QUrl _parsedModelURL; bool _animating { false }; + QString _animationURL; uint64_t _lastAnimated { 0 }; render::ItemKey _itemKey { render::ItemKey::Builder().withTypeMeta() }; diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 57ff8ed8c2..631148c27a 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -198,24 +198,33 @@ void ZoneEntityRenderer::removeFromScene(const ScenePointer& scene, Transaction& void ZoneEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { DependencyManager::get()->updateZone(entity->getID()); + auto position = entity->getWorldPosition(); + auto rotation = entity->getWorldOrientation(); + auto dimensions = entity->getScaledDimensions(); + bool rotationChanged = rotation != _lastRotation; + bool transformChanged = rotationChanged || position != _lastPosition || dimensions != _lastDimensions; + + auto proceduralUserData = entity->getUserData(); + bool proceduralUserDataChanged = _proceduralUserData != proceduralUserData; + // FIXME one of the bools here could become true between being fetched and being reset, // resulting in a lost update - bool keyLightChanged = entity->keyLightPropertiesChanged(); - bool ambientLightChanged = entity->ambientLightPropertiesChanged(); - bool skyboxChanged = entity->skyboxPropertiesChanged(); + bool keyLightChanged = entity->keyLightPropertiesChanged() || rotationChanged; + bool ambientLightChanged = entity->ambientLightPropertiesChanged() || transformChanged; + bool skyboxChanged = entity->skyboxPropertiesChanged() || proceduralUserDataChanged; bool hazeChanged = entity->hazePropertiesChanged(); bool bloomChanged = entity->bloomPropertiesChanged(); - entity->resetRenderingPropertiesChanged(); - _lastPosition = entity->getWorldPosition(); - _lastRotation = entity->getWorldOrientation(); - _lastDimensions = entity->getScaledDimensions(); - _keyLightProperties = entity->getKeyLightProperties(); - _ambientLightProperties = entity->getAmbientLightProperties(); - _skyboxProperties = entity->getSkyboxProperties(); - _hazeProperties = entity->getHazeProperties(); - _bloomProperties = entity->getBloomProperties(); + if (transformChanged) { + _lastPosition = entity->getWorldPosition(); + _lastRotation = entity->getWorldOrientation(); + _lastDimensions = entity->getScaledDimensions(); + } + + if (proceduralUserDataChanged) { + _proceduralUserData = entity->getUserData(); + } #if 0 if (_lastShapeURL != _typedEntity->getCompoundShapeURL()) { @@ -239,21 +248,29 @@ void ZoneEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scen updateKeyZoneItemFromEntity(entity); if (keyLightChanged) { + _keyLightProperties = entity->getKeyLightProperties(); updateKeySunFromEntity(entity); } if (ambientLightChanged) { + _ambientLightProperties = entity->getAmbientLightProperties(); updateAmbientLightFromEntity(entity); } - if (skyboxChanged || _proceduralUserData != entity->getUserData()) { + if (skyboxChanged) { + _skyboxProperties = entity->getSkyboxProperties(); updateKeyBackgroundFromEntity(entity); } if (hazeChanged) { + _hazeProperties = entity->getHazeProperties(); updateHazeFromEntity(entity); } + if (bloomChanged) { + _bloomProperties = entity->getBloomProperties(); + updateBloomFromEntity(entity); + } bool visuallyReady = true; uint32_t skyboxMode = entity->getSkyboxMode(); @@ -264,10 +281,6 @@ void ZoneEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scen } entity->setVisuallyReady(visuallyReady); - - if (bloomChanged) { - updateBloomFromEntity(entity); - } } void ZoneEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { @@ -344,7 +357,7 @@ void ZoneEntityRenderer::updateKeySunFromEntity(const TypedEntityPointer& entity // Set the keylight sunLight->setColor(ColorUtils::toVec3(_keyLightProperties.getColor())); sunLight->setIntensity(_keyLightProperties.getIntensity()); - sunLight->setDirection(entity->getTransform().getRotation() * _keyLightProperties.getDirection()); + sunLight->setDirection(_lastRotation * _keyLightProperties.getDirection()); sunLight->setCastShadows(_keyLightProperties.getCastShadows()); } @@ -356,7 +369,6 @@ void ZoneEntityRenderer::updateAmbientLightFromEntity(const TypedEntityPointer& ambientLight->setPosition(_lastPosition); ambientLight->setOrientation(_lastRotation); - // Set the ambient light ambientLight->setAmbientIntensity(_ambientLightProperties.getAmbientIntensity()); @@ -395,8 +407,6 @@ void ZoneEntityRenderer::updateHazeFromEntity(const TypedEntityPointer& entity) haze->setHazeAttenuateKeyLight(_hazeProperties.getHazeAttenuateKeyLight()); haze->setHazeKeyLightRangeFactor(graphics::Haze::convertHazeRangeToHazeRangeFactor(_hazeProperties.getHazeKeyLightRange())); haze->setHazeKeyLightAltitudeFactor(graphics::Haze::convertHazeAltitudeToHazeAltitudeFactor(_hazeProperties.getHazeKeyLightAltitude())); - - haze->setTransform(entity->getTransform().getMatrix()); } void ZoneEntityRenderer::updateBloomFromEntity(const TypedEntityPointer& entity) { @@ -414,13 +424,13 @@ void ZoneEntityRenderer::updateKeyBackgroundFromEntity(const TypedEntityPointer& editBackground(); setSkyboxColor(toGlm(_skyboxProperties.getColor())); - setProceduralUserData(entity->getUserData()); + setProceduralUserData(_proceduralUserData); setSkyboxURL(_skyboxProperties.getURL()); } void ZoneEntityRenderer::updateKeyZoneItemFromEntity(const TypedEntityPointer& entity) { // Update rotation values - editSkybox()->setOrientation(entity->getTransform().getRotation()); + editSkybox()->setOrientation(_lastRotation); /* TODO: Implement the sun model behavior / Keep this code here for reference, this is how we { @@ -540,9 +550,6 @@ void ZoneEntityRenderer::setSkyboxColor(const glm::vec3& color) { } void ZoneEntityRenderer::setProceduralUserData(const QString& userData) { - if (_proceduralUserData != userData) { - _proceduralUserData = userData; - std::dynamic_pointer_cast(editSkybox())->parse(_proceduralUserData); - } + std::dynamic_pointer_cast(editSkybox())->parse(userData); } diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 7cafaece7a..c1488a5893 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -1414,8 +1414,9 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * @property {Entities.Bloom} bloom - The bloom properties of the zone. * * @property {boolean} flyingAllowed=true - If true then visitors can fly in the zone; otherwise they cannot. + * Only works on domain entities. * @property {boolean} ghostingAllowed=true - If true then visitors with avatar collisions turned off will not - * collide with content in the zone; otherwise visitors will always collide with content in the zone. + * collide with content in the zone; otherwise visitors will always collide with content in the zone. Only works on domain entities. * @property {string} filterURL="" - The URL of a JavaScript file that filters changes to properties of entities within the * zone. It is periodically executed for each entity in the zone. It can, for example, be used to not allow changes to diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index ddbb028b6e..e365d0a7b6 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -43,14 +43,15 @@ ModelEntityItem::ModelEntityItem(const EntityItemID& entityItemID) : EntityItem( } const QString ModelEntityItem::getTextures() const { - QReadLocker locker(&_texturesLock); - auto textures = _textures; - return textures; + return resultWithReadLock([&] { + return _textures; + }); } void ModelEntityItem::setTextures(const QString& textures) { - QWriteLocker locker(&_texturesLock); - _textures = textures; + withWriteLock([&] { + _textures = textures; + }); } EntityItemProperties ModelEntityItem::getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const { diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 8c9fbdc45f..649a6cb50f 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -163,7 +163,6 @@ protected: AnimationPropertyGroup _animationProperties; - mutable QReadWriteLock _texturesLock; QString _textures; ShapeType _shapeType = SHAPE_TYPE_NONE; diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 7f7f6170d4..7b0491dbc0 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -119,7 +119,7 @@ bool ZoneEntityItem::setSubClassProperties(const EntityItemProperties& propertie SET_ENTITY_PROPERTY_FROM_PROPERTIES(bloomMode, setBloomMode); somethingChanged = somethingChanged || _keyLightPropertiesChanged || _ambientLightPropertiesChanged || - _stagePropertiesChanged || _skyboxPropertiesChanged || _hazePropertiesChanged || _bloomPropertiesChanged; + _skyboxPropertiesChanged || _hazePropertiesChanged || _bloomPropertiesChanged; return somethingChanged; } @@ -394,7 +394,6 @@ void ZoneEntityItem::resetRenderingPropertiesChanged() { _skyboxPropertiesChanged = false; _hazePropertiesChanged = false; _bloomPropertiesChanged = false; - _stagePropertiesChanged = false; }); } diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 813115add9..11c85dab89 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -102,8 +102,6 @@ public: bool hazePropertiesChanged() const { return _hazePropertiesChanged; } bool bloomPropertiesChanged() const { return _bloomPropertiesChanged; } - bool stagePropertiesChanged() const { return _stagePropertiesChanged; } - void resetRenderingPropertiesChanged(); virtual bool supportsDetailedIntersection() const override { return true; } @@ -155,7 +153,6 @@ protected: bool _skyboxPropertiesChanged { false }; bool _hazePropertiesChanged{ false }; bool _bloomPropertiesChanged { false }; - bool _stagePropertiesChanged { false }; static bool _drawZoneBoundaries; static bool _zonesArePickable; diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 38465d2809..207ee2982d 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -384,43 +384,6 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { return filepath.mid(filepath.lastIndexOf('/') + 1); } -QMap getJointNameMapping(const QVariantHash& mapping) { - static const QString JOINT_NAME_MAPPING_FIELD = "jointMap"; - QMap hfmToHifiJointNameMap; - if (!mapping.isEmpty() && mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].type() == QVariant::Hash) { - auto jointNames = mapping[JOINT_NAME_MAPPING_FIELD].toHash(); - for (auto itr = jointNames.begin(); itr != jointNames.end(); itr++) { - hfmToHifiJointNameMap.insert(itr.key(), itr.value().toString()); - qCDebug(modelformat) << "the mapped key " << itr.key() << " has a value of " << hfmToHifiJointNameMap[itr.key()]; - } - } - return hfmToHifiJointNameMap; -} - -QMap getJointRotationOffsets(const QVariantHash& mapping) { - QMap jointRotationOffsets; - static const QString JOINT_ROTATION_OFFSET_FIELD = "jointRotationOffset"; - if (!mapping.isEmpty() && mapping.contains(JOINT_ROTATION_OFFSET_FIELD) && mapping[JOINT_ROTATION_OFFSET_FIELD].type() == QVariant::Hash) { - auto offsets = mapping[JOINT_ROTATION_OFFSET_FIELD].toHash(); - for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { - QString jointName = itr.key(); - QString line = itr.value().toString(); - auto quatCoords = line.split(','); - if (quatCoords.size() == 4) { - float quatX = quatCoords[0].mid(1).toFloat(); - float quatY = quatCoords[1].toFloat(); - float quatZ = quatCoords[2].toFloat(); - float quatW = quatCoords[3].mid(0, quatCoords[3].size() - 1).toFloat(); - if (!isNaN(quatX) && !isNaN(quatY) && !isNaN(quatZ) && !isNaN(quatW)) { - glm::quat rotationOffset = glm::quat(quatW, quatX, quatY, quatZ); - jointRotationOffsets.insert(jointName, rotationOffset); - } - } - } - } - return jointRotationOffsets; -} - HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QString& url) { const FBXNode& node = _rootNode; QMap meshes; @@ -444,8 +407,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr std::map lights; - QVariantHash joints = mapping.value("joint").toHash(); - QVariantHash blendshapeMappings = mapping.value("bs").toHash(); QMultiHash blendshapeIndices; @@ -473,8 +434,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr HFMModel& hfmModel = *hfmModelPtr; hfmModel.originalURL = url; - hfmModel.hfmToHifiJointNameMapping.clear(); - hfmModel.hfmToHifiJointNameMapping = getJointNameMapping(mapping); float unitScaleFactor = 1.0f; glm::vec3 ambientColor; @@ -1287,26 +1246,14 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } // convert the models to joints - QVariantList freeJoints = mapping.values("freeJoint"); hfmModel.hasSkeletonJoints = false; foreach (const QString& modelID, modelIDs) { const FBXModel& fbxModel = fbxModels[modelID]; HFMJoint joint; - joint.isFree = freeJoints.contains(fbxModel.name); joint.parentIndex = fbxModel.parentIndex; - - // get the indices of all ancestors starting with the first free one (if any) int jointIndex = hfmModel.joints.size(); - joint.freeLineage.append(jointIndex); - int lastFreeIndex = joint.isFree ? 0 : -1; - for (int index = joint.parentIndex; index != -1; index = hfmModel.joints.at(index).parentIndex) { - if (hfmModel.joints.at(index).isFree) { - lastFreeIndex = joint.freeLineage.size(); - } - joint.freeLineage.append(index); - } - joint.freeLineage.remove(lastFreeIndex + 1, joint.freeLineage.size() - lastFreeIndex - 1); + joint.translation = fbxModel.translation; // these are usually in centimeters joint.preTransform = fbxModel.preTransform; joint.preRotation = fbxModel.preRotation; @@ -1341,14 +1288,10 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } joint.inverseBindRotation = joint.inverseDefaultRotation; joint.name = fbxModel.name; - if (hfmModel.hfmToHifiJointNameMapping.contains(hfmModel.hfmToHifiJointNameMapping.key(joint.name))) { - joint.name = hfmModel.hfmToHifiJointNameMapping.key(fbxModel.name); - } joint.bindTransformFoundInCluster = false; hfmModel.joints.append(joint); - hfmModel.jointIndices.insert(joint.name, hfmModel.joints.size()); QString rotationID = localRotations.value(modelID); AnimationCurve xRotCurve = animationCurves.value(xComponents.value(rotationID)); @@ -1704,7 +1647,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr generateBoundryLinesForDop14(joint.shapeInfo.dots, joint.shapeInfo.avgPoint, joint.shapeInfo.debugLines); } } - hfmModel.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); // attempt to map any meshes to a named model for (QHash::const_iterator m = meshIDsToMeshIndices.constBegin(); @@ -1722,21 +1664,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } - auto offsets = getJointRotationOffsets(mapping); - hfmModel.jointRotationOffsets.clear(); - for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { - QString jointName = itr.key(); - glm::quat rotationOffset = itr.value(); - int jointIndex = hfmModel.getJointIndex(jointName); - if (hfmModel.hfmToHifiJointNameMapping.contains(jointName)) { - jointIndex = hfmModel.getJointIndex(jointName); - } - if (jointIndex != -1) { - hfmModel.jointRotationOffsets.insert(jointIndex, rotationOffset); - } - qCDebug(modelformat) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset; - } - return hfmModelPtr; } diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index 7828037c74..b6f109c217 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -82,11 +82,6 @@ FST* FST::createFSTFromModel(const QString& fstPath, const QString& modelFilePat } mapping.insert(JOINT_INDEX_FIELD, jointIndices); - mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); - // If there are no blendshape mappings, and we detect that this is likely a mixamo file, // then we can add the default mixamo to "faceshift" mappings diff --git a/libraries/fbx/src/FSTReader.cpp b/libraries/fbx/src/FSTReader.cpp index 43806560dc..99087954eb 100644 --- a/libraries/fbx/src/FSTReader.cpp +++ b/libraries/fbx/src/FSTReader.cpp @@ -84,7 +84,7 @@ void FSTReader::writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it) QByteArray FSTReader::writeMapping(const QVariantHash& mapping) { static const QStringList PREFERED_ORDER = QStringList() << NAME_FIELD << TYPE_FIELD << SCALE_FIELD << FILENAME_FIELD - << MARKETPLACE_ID_FIELD << TEXDIR_FIELD << SCRIPT_FIELD << JOINT_FIELD << FREE_JOINT_FIELD + << MARKETPLACE_ID_FIELD << TEXDIR_FIELD << SCRIPT_FIELD << JOINT_FIELD << BLENDSHAPE_FIELD << JOINT_INDEX_FIELD; QBuffer buffer; buffer.open(QIODevice::WriteOnly); @@ -92,7 +92,7 @@ QByteArray FSTReader::writeMapping(const QVariantHash& mapping) { for (auto key : PREFERED_ORDER) { auto it = mapping.find(key); if (it != mapping.constEnd()) { - if (key == FREE_JOINT_FIELD || key == SCRIPT_FIELD) { // writeVariant does not handle strings added using insertMulti. + if (key == SCRIPT_FIELD) { // writeVariant does not handle strings added using insertMulti. for (auto multi : mapping.values(key)) { buffer.write(key.toUtf8()); buffer.write(" = "); diff --git a/libraries/fbx/src/FSTReader.h b/libraries/fbx/src/FSTReader.h index 993d7c3148..ad952c4ed7 100644 --- a/libraries/fbx/src/FSTReader.h +++ b/libraries/fbx/src/FSTReader.h @@ -27,7 +27,6 @@ static const QString TRANSLATION_X_FIELD = "tx"; static const QString TRANSLATION_Y_FIELD = "ty"; static const QString TRANSLATION_Z_FIELD = "tz"; static const QString JOINT_FIELD = "joint"; -static const QString FREE_JOINT_FIELD = "freeJoint"; static const QString BLENDSHAPE_FIELD = "bs"; static const QString SCRIPT_FIELD = "script"; static const QString JOINT_NAME_MAPPING_FIELD = "jointMap"; diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp old mode 100644 new mode 100755 index 96c236f703..51f721e985 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -352,9 +352,14 @@ bool GLTFSerializer::addImage(const QJsonObject& object) { QString mime; getStringVal(object, "uri", image.uri, image.defined); + if (image.uri.contains("data:image/png;base64,")) { + image.mimeType = getImageMimeType("image/png"); + } else if (image.uri.contains("data:image/jpeg;base64,")) { + image.mimeType = getImageMimeType("image/jpeg"); + } if (getStringVal(object, "mimeType", mime, image.defined)) { image.mimeType = getImageMimeType(mime); - } + } getIntVal(object, "bufferView", image.bufferView, image.defined); _file.images.push_back(image); @@ -719,7 +724,6 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { //Build default joints hfmModel.joints.resize(1); - hfmModel.joints[0].isFree = false; hfmModel.joints[0].parentIndex = -1; hfmModel.joints[0].distanceToParent = 0; hfmModel.joints[0].translation = glm::vec3(0, 0, 0); @@ -831,6 +835,22 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { for (int n = 0; n < normals.size(); n = n + 3) { mesh.normals.push_back(glm::vec3(normals[n], normals[n + 1], normals[n + 2])); } + } else if (key == "COLOR_0") { + QVector colors; + success = addArrayOfType(buffer.blob, + bufferview.byteOffset + accBoffset, + accessor.count, + colors, + accessor.type, + accessor.componentType); + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF COLOR_0 data for model " << _url; + continue; + } + int stride = (accessor.type == GLTFAccessorType::VEC4) ? 4 : 3; + for (int n = 0; n < colors.size() - 3; n += stride) { + mesh.colors.push_back(glm::vec3(colors[n], colors[n + 1], colors[n + 2])); + } } else if (key == "TEXCOORD_0") { QVector texcoords; success = addArrayOfType(buffer.blob, @@ -926,7 +946,6 @@ HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHas //_file.dump(); auto hfmModelPtr = std::make_shared(); HFMModel& hfmModel = *hfmModelPtr; - buildGeometry(hfmModel, _url); //hfmDebugDump(data); @@ -939,10 +958,15 @@ HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHas } bool GLTFSerializer::readBinary(const QString& url, QByteArray& outdata) { - QUrl binaryUrl = _url.resolved(url); - bool success; - std::tie(success, outdata) = requestData(binaryUrl); + + if (url.contains("data:application/octet-stream;base64,")) { + outdata = requestEmbeddedData(url); + success = !outdata.isEmpty(); + } else { + QUrl binaryUrl = _url.resolved(url); + std::tie(success, outdata) = requestData(binaryUrl); + } return success; } @@ -975,6 +999,11 @@ std::tuple GLTFSerializer::requestData(QUrl& url) { } } +QByteArray GLTFSerializer::requestEmbeddedData(const QString& url) { + QString binaryUrl = url.split(",")[1]; + return binaryUrl.isEmpty() ? QByteArray() : QByteArray::fromBase64(binaryUrl.toUtf8()); +} + QNetworkReply* GLTFSerializer::request(QUrl& url, bool isTest) { if (!qApp) { @@ -1006,11 +1035,16 @@ HFMTexture GLTFSerializer::getHFMTexture(const GLTFTexture& texture) { if (texture.defined["source"]) { QString url = _file.images[texture.source].uri; + QString fname = QUrl(url).fileName(); QUrl textureUrl = _url.resolved(url); qCDebug(modelformat) << "fname: " << fname; fbxtex.name = fname; fbxtex.filename = textureUrl.toEncoded(); + + if (url.contains("data:image/jpeg;base64,") || url.contains("data:image/png;base64,")) { + fbxtex.content = requestEmbeddedData(url); + } } return fbxtex; } @@ -1187,8 +1221,6 @@ void GLTFSerializer::hfmDebugDump(const HFMModel& hfmModel) { qCDebug(modelformat) << " hasSkeletonJoints =" << hfmModel.hasSkeletonJoints; qCDebug(modelformat) << " offset =" << hfmModel.offset; - qCDebug(modelformat) << " palmDirection = " << hfmModel.palmDirection; - qCDebug(modelformat) << " neckPivot = " << hfmModel.neckPivot; qCDebug(modelformat) << " bindExtents.size() = " << hfmModel.bindExtents.size(); @@ -1301,8 +1333,6 @@ void GLTFSerializer::hfmDebugDump(const HFMModel& hfmModel) { qCDebug(modelformat) << " shapeInfo.dots =" << joint.shapeInfo.dots; qCDebug(modelformat) << " shapeInfo.points =" << joint.shapeInfo.points; - qCDebug(modelformat) << " isFree =" << joint.isFree; - qCDebug(modelformat) << " freeLineage" << joint.freeLineage; qCDebug(modelformat) << " parentIndex" << joint.parentIndex; qCDebug(modelformat) << " distanceToParent" << joint.distanceToParent; qCDebug(modelformat) << " translation" << joint.translation; diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h old mode 100644 new mode 100755 index 5fca77c4fd..57ea126a7b --- a/libraries/fbx/src/GLTFSerializer.h +++ b/libraries/fbx/src/GLTFSerializer.h @@ -772,6 +772,8 @@ private: QVector& out_vertices, QVector& out_normals); std::tuple requestData(QUrl& url); + QByteArray requestEmbeddedData(const QString& url); + QNetworkReply* request(QUrl& url, bool isTest); bool doesResourceExist(const QString& url); diff --git a/libraries/fbx/src/OBJSerializer.cpp b/libraries/fbx/src/OBJSerializer.cpp index 9d4b1f16a1..91d3fc7cc0 100644 --- a/libraries/fbx/src/OBJSerializer.cpp +++ b/libraries/fbx/src/OBJSerializer.cpp @@ -687,7 +687,6 @@ HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash mesh.meshIndex = 0; hfmModel.joints.resize(1); - hfmModel.joints[0].isFree = false; hfmModel.joints[0].parentIndex = -1; hfmModel.joints[0].distanceToParent = 0; hfmModel.joints[0].translation = glm::vec3(0, 0, 0); @@ -1048,8 +1047,7 @@ void hfmDebugDump(const HFMModel& hfmModel) { qCDebug(modelformat) << " joints.count() =" << hfmModel.joints.count(); foreach (HFMJoint joint, hfmModel.joints) { - qCDebug(modelformat) << " isFree =" << joint.isFree; - qCDebug(modelformat) << " freeLineage" << joint.freeLineage; + qCDebug(modelformat) << " parentIndex" << joint.parentIndex; qCDebug(modelformat) << " distanceToParent" << joint.distanceToParent; qCDebug(modelformat) << " translation" << joint.translation; diff --git a/libraries/graphics/src/graphics/Haze.cpp b/libraries/graphics/src/graphics/Haze.cpp index ded48429ba..d9bee7507f 100644 --- a/libraries/graphics/src/graphics/Haze.cpp +++ b/libraries/graphics/src/graphics/Haze.cpp @@ -182,12 +182,3 @@ void Haze::setHazeBackgroundBlend(const float hazeBackgroundBlend) { _hazeParametersBuffer.edit().hazeBackgroundBlend = newBlend; } } - -void Haze::setTransform(const glm::mat4& transform) { - auto& params = _hazeParametersBuffer.get(); - - if (params.transform != transform) { - _hazeParametersBuffer.edit().transform = transform; - } -} - diff --git a/libraries/graphics/src/graphics/Haze.h b/libraries/graphics/src/graphics/Haze.h index 59138319f4..25004f098f 100644 --- a/libraries/graphics/src/graphics/Haze.h +++ b/libraries/graphics/src/graphics/Haze.h @@ -92,8 +92,6 @@ namespace graphics { void setHazeBackgroundBlend(const float hazeBackgroundBlend); - void setTransform(const glm::mat4& transform); - using UniformBufferView = gpu::BufferView; UniformBufferView getHazeParametersBuffer() const { return _hazeParametersBuffer; } @@ -101,30 +99,32 @@ namespace graphics { class Parameters { public: // DO NOT CHANGE ORDER HERE WITHOUT UNDERSTANDING THE std140 LAYOUT - glm::vec3 hazeColor{ INITIAL_HAZE_COLOR }; - float hazeGlareBlend{ convertGlareAngleToPower(INITIAL_HAZE_GLARE_ANGLE) }; + glm::vec3 hazeColor { INITIAL_HAZE_COLOR }; + float hazeGlareBlend { convertGlareAngleToPower(INITIAL_HAZE_GLARE_ANGLE) }; - glm::vec3 hazeGlareColor{ INITIAL_HAZE_GLARE_COLOR }; - float hazeBaseReference{ INITIAL_HAZE_BASE_REFERENCE }; + glm::vec3 hazeGlareColor { INITIAL_HAZE_GLARE_COLOR }; + float hazeBaseReference { INITIAL_HAZE_BASE_REFERENCE }; glm::vec3 colorModulationFactor; - int hazeMode{ 0 }; // bit 0 - set to activate haze attenuation of fragment color + int hazeMode { 0 }; // bit 0 - set to activate haze attenuation of fragment color // bit 1 - set to add the effect of altitude to the haze attenuation // bit 2 - set to activate directional light attenuation mode // bit 3 - set to blend between blend-in and blend-out colours - glm::mat4 transform; + // Padding required to align the struct +#if defined(__clang__) + __attribute__((unused)) +#endif + vec3 __padding; // Amount of background (skybox) to display, overriding the haze effect for the background - float hazeBackgroundBlend{ INITIAL_HAZE_BACKGROUND_BLEND }; + float hazeBackgroundBlend { INITIAL_HAZE_BACKGROUND_BLEND }; // The haze attenuation exponents used by both fragment and directional light attenuation - float hazeRangeFactor{ convertHazeRangeToHazeRangeFactor(INITIAL_HAZE_RANGE) }; - float hazeHeightFactor{ convertHazeAltitudeToHazeAltitudeFactor(INITIAL_HAZE_HEIGHT) }; - float hazeKeyLightRangeFactor{ convertHazeRangeToHazeRangeFactor(INITIAL_KEY_LIGHT_RANGE) }; + float hazeRangeFactor { convertHazeRangeToHazeRangeFactor(INITIAL_HAZE_RANGE) }; + float hazeHeightFactor { convertHazeAltitudeToHazeAltitudeFactor(INITIAL_HAZE_HEIGHT) }; + float hazeKeyLightRangeFactor { convertHazeRangeToHazeRangeFactor(INITIAL_KEY_LIGHT_RANGE) }; - float hazeKeyLightAltitudeFactor{ convertHazeAltitudeToHazeAltitudeFactor(INITIAL_KEY_LIGHT_ALTITUDE) }; - // Padding required to align the structure to sizeof(vec4) - vec3 __padding; + float hazeKeyLightAltitudeFactor { convertHazeAltitudeToHazeAltitudeFactor(INITIAL_KEY_LIGHT_ALTITUDE) }; Parameters() {} }; diff --git a/libraries/graphics/src/graphics/Material.cpp b/libraries/graphics/src/graphics/Material.cpp index 7befb7e053..7743c4bf50 100755 --- a/libraries/graphics/src/graphics/Material.cpp +++ b/libraries/graphics/src/graphics/Material.cpp @@ -87,7 +87,7 @@ void Material::setUnlit(bool value) { } void Material::setAlbedo(const glm::vec3& albedo, bool isSRGB) { - _key.setAlbedo(glm::any(glm::greaterThan(albedo, glm::vec3(0.0f)))); + _key.setAlbedo(true); _albedo = (isSRGB ? ColorUtils::sRGBToLinearVec3(albedo) : albedo); } diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h index 1bd87332a1..cccfaa3f7d 100644 --- a/libraries/hfm/src/hfm/HFM.h +++ b/libraries/hfm/src/hfm/HFM.h @@ -75,8 +75,6 @@ struct JointShapeInfo { class Joint { public: JointShapeInfo shapeInfo; - QVector freeLineage; - bool isFree; int parentIndex; float distanceToParent; @@ -291,8 +289,6 @@ public: glm::mat4 offset; // This includes offset, rotation, and scale as specified by the FST file - glm::vec3 palmDirection; - glm::vec3 neckPivot; Extents bindExtents; @@ -319,7 +315,6 @@ public: QList blendshapeChannelNames; QMap jointRotationOffsets; - QMap hfmToHifiJointNameMapping; }; }; diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 8d1a82518d..1c2a2f5c63 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -19,13 +19,14 @@ #include "CalculateMeshTangentsTask.h" #include "CalculateBlendshapeNormalsTask.h" #include "CalculateBlendshapeTangentsTask.h" +#include "PrepareJointsTask.h" namespace baker { class GetModelPartsTask { public: using Input = hfm::Model::Pointer; - using Output = VaryingSet5, hifi::URL, baker::MeshIndicesToModelNames, baker::BlendshapesPerMesh, QHash>; + using Output = VaryingSet6, hifi::URL, baker::MeshIndicesToModelNames, baker::BlendshapesPerMesh, QHash, std::vector>; using JobModel = Job::ModelIO; void run(const BakeContextPointer& context, const Input& input, Output& output) { @@ -39,6 +40,7 @@ namespace baker { blendshapesPerMesh.push_back(hfmModelIn->meshes[i].blendshapes.toStdVector()); } output.edit4() = hfmModelIn->materials; + output.edit5() = hfmModelIn->joints.toStdVector(); } }; @@ -99,23 +101,29 @@ namespace baker { class BuildModelTask { public: - using Input = VaryingSet2>; + using Input = VaryingSet5, std::vector, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; using Output = hfm::Model::Pointer; using JobModel = Job::ModelIO; void run(const BakeContextPointer& context, const Input& input, Output& output) { auto hfmModelOut = input.get0(); hfmModelOut->meshes = QVector::fromStdVector(input.get1()); + hfmModelOut->joints = QVector::fromStdVector(input.get2()); + hfmModelOut->jointRotationOffsets = input.get3(); + hfmModelOut->jointIndices = input.get4(); output = hfmModelOut; } }; class BakerEngineBuilder { public: - using Input = hfm::Model::Pointer; + using Input = VaryingSet2; using Output = hfm::Model::Pointer; using JobModel = Task::ModelIO; - void build(JobModel& model, const Varying& hfmModelIn, Varying& hfmModelOut) { + void build(JobModel& model, const Varying& input, Varying& hfmModelOut) { + const auto& hfmModelIn = input.getN(0); + const auto& mapping = input.getN(1); + // Split up the inputs from hfm::Model const auto modelPartsIn = model.addJob("GetModelParts", hfmModelIn); const auto meshesIn = modelPartsIn.getN(0); @@ -123,6 +131,7 @@ namespace baker { const auto meshIndicesToModelNames = modelPartsIn.getN(2); const auto blendshapesPerMeshIn = modelPartsIn.getN(3); const auto materials = modelPartsIn.getN(4); + const auto jointsIn = modelPartsIn.getN(5); // Calculate normals and tangents for meshes and blendshapes if they do not exist // Note: Normals are never calculated here for OBJ models. OBJ files optionally define normals on a per-face basis, so for consistency normals are calculated beforehand in OBJSerializer. @@ -138,19 +147,27 @@ namespace baker { const auto buildGraphicsMeshInputs = BuildGraphicsMeshTask::Input(meshesIn, url, meshIndicesToModelNames, normalsPerMesh, tangentsPerMesh).asVarying(); const auto graphicsMeshes = model.addJob("BuildGraphicsMesh", buildGraphicsMeshInputs); + // Prepare joint information + const auto prepareJointsInputs = PrepareJointsTask::Input(jointsIn, mapping).asVarying(); + const auto jointInfoOut = model.addJob("PrepareJoints", prepareJointsInputs); + const auto jointsOut = jointInfoOut.getN(0); + const auto jointRotationOffsets = jointInfoOut.getN(1); + const auto jointIndices = jointInfoOut.getN(2); + // Combine the outputs into a new hfm::Model const auto buildBlendshapesInputs = BuildBlendshapesTask::Input(blendshapesPerMeshIn, normalsPerBlendshapePerMesh, tangentsPerBlendshapePerMesh).asVarying(); const auto blendshapesPerMeshOut = model.addJob("BuildBlendshapes", buildBlendshapesInputs); const auto buildMeshesInputs = BuildMeshesTask::Input(meshesIn, graphicsMeshes, normalsPerMesh, tangentsPerMesh, blendshapesPerMeshOut).asVarying(); const auto meshesOut = model.addJob("BuildMeshes", buildMeshesInputs); - const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut).asVarying(); + const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices).asVarying(); hfmModelOut = model.addJob("BuildModel", buildModelInputs); } }; - Baker::Baker(const hfm::Model::Pointer& hfmModel) : + Baker::Baker(const hfm::Model::Pointer& hfmModel, const QVariantHash& mapping) : _engine(std::make_shared(BakerEngineBuilder::JobModel::create("Baker"), std::make_shared())) { - _engine->feedInput(hfmModel); + _engine->feedInput(0, hfmModel); + _engine->feedInput(1, mapping); } void Baker::run() { diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 7fb3f420e0..41989d73df 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -12,6 +12,8 @@ #ifndef hifi_baker_Baker_h #define hifi_baker_Baker_h +#include + #include #include "Engine.h" @@ -19,7 +21,7 @@ namespace baker { class Baker { public: - Baker(const hfm::Model::Pointer& hfmModel); + Baker(const hfm::Model::Pointer& hfmModel, const QVariantHash& mapping); void run(); diff --git a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp index e94e15507e..6e12ec546d 100644 --- a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp +++ b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp @@ -13,6 +13,17 @@ #include "ModelMath.h" +bool needTangents(const hfm::Mesh& mesh, const QHash& materials) { + // Check if we actually need to calculate the tangents + for (const auto& meshPart : mesh.parts) { + auto materialIt = materials.find(meshPart.materialID); + if (materialIt != materials.end() && (*materialIt).needTangentSpace()) { + return true; + } + } + return false; +} + void CalculateMeshTangentsTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { const auto& normalsPerMesh = input.get0(); const std::vector& meshes = input.get1(); @@ -28,38 +39,20 @@ void CalculateMeshTangentsTask::run(const baker::BakeContextPointer& context, co auto& tangentsOut = tangentsPerMeshOut[tangentsPerMeshOut.size()-1]; // Check if we already have tangents and therefore do not need to do any calculation + // Otherwise confirm if we have the normals needed, and need to calculate the tangents if (!tangentsIn.empty()) { tangentsOut = tangentsIn.toStdVector(); - continue; + } else if (!normals.empty() && needTangents(mesh, materials)) { + tangentsOut.resize(normals.size()); + baker::calculateTangents(mesh, + [&mesh, &normals, &tangentsOut](int firstIndex, int secondIndex, glm::vec3* outVertices, glm::vec2* outTexCoords, glm::vec3& outNormal) { + outVertices[0] = mesh.vertices[firstIndex]; + outVertices[1] = mesh.vertices[secondIndex]; + outNormal = normals[firstIndex]; + outTexCoords[0] = mesh.texCoords[firstIndex]; + outTexCoords[1] = mesh.texCoords[secondIndex]; + return &(tangentsOut[firstIndex]); + }); } - - // Check if we have normals, and if not then tangents can't be calculated - if (normals.empty()) { - continue; - } - - // Check if we actually need to calculate the tangents - bool needTangents = false; - for (const auto& meshPart : mesh.parts) { - auto materialIt = materials.find(meshPart.materialID); - if (materialIt != materials.end() && (*materialIt).needTangentSpace()) { - needTangents = true; - break; - } - } - if (needTangents) { - continue; - } - - tangentsOut.resize(normals.size()); - baker::calculateTangents(mesh, - [&mesh, &normals, &tangentsOut](int firstIndex, int secondIndex, glm::vec3* outVertices, glm::vec2* outTexCoords, glm::vec3& outNormal) { - outVertices[0] = mesh.vertices[firstIndex]; - outVertices[1] = mesh.vertices[secondIndex]; - outNormal = normals[firstIndex]; - outTexCoords[0] = mesh.texCoords[firstIndex]; - outTexCoords[1] = mesh.texCoords[secondIndex]; - return &(tangentsOut[firstIndex]); - }); } } diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp new file mode 100644 index 0000000000..3b1a57cb43 --- /dev/null +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp @@ -0,0 +1,86 @@ +// +// PrepareJointsTask.cpp +// model-baker/src/model-baker +// +// Created by Sabrina Shanman on 2019/01/25. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "PrepareJointsTask.h" + +#include "ModelBakerLogging.h" + +QMap getJointNameMapping(const QVariantHash& mapping) { + static const QString JOINT_NAME_MAPPING_FIELD = "jointMap"; + QMap hfmToHifiJointNameMap; + if (!mapping.isEmpty() && mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].type() == QVariant::Hash) { + auto jointNames = mapping[JOINT_NAME_MAPPING_FIELD].toHash(); + for (auto itr = jointNames.begin(); itr != jointNames.end(); itr++) { + hfmToHifiJointNameMap.insert(itr.key(), itr.value().toString()); + qCDebug(model_baker) << "the mapped key " << itr.key() << " has a value of " << hfmToHifiJointNameMap[itr.key()]; + } + } + return hfmToHifiJointNameMap; +} + +QMap getJointRotationOffsets(const QVariantHash& mapping) { + QMap jointRotationOffsets; + static const QString JOINT_ROTATION_OFFSET_FIELD = "jointRotationOffset"; + if (!mapping.isEmpty() && mapping.contains(JOINT_ROTATION_OFFSET_FIELD) && mapping[JOINT_ROTATION_OFFSET_FIELD].type() == QVariant::Hash) { + auto offsets = mapping[JOINT_ROTATION_OFFSET_FIELD].toHash(); + for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { + QString jointName = itr.key(); + QString line = itr.value().toString(); + auto quatCoords = line.split(','); + if (quatCoords.size() == 4) { + float quatX = quatCoords[0].mid(1).toFloat(); + float quatY = quatCoords[1].toFloat(); + float quatZ = quatCoords[2].toFloat(); + float quatW = quatCoords[3].mid(0, quatCoords[3].size() - 1).toFloat(); + if (!isNaN(quatX) && !isNaN(quatY) && !isNaN(quatZ) && !isNaN(quatW)) { + glm::quat rotationOffset = glm::quat(quatW, quatX, quatY, quatZ); + jointRotationOffsets.insert(jointName, rotationOffset); + } + } + } + } + return jointRotationOffsets; +} + +void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { + const auto& jointsIn = input.get0(); + const auto& mapping = input.get1(); + auto& jointsOut = output.edit0(); + auto& jointRotationOffsets = output.edit1(); + auto& jointIndices = output.edit2(); + + // Get joint renames + auto jointNameMapping = getJointNameMapping(mapping); + // Apply joint metadata from FST file mappings + for (const auto& jointIn : jointsIn) { + jointsOut.push_back(jointIn); + auto& jointOut = jointsOut.back(); + + auto jointNameMapKey = jointNameMapping.key(jointIn.name); + if (jointNameMapping.contains(jointNameMapKey)) { + jointOut.name = jointNameMapKey; + } + + jointIndices.insert(jointOut.name, (int)jointsOut.size()); + } + + // Get joint rotation offsets from FST file mappings + auto offsets = getJointRotationOffsets(mapping); + for (auto itr = offsets.begin(); itr != offsets.end(); itr++) { + QString jointName = itr.key(); + int jointIndex = jointIndices.value(jointName) - 1; + if (jointIndex != -1) { + glm::quat rotationOffset = itr.value(); + jointRotationOffsets.insert(jointIndex, rotationOffset); + qCDebug(model_baker) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset; + } + } +} diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.h b/libraries/model-baker/src/model-baker/PrepareJointsTask.h new file mode 100644 index 0000000000..e12d8ffd2c --- /dev/null +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.h @@ -0,0 +1,30 @@ +// +// PrepareJointsTask.h +// model-baker/src/model-baker +// +// Created by Sabrina Shanman on 2019/01/25. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_PrepareJointsTask_h +#define hifi_PrepareJointsTask_h + +#include + +#include + +#include "Engine.h" + +class PrepareJointsTask { +public: + using Input = baker::VaryingSet2, QVariantHash /*mapping*/>; + using Output = baker::VaryingSet3, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; + using JobModel = baker::Job::ModelIO; + + void run(const baker::BakeContextPointer& context, const Input& input, Output& output); +}; + +#endif // hifi_PrepareJointsTask_h \ No newline at end of file diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 8c541040a7..299a2ad729 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -233,7 +233,7 @@ void GeometryReader::run() { } QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition", - Q_ARG(HFMModel::Pointer, hfmModel)); + Q_ARG(HFMModel::Pointer, hfmModel), Q_ARG(QVariantHash, _mapping)); } catch (const std::exception&) { auto resource = _resource.toStrongRef(); if (resource) { @@ -261,7 +261,7 @@ public: virtual void downloadFinished(const QByteArray& data) override; protected: - Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel); + Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, QVariantHash mapping); private: ModelLoader _modelLoader; @@ -277,9 +277,9 @@ void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { QThreadPool::globalInstance()->start(new GeometryReader(_modelLoader, _self, _effectiveBaseURL, _mapping, data, _combineParts, _request->getWebMediaType())); } -void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel) { +void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel, QVariantHash mapping) { // Do processing on the model - baker::Baker modelBaker(hfmModel); + baker::Baker modelBaker(hfmModel, mapping); modelBaker.run(); // Assume ownership of the processed HFMModel diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index cf77c1cad5..4647c50496 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -208,7 +208,7 @@ void AccountManager::setSessionID(const QUuid& sessionID) { } } -QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth::Type authType, const QUrlQuery & query) { +QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth::Type authType) { QNetworkRequest networkRequest; networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); @@ -217,17 +217,22 @@ QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth:: uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit()); QUrl requestURL = _authURL; - + if (requestURL.isEmpty()) { // Assignment client doesn't set _authURL. requestURL = getMetaverseServerURL(); } + int queryStringLocation = path.indexOf("?"); if (path.startsWith("/")) { - requestURL.setPath(path); + requestURL.setPath(path.left(queryStringLocation)); } else { - requestURL.setPath("/" + path); + requestURL.setPath("/" + path.left(queryStringLocation)); + } + + if (queryStringLocation >= 0) { + QUrlQuery query(path.mid(queryStringLocation+1)); + requestURL.setQuery(query); } - requestURL.setQuery(query); if (authType != AccountManagerAuth::None ) { if (hasValidAccessToken()) { @@ -253,8 +258,7 @@ void AccountManager::sendRequest(const QString& path, const JSONCallbackParameters& callbackParams, const QByteArray& dataByteArray, QHttpMultiPart* dataMultiPart, - const QVariantMap& propertyMap, - QUrlQuery query) { + const QVariantMap& propertyMap) { if (thread() != QThread::currentThread()) { QMetaObject::invokeMethod(this, "sendRequest", @@ -264,14 +268,13 @@ void AccountManager::sendRequest(const QString& path, Q_ARG(const JSONCallbackParameters&, callbackParams), Q_ARG(const QByteArray&, dataByteArray), Q_ARG(QHttpMultiPart*, dataMultiPart), - Q_ARG(QVariantMap, propertyMap), - Q_ARG(QUrlQuery, query)); + Q_ARG(QVariantMap, propertyMap)); return; } QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = createRequest(path, authType, query); + QNetworkRequest networkRequest = createRequest(path, authType); if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qCDebug(networking) << "Making a request to" << qPrintable(networkRequest.url().toString()); diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 2ccebdd73c..8732042e93 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -61,15 +61,14 @@ class AccountManager : public QObject, public Dependency { public: AccountManager(UserAgentGetter userAgentGetter = DEFAULT_USER_AGENT_GETTER); - QNetworkRequest createRequest(QString path, AccountManagerAuth::Type authType, const QUrlQuery & query = QUrlQuery()); + QNetworkRequest createRequest(QString path, AccountManagerAuth::Type authType); Q_INVOKABLE void sendRequest(const QString& path, AccountManagerAuth::Type authType, QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation, const JSONCallbackParameters& callbackParams = JSONCallbackParameters(), const QByteArray& dataByteArray = QByteArray(), QHttpMultiPart* dataMultiPart = NULL, - const QVariantMap& propertyMap = QVariantMap(), - QUrlQuery query = QUrlQuery()); + const QVariantMap& propertyMap = QVariantMap()); void setIsAgent(bool isAgent) { _isAgent = isAgent; } diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 1ebb488458..66ce5f32bf 100755 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -683,7 +683,7 @@ void CharacterController::updateState() { return; } if (_pendingFlags & PENDING_FLAG_RECOMPUTE_FLYING) { - SET_STATE(CharacterController::State::Hover, "recomputeFlying"); + SET_STATE(CharacterController::State::Hover, "recomputeFlying"); _hasSupport = false; _stepHeight = _minStepHeight; // clears memory of last step obstacle _pendingFlags &= ~PENDING_FLAG_RECOMPUTE_FLYING; @@ -795,15 +795,13 @@ void CharacterController::updateState() { // Transition to hover if we are above the fall threshold SET_STATE(State::Hover, "above fall threshold"); } - } else if (!rayHasHit && !_hasSupport) { - SET_STATE(State::Hover, "no ground detected"); } break; } case State::Hover: btScalar horizontalSpeed = (velocity - velocity.dot(_currentUp) * _currentUp).length(); bool flyingFast = horizontalSpeed > (MAX_WALKING_SPEED * 0.75f); - if (!_flyingAllowed && rayHasHit) { + if (!_flyingAllowed) { SET_STATE(State::InAir, "flying not allowed"); } else if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) { SET_STATE(State::InAir, "near ground"); diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 05cbde374d..ff8c270371 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -104,13 +104,13 @@ void ProceduralData::parse(const QJsonObject& proceduralData) { //} Procedural::Procedural() { - _opaqueState->setCullMode(gpu::State::CULL_BACK); + _opaqueState->setCullMode(gpu::State::CULL_NONE); _opaqueState->setDepthTest(true, true, gpu::LESS_EQUAL); _opaqueState->setBlendFunction(false, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - _transparentState->setCullMode(gpu::State::CULL_BACK); + _transparentState->setCullMode(gpu::State::CULL_NONE); _transparentState->setDepthTest(true, true, gpu::LESS_EQUAL); _transparentState->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, diff --git a/libraries/render-utils/src/AnimDebugDraw.cpp b/libraries/render-utils/src/AnimDebugDraw.cpp index bf528ee5f0..8944ae7996 100644 --- a/libraries/render-utils/src/AnimDebugDraw.cpp +++ b/libraries/render-utils/src/AnimDebugDraw.cpp @@ -374,7 +374,7 @@ void AnimDebugDraw::update() { glm::vec4 color = std::get<3>(iter.second); for (int i = 0; i < skeleton->getNumJoints(); i++) { - const float radius = BONE_RADIUS / (absPoses[i].scale().x * rootPose.scale().x); + const float radius = BONE_RADIUS / (absPoses[i].scale() * rootPose.scale()); // draw bone addBone(rootPose, absPoses[i], radius, color, v); @@ -394,16 +394,16 @@ void AnimDebugDraw::update() { glm::vec3 pos = std::get<1>(iter.second); glm::vec4 color = std::get<2>(iter.second); const float radius = POSE_RADIUS; - addBone(AnimPose::identity, AnimPose(glm::vec3(1), rot, pos), radius, color, v); + addBone(AnimPose::identity, AnimPose(1.0f, rot, pos), radius, color, v); } - AnimPose myAvatarPose(glm::vec3(1), DebugDraw::getInstance().getMyAvatarRot(), DebugDraw::getInstance().getMyAvatarPos()); + AnimPose myAvatarPose(1.0f, DebugDraw::getInstance().getMyAvatarRot(), DebugDraw::getInstance().getMyAvatarPos()); for (auto& iter : myAvatarMarkerMap) { glm::quat rot = std::get<0>(iter.second); glm::vec3 pos = std::get<1>(iter.second); glm::vec4 color = std::get<2>(iter.second); const float radius = POSE_RADIUS; - addBone(myAvatarPose, AnimPose(glm::vec3(1), rot, pos), radius, color, v); + addBone(myAvatarPose, AnimPose(1.0f, rot, pos), radius, color, v); } // draw rays from shared DebugDraw singleton diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index 81a81c5602..b70925201a 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -122,7 +122,7 @@ void CauterizedModel::updateClusterMatrices() { if (_useDualQuaternionSkinning) { auto jointPose = _rig.getJointPose(cluster.jointIndex); - Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); + Transform jointTransform(jointPose.rot(), glm::vec3(jointPose.scale()), jointPose.trans()); Transform clusterTransform; Transform::mult(clusterTransform, jointTransform, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindTransform); state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform); @@ -138,7 +138,7 @@ void CauterizedModel::updateClusterMatrices() { if (!_cauterizeBoneSet.empty()) { AnimPose cauterizePose = _rig.getJointPose(_rig.indexOfJoint("Neck")); - cauterizePose.scale() = glm::vec3(0.0001f, 0.0001f, 0.0001f); + cauterizePose.scale() = 0.0001f; static const glm::mat4 zeroScale( glm::vec4(0.0001f, 0.0f, 0.0f, 0.0f), @@ -161,7 +161,7 @@ void CauterizedModel::updateClusterMatrices() { // not cauterized so just copy the value from the non-cauterized version. state.clusterDualQuaternions[j] = _meshStates[i].clusterDualQuaternions[j]; } else { - Transform jointTransform(cauterizePose.rot(), cauterizePose.scale(), cauterizePose.trans()); + Transform jointTransform(cauterizePose.rot(), glm::vec3(cauterizePose.scale()), cauterizePose.trans()); Transform clusterTransform; Transform::mult(clusterTransform, jointTransform, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindTransform); state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform); diff --git a/libraries/render-utils/src/Haze.slh b/libraries/render-utils/src/Haze.slh index 0bf1d5d689..e2285febe4 100644 --- a/libraries/render-utils/src/Haze.slh +++ b/libraries/render-utils/src/Haze.slh @@ -28,7 +28,7 @@ struct HazeParams { vec3 colorModulationFactor; int hazeMode; - mat4 transform; + vec3 spare; float backgroundBlend; float hazeRangeFactor; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index da8dceb176..260e25009e 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1116,10 +1116,6 @@ int Model::getParentJointIndex(int jointIndex) const { return (isActive() && jointIndex != -1) ? getHFMModel().joints.at(jointIndex).parentIndex : -1; } -int Model::getLastFreeJointIndex(int jointIndex) const { - return (isActive() && jointIndex != -1) ? getHFMModel().joints.at(jointIndex).freeLineage.last() : -1; -} - void Model::setTextures(const QVariantMap& textures) { if (isLoaded()) { _needsFixupInScene = true; @@ -1368,7 +1364,7 @@ void Model::updateClusterMatrices() { if (_useDualQuaternionSkinning) { auto jointPose = _rig.getJointPose(cluster.jointIndex); - Transform jointTransform(jointPose.rot(), jointPose.scale(), jointPose.trans()); + Transform jointTransform(jointPose.rot(), glm::vec3(jointPose.scale()), jointPose.trans()); Transform clusterTransform; Transform::mult(clusterTransform, jointTransform, _rig.getAnimSkeleton()->getClusterBindMatricesOriginalValues(meshIndex, clusterIndex).inverseBindTransform); state.clusterDualQuaternions[j] = Model::TransformDualQuaternion(clusterTransform); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 16e08c2b23..c55178a21a 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -298,9 +298,9 @@ public: TransformDualQuaternion() {} TransformDualQuaternion(const glm::mat4& m) { AnimPose p(m); - _scale.x = p.scale().x; - _scale.y = p.scale().y; - _scale.z = p.scale().z; + _scale.x = p.scale(); + _scale.y = p.scale(); + _scale.z = p.scale(); _scale.w = 0.0f; _dq = DualQuaternion(p.rot(), p.trans()); } @@ -379,9 +379,6 @@ protected: /// Clear the joint states void clearJointState(int index); - /// Returns the index of the last free ancestor of the indexed joint, or -1 if not found. - int getLastFreeJointIndex(int jointIndex) const; - /// \param jointIndex index of joint in model structure /// \param position[out] position of joint in model-frame /// \return true if joint exists diff --git a/libraries/shared/src/CubicHermiteSpline.h b/libraries/shared/src/CubicHermiteSpline.h index da2ed26de4..cdbc64308d 100644 --- a/libraries/shared/src/CubicHermiteSpline.h +++ b/libraries/shared/src/CubicHermiteSpline.h @@ -60,7 +60,7 @@ protected: class CubicHermiteSplineFunctorWithArcLength : public CubicHermiteSplineFunctor { public: - enum Constants { NUM_SUBDIVISIONS = 30 }; + enum Constants { NUM_SUBDIVISIONS = 15 }; CubicHermiteSplineFunctorWithArcLength() : CubicHermiteSplineFunctor() { memset(_values, 0, sizeof(float) * (NUM_SUBDIVISIONS + 1)); @@ -71,11 +71,13 @@ public: float alpha = 0.0f; float accum = 0.0f; _values[0] = 0.0f; + glm::vec3 prevValue = this->operator()(alpha); for (int i = 1; i < NUM_SUBDIVISIONS + 1; i++) { - accum += glm::distance(this->operator()(alpha), - this->operator()(alpha + DELTA)); + glm::vec3 nextValue = this->operator()(alpha + DELTA); + accum += glm::distance(prevValue, nextValue); alpha += DELTA; _values[i] = accum; + prevValue = nextValue; } } diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 5394a0f448..ec1126c92f 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -1247,30 +1247,30 @@ void qVectorMeshFaceFromScriptValue(const QScriptValue& array, QVector } } -QVariantMap parseTexturesToMap(QString textures, const QVariantMap& defaultTextures) { +QVariantMap parseTexturesToMap(QString newTextures, const QVariantMap& defaultTextures) { // If textures are unset, revert to original textures - if (textures.isEmpty()) { + if (newTextures.isEmpty()) { return defaultTextures; } // Legacy: a ,\n-delimited list of filename:"texturepath" - if (*textures.cbegin() != '{') { - textures = "{\"" + textures.replace(":\"", "\":\"").replace(",\n", ",\"") + "}"; + if (*newTextures.cbegin() != '{') { + newTextures = "{\"" + newTextures.replace(":\"", "\":\"").replace(",\n", ",\"") + "}"; } QJsonParseError error; - QJsonDocument texturesJson = QJsonDocument::fromJson(textures.toUtf8(), &error); + QJsonDocument newTexturesJson = QJsonDocument::fromJson(newTextures.toUtf8(), &error); // If textures are invalid, revert to original textures if (error.error != QJsonParseError::NoError) { - qWarning() << "Could not evaluate textures property value:" << textures; + qWarning() << "Could not evaluate textures property value:" << newTextures; return defaultTextures; } - QVariantMap texturesMap = texturesJson.toVariant().toMap(); - // If textures are unset, revert to original textures - if (texturesMap.isEmpty()) { - return defaultTextures; + QVariantMap newTexturesMap = newTexturesJson.toVariant().toMap(); + QVariantMap toReturn = defaultTextures; + for (auto& texture : newTexturesMap.keys()) { + toReturn[texture] = newTexturesMap[texture]; } - return texturesJson.toVariant().toMap(); + return toReturn; } \ No newline at end of file diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index 564d79bfda..c256cf2b76 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -152,7 +152,8 @@ void ShapeInfo::setSphere(float radius) { void ShapeInfo::setMultiSphere(const std::vector& centers, const std::vector& radiuses) { _url = ""; _type = SHAPE_TYPE_MULTISPHERE; - assert(centers.size() == radiuses.size() && centers.size() > 0); + assert(centers.size() == radiuses.size()); + assert(centers.size() > 0); for (size_t i = 0; i < centers.size(); i++) { SphereData sphere = SphereData(centers[i], radiuses[i]); _sphereCollection.push_back(sphere); diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 71bb65509f..8e938ce25b 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -238,7 +238,10 @@ void OffscreenQmlSurface::clearFocusItem() { void OffscreenQmlSurface::initializeEngine(QQmlEngine* engine) { Parent::initializeEngine(engine); - QQmlFileSelector* fileSelector = new QQmlFileSelector(engine); + auto fileSelector = QQmlFileSelector::get(engine); + if (!fileSelector) { + fileSelector = new QQmlFileSelector(engine); + } fileSelector->setExtraSelectors(FileUtils::getFileSelectors()); static std::once_flag once; diff --git a/prebuild.py b/prebuild.py index fb54b8d6fe..060e1fd3b0 100644 --- a/prebuild.py +++ b/prebuild.py @@ -35,9 +35,50 @@ import re import tempfile import time import functools +import subprocess +import logging + +from uuid import uuid4 +from contextlib import contextmanager print = functools.partial(print, flush=True) +class TrackableLogger(logging.Logger): + guid = str(uuid4()) + + def _log(self, msg, *args, **kwargs): + x = {'guid': self.guid} + if 'extra' in kwargs: + kwargs['extra'].update(x) + else: + kwargs['extra'] = x + super()._log(msg, *args, **kwargs) + +logging.setLoggerClass(TrackableLogger) +logger = logging.getLogger('prebuild') + +def headSha(): + repo_dir = os.path.dirname(os.path.abspath(__file__)) + git = subprocess.Popen( + 'git rev-parse --short HEAD', + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + shell=True, cwd=repo_dir, universal_newlines=True, + ) + stdout, _ = git.communicate() + sha = stdout.split('\n')[0] + if not sha: + raise RuntimeError("couldn't find git sha") + return sha + +@contextmanager +def timer(name): + ''' Print the elapsed time a context's execution takes to execute ''' + start = time.time() + yield + # Please take care when modifiying this print statement. + # Log parsing logic may depend on it. + logger.info('%s took %.3f secs' % (name, time.time() - start)) + def parse_args(): # our custom ports, relative to the script location defaultPortsPath = hifi_utils.scriptRelative('cmake', 'ports') @@ -50,6 +91,7 @@ def parse_args(): parser.add_argument('--vcpkg-root', type=str, help='The location of the vcpkg distribution') parser.add_argument('--build-root', required=True, type=str, help='The location of the cmake build') parser.add_argument('--ports-path', type=str, default=defaultPortsPath) + parser.add_argument('--ci-build', action='store_true') if True: args = parser.parse_args() else: @@ -66,11 +108,19 @@ def main(): del os.environ[var] args = parse_args() + + if args.ci_build: + logging.basicConfig(datefmt='%s', format='%(asctime)s %(guid)s %(message)s', level=logging.INFO) + + logger.info('sha=%s' % headSha()) + logger.info('start') + # Only allow one instance of the program to run at a time pm = hifi_vcpkg.VcpkgRepo(args) with hifi_singleton.Singleton(pm.lockFile) as lock: - if not pm.upToDate(): - pm.bootstrap() + with timer('Bootstraping'): + if not pm.upToDate(): + pm.bootstrap() # Always write the tag, even if we changed nothing. This # allows vcpkg to reclaim disk space by identifying directories with @@ -80,11 +130,13 @@ def main(): # Grab our required dependencies: # * build host tools, like spirv-cross and scribe # * build client dependencies like openssl and nvtt - pm.setupDependencies() + with timer('Setting up dependencies'): + pm.setupDependencies() # wipe out the build directories (after writing the tag, since failure # here shouldn't invalidte the vcpkg install) - pm.cleanBuilds() + with timer('Cleaning builds'): + pm.cleanBuilds() # If we're running in android mode, we also need to grab a bunch of additional binaries # (this logic is all migrated from the old setupDependencies tasks in gradle) @@ -98,7 +150,10 @@ def main(): hifi_android.QtPackager(appPath, qtPath).bundle() # Write the vcpkg config to the build directory last - pm.writeConfig() + with timer('Writing configuration'): + pm.writeConfig() + + logger.info('end') print(sys.argv) main() diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index f059b91e81..b19873a049 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -681,6 +681,9 @@ function loaded() { if (isNullOrEmpty(valueB)) { return (isDefaultSort ? -1 : 1) * (isAscendingSort ? 1 : -1); } + if (typeof(valueA) === "string") { + return valueA.localeCompare(valueB); + } return valueA < valueB ? -1 : 1; }); }); diff --git a/scripts/system/modules/entityShapeVisualizer.js b/scripts/system/modules/entityShapeVisualizer.js index fe950c2e2b..da28369cdd 100644 --- a/scripts/system/modules/entityShapeVisualizer.js +++ b/scripts/system/modules/entityShapeVisualizer.js @@ -135,6 +135,7 @@ EntityShape.prototype = { overlayProperties.canCastShadows = false; overlayProperties.parentID = this.entityID; overlayProperties.collisionless = true; + overlayProperties.ignorePickIntersection = true; this.entity = Entities.addEntity(overlayProperties, "local"); var PROJECTED_MATERIALS = false; this.materialEntity = Entities.addEntity({ @@ -146,6 +147,7 @@ EntityShape.prototype = { priority: 1, materialMappingMode: PROJECTED_MATERIALS ? "projected" : "uv", materialURL: Script.resolvePath("../assets/images/materials/GridPattern.json"), + ignorePickIntersection: true, }, "local"); }, update: function() { diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 0cd9571e22..b1926efb71 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -22,9 +22,11 @@ #include #include #include +#include QTEST_MAIN(AnimTests) + const float TEST_EPSILON = 0.001f; void AnimTests::initTestCase() { @@ -372,16 +374,10 @@ void AnimTests::testAnimPose() { const glm::quat ROT_Y_180 = glm::angleAxis(PI, glm::vec3(0.0f, 1.0, 0.0f)); const glm::quat ROT_Z_30 = glm::angleAxis(PI / 6.0f, glm::vec3(1.0f, 0.0f, 0.0f)); - std::vector scaleVec = { - glm::vec3(1), - glm::vec3(2.0f, 1.0f, 1.0f), - glm::vec3(1.0f, 0.5f, 1.0f), - glm::vec3(1.0f, 1.0f, 1.5f), - glm::vec3(2.0f, 0.5f, 1.5f), - glm::vec3(-2.0f, 0.5f, 1.5f), - glm::vec3(2.0f, -0.5f, 1.5f), - glm::vec3(2.0f, 0.5f, -1.5f), - glm::vec3(-2.0f, -0.5f, -1.5f), + std::vector scaleVec = { + 1.0f, + 2.0f, + 0.5f }; std::vector rotVec = { @@ -411,7 +407,7 @@ void AnimTests::testAnimPose() { for (auto& trans : transVec) { // build a matrix the old fashioned way. - glm::mat4 scaleMat = glm::scale(glm::mat4(), scale); + glm::mat4 scaleMat = glm::scale(glm::mat4(), glm::vec3(scale)); glm::mat4 rotTransMat = createMatFromQuatAndPos(rot, trans); glm::mat4 rawMat = rotTransMat * scaleMat; @@ -429,7 +425,7 @@ void AnimTests::testAnimPose() { for (auto& trans : transVec) { // build a matrix the old fashioned way. - glm::mat4 scaleMat = glm::scale(glm::mat4(), scale); + glm::mat4 scaleMat = glm::scale(glm::mat4(), glm::vec3(scale)); glm::mat4 rotTransMat = createMatFromQuatAndPos(rot, trans); glm::mat4 rawMat = rotTransMat * scaleMat; @@ -445,6 +441,145 @@ void AnimTests::testAnimPose() { } } +void AnimTests::testAnimPoseMultiply() { + const float PI = (float)M_PI; + const glm::quat ROT_X_90 = glm::angleAxis(PI / 2.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + const glm::quat ROT_Y_180 = glm::angleAxis(PI, glm::vec3(0.0f, 1.0, 0.0f)); + const glm::quat ROT_Z_30 = glm::angleAxis(PI / 6.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + + std::vector scaleVec = { + 1.0f, + 2.0f, + 0.5f, + }; + + std::vector rotVec = { + glm::quat(), + ROT_X_90, + ROT_Y_180, + ROT_Z_30, + ROT_X_90 * ROT_Y_180 * ROT_Z_30, + -ROT_Y_180 + }; + + std::vector transVec = { + glm::vec3(), + glm::vec3(10.0f, 0.0f, 0.0f), + glm::vec3(0.0f, 5.0f, 0.0f), + glm::vec3(0.0f, 0.0f, 7.5f), + glm::vec3(10.0f, 5.0f, 7.5f), + glm::vec3(-10.0f, 5.0f, 7.5f), + glm::vec3(10.0f, -5.0f, 7.5f), + glm::vec3(10.0f, 5.0f, -7.5f) + }; + + const float TEST_EPSILON = 0.001f; + + std::vector matrixVec; + std::vector poseVec; + + for (auto& scale : scaleVec) { + for (auto& rot : rotVec) { + for (auto& trans : transVec) { + + // build a matrix the old fashioned way. + glm::mat4 scaleMat = glm::scale(glm::mat4(), glm::vec3(scale)); + glm::mat4 rotTransMat = createMatFromQuatAndPos(rot, trans); + glm::mat4 rawMat = rotTransMat * scaleMat; + + matrixVec.push_back(rawMat); + + // use an anim pose to build a matrix by parts. + AnimPose pose(scale, rot, trans); + poseVec.push_back(pose); + } + } + } + + for (int i = 0; i < matrixVec.size(); i++) { + for (int j = 0; j < matrixVec.size(); j++) { + + // multiply the matrices together + glm::mat4 matrix = matrixVec[i] * matrixVec[j]; + + // convert to matrix (note this will remove sheer from the matrix) + AnimPose resultA(matrix); + + // multiply the poses together directly + AnimPose resultB = poseVec[i] * poseVec[j]; + + /* + qDebug() << "matrixVec[" << i << "] =" << matrixVec[i]; + qDebug() << "matrixVec[" << j << "] =" << matrixVec[j]; + qDebug() << "matrixResult =" << resultA; + + qDebug() << "poseVec[" << i << "] =" << poseVec[i]; + qDebug() << "poseVec[" << j << "] =" << poseVec[j]; + qDebug() << "poseResult =" << resultB; + */ + + // compare results. + QCOMPARE_WITH_ABS_ERROR(resultA.scale(), resultB.scale(), TEST_EPSILON); + QCOMPARE_WITH_ABS_ERROR(resultA.rot(), resultB.rot(), TEST_EPSILON); + QCOMPARE_WITH_ABS_ERROR(resultA.trans(), resultB.trans(), TEST_EPSILON); + } + } +} + +void AnimTests::testAnimPoseInverse() { + const float PI = (float)M_PI; + const glm::quat ROT_X_90 = glm::angleAxis(PI / 2.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + const glm::quat ROT_Y_180 = glm::angleAxis(PI, glm::vec3(0.0f, 1.0, 0.0f)); + const glm::quat ROT_Z_30 = glm::angleAxis(PI / 6.0f, glm::vec3(1.0f, 0.0f, 0.0f)); + + std::vector scaleVec = { + 1.0f, + 2.0f, + 0.5f + }; + + std::vector rotVec = { + glm::quat(), + ROT_X_90, + ROT_Y_180, + ROT_Z_30, + ROT_X_90 * ROT_Y_180 * ROT_Z_30, + -ROT_Y_180 + }; + + std::vector transVec = { + glm::vec3(), + glm::vec3(10.0f, 0.0f, 0.0f), + glm::vec3(0.0f, 5.0f, 0.0f), + glm::vec3(0.0f, 0.0f, 7.5f), + glm::vec3(10.0f, 5.0f, 7.5f), + glm::vec3(-10.0f, 5.0f, 7.5f), + glm::vec3(10.0f, -5.0f, 7.5f), + glm::vec3(10.0f, 5.0f, -7.5f) + }; + + const float TEST_EPSILON = 0.001f; + + for (auto& scale : scaleVec) { + for (auto& rot : rotVec) { + for (auto& trans : transVec) { + + // build a matrix the old fashioned way. + glm::mat4 scaleMat = glm::scale(glm::mat4(), glm::vec3(scale)); + glm::mat4 rotTransMat = createMatFromQuatAndPos(rot, trans); + glm::mat4 rawMat = glm::inverse(rotTransMat * scaleMat); + + // use an anim pose to build a matrix by parts. + AnimPose pose(scale, rot, trans); + glm::mat4 poseMat = pose.inverse(); + + QCOMPARE_WITH_ABS_ERROR(rawMat, poseMat, TEST_EPSILON); + } + } + } +} + + void AnimTests::testExpressionTokenizer() { QString str = "(10 + x) >= 20.1 && (y != !z)"; AnimExpression e("x"); diff --git a/tests/animation/src/AnimTests.h b/tests/animation/src/AnimTests.h index 439793f21d..326545b0a9 100644 --- a/tests/animation/src/AnimTests.h +++ b/tests/animation/src/AnimTests.h @@ -27,6 +27,8 @@ private slots: void testVariant(); void testAccumulateTime(); void testAnimPose(); + void testAnimPoseMultiply(); + void testAnimPoseInverse(); void testExpressionTokenizer(); void testExpressionParser(); void testExpressionEvaluator(); diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 886f15ded4..4d7a594d92 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -19,20 +19,36 @@ function(check_test name) endfunction() if (BUILD_TOOLS) - set(ALL_TOOLS - udt-test - vhacd-util - frame-optimizer - gpu-frame-player - ice-client - ktx-tool - ac-client - skeleton-dump - atp-client - oven - nitpick - ) - + # Allow different tools for production builds + if (RELEASE_TYPE STREQUAL "PRODUCTION") + set(ALL_TOOLS + udt-test + vhacd-util + frame-optimizer + gpu-frame-player + ice-client + ktx-tool + ac-client + skeleton-dump + atp-client + oven + ) + else() + set(ALL_TOOLS + udt-test + vhacd-util + frame-optimizer + gpu-frame-player + ice-client + ktx-tool + ac-client + skeleton-dump + atp-client + oven + nitpick + ) + endif() + foreach(TOOL ${ALL_TOOLS}) check_test(${TOOL}) if (${BUILD_TOOL_RESULT}) diff --git a/tools/gpu-frame-player/CMakeLists.txt b/tools/gpu-frame-player/CMakeLists.txt index bd50839f9c..996fc859d8 100644 --- a/tools/gpu-frame-player/CMakeLists.txt +++ b/tools/gpu-frame-player/CMakeLists.txt @@ -7,7 +7,7 @@ setup_hifi_project(Gui Widgets) # link in the shared libraries link_hifi_libraries( - shared ktx shaders gpu + shared ktx shaders gpu # vk gpu-vk gl ${PLATFORM_GL_BACKEND} ) diff --git a/tools/nitpick/CMakeLists.txt b/tools/nitpick/CMakeLists.txt index efb5125f69..e69b16b866 100644 --- a/tools/nitpick/CMakeLists.txt +++ b/tools/nitpick/CMakeLists.txt @@ -1,75 +1,197 @@ -set (TARGET_NAME nitpick) +set(TARGET_NAME nitpick) project(${TARGET_NAME}) -# Automatically run UIC and MOC. This replaces the older WRAP macros -SET (CMAKE_AUTOUIC ON) -SET (CMAKE_AUTOMOC ON) +set(CUSTOM_NITPICK_QRC_PATHS "") -setup_hifi_project (Core Widgets Network Xml) -link_hifi_libraries () +find_npm() -# FIX: Qt was built with -reduce-relocations -if (Qt5_POSITION_INDEPENDENT_CODE) - SET (CMAKE_POSITION_INDEPENDENT_CODE ON) -endif() +set(RESOURCES_QRC ${CMAKE_CURRENT_BINARY_DIR}/resources.qrc) +set(RESOURCES_RCC ${CMAKE_CURRENT_SOURCE_DIR}/compiledResources/resources.rcc) +generate_qrc(OUTPUT ${RESOURCES_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources CUSTOM_PATHS ${CUSTOM_NITPICK_QRC_PATHS} GLOBS *) -# Qt includes -include_directories (${CMAKE_CURRENT_SOURCE_DIR}) -include_directories (${Qt5Core_INCLUDE_DIRS}) -include_directories (${Qt5Widgets_INCLUDE_DIRS}) +add_custom_command( + OUTPUT ${RESOURCES_RCC} + DEPENDS ${RESOURCES_QRC} ${GENERATE_QRC_DEPENDS} + COMMAND "${QT_DIR}/bin/rcc" + ARGS ${RESOURCES_QRC} -binary -o ${RESOURCES_RCC} +) -set (QT_LIBRARIES Qt5::Core Qt5::Widgets QT::Gui Qt5::Xml) +# grab the implementation and header files from src dirs +file(GLOB_RECURSE NITPICK_SRCS "src/*.cpp" "src/*.h") +GroupSources("src") +list(APPEND NITPICK_SRCS ${RESOURCES_RCC}) -if (WIN32) - # Do not show Console - set_property (TARGET nitpick PROPERTY WIN32_EXECUTABLE true) -endif() +find_package(Qt5 COMPONENTS Widgets) -target_zlib() -add_dependency_external_projects (quazip) -find_package (QuaZip REQUIRED) -target_include_directories( ${TARGET_NAME} SYSTEM PUBLIC ${QUAZIP_INCLUDE_DIRS}) -target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) - -package_libraries_for_deployment() +# grab the ui files in ui +file (GLOB_RECURSE QT_UI_FILES ui/*.ui) +source_group("UI Files" FILES ${QT_UI_FILES}) -if (WIN32) - add_paths_to_fixup_libs (${QUAZIP_DLL_PATH}) +# have qt5 wrap them and generate the appropriate header files +qt5_wrap_ui(QT_UI_HEADERS "${QT_UI_FILES}") - find_program(WINDEPLOYQT_COMMAND windeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) - - if (NOT WINDEPLOYQT_COMMAND) - message(FATAL_ERROR "Could not find windeployqt at ${QT_DIR}/bin. windeployqt is required.") +# add them to the nitpick source files +set(NITPICK_SRCS ${NITPICK_SRCS} "${QT_UI_HEADERS}" "${QT_RESOURCES}") + +if (APPLE) + # configure CMake to use a custom Info.plist + set_target_properties(${this_target} PROPERTIES MACOSX_BUNDLE_INFO_PLIST MacOSXBundleInfo.plist.in) + + if (PRODUCTION_BUILD) + set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.nitpick) + else () + if (DEV_BUILD) + set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.nitpick-dev) + elseif (PR_BUILD) + set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.nitpick-pr) + endif () endif () - # add a post-build command to call windeployqt to copy Qt plugins - add_custom_command( - TARGET ${TARGET_NAME} - POST_BUILD - COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> \"$\"" - ) - - # add a custom command to copy the empty Apps/Data High Fidelity folder (i.e. - a valid folder with no entities) - # this also copied to the containing folder, to facilitate running from Visual Studio - add_custom_command( - TARGET ${TARGET_NAME} - POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "$/AppDataHighFidelity" - COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "AppDataHighFidelity" - ) - - # add a custom command to copy the SSL DLLs - add_custom_command( - TARGET ${TARGET_NAME} - POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy_directory "$ENV{VCPKG_ROOT}/installed/x64-windows/bin" "$" - ) -elseif (APPLE) - # add a custom command to copy the empty Apps/Data High Fidelity folder (i.e. - a valid folder with no entities) - add_custom_command( - TARGET ${TARGET_NAME} - POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "$/AppDataHighFidelity" - ) + # set how the icon shows up in the Info.plist file + set(MACOSX_BUNDLE_ICON_FILE "${NITPICK_ICON_FILENAME}") + + # set where in the bundle to put the resources file + set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/icon/${NITPICK_ICON_FILENAME} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + + # append the discovered resources to our list of nitpick sources + list(APPEND NITPICK_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/icon/${NITPICK_ICON_FILENAME}) +endif() + +# create the executable, make it a bundle on OS X +if (APPLE) + add_executable(${TARGET_NAME} MACOSX_BUNDLE ${NITPICK_SRCS} ${QM}) + + # make sure the output name for the .app bundle is correct + # Fix up the rpath so macdeployqt works + set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "@executable_path/../Frameworks") +elseif (WIN32) + # configure an rc file for the chosen icon + set(CONFIGURE_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/icon/${NITPICK_ICON_FILENAME}") + set(CONFIGURE_ICON_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/Icon.rc") + configure_file("${HF_CMAKE_DIR}/templates/Icon.rc.in" ${CONFIGURE_ICON_RC_OUTPUT}) + + set(APP_FULL_NAME "High Fidelity Nitpick") + set(CONFIGURE_VERSION_INFO_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/VersionInfo.rc") + configure_file("${HF_CMAKE_DIR}/templates/VersionInfo.rc.in" ${CONFIGURE_VERSION_INFO_RC_OUTPUT}) + + # add an executable that also has the icon itself and the configured rc file as resources + add_executable(${TARGET_NAME} WIN32 ${NITPICK_SRCS} ${QM} ${CONFIGURE_ICON_RC_OUTPUT} ${CONFIGURE_VERSION_INFO_RC_OUTPUT}) +else () + add_executable(${TARGET_NAME} ${NITPICK_SRCS} ${QM}) endif () +add_dependencies(${TARGET_NAME} resources) + +# disable /OPT:REF and /OPT:ICF for the Debug builds +# This will prevent the following linker warnings +# LINK : warning LNK4075: ignoring '/INCREMENTAL' due to '/OPT:ICF' specification +if (WIN32) + set_property(TARGET ${TARGET_NAME} APPEND_STRING PROPERTY LINK_FLAGS_DEBUG "/OPT:NOREF /OPT:NOICF") +endif() + +link_hifi_libraries(entities-renderer) + +# perform standard include and linking for found externals +foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) + if (${${EXTERNAL}_UPPERCASE}_REQUIRED) + find_package(${EXTERNAL} REQUIRED) + else () + find_package(${EXTERNAL}) + endif () + + if (${${EXTERNAL}_UPPERCASE}_FOUND AND NOT DISABLE_${${EXTERNAL}_UPPERCASE}) + add_definitions(-DHAVE_${${EXTERNAL}_UPPERCASE}) + + # include the library directories (ignoring warnings) + if (NOT ${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS) + set(${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIR}) + endif () + + include_directories(SYSTEM ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) + + # perform the system include hack for OS X to ignore warnings + if (APPLE) + foreach(EXTERNAL_INCLUDE_DIR ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -isystem ${EXTERNAL_INCLUDE_DIR}") + endforeach() + endif () + + if (NOT ${${EXTERNAL}_UPPERCASE}_LIBRARIES) + set(${${EXTERNAL}_UPPERCASE}_LIBRARIES ${${${EXTERNAL}_UPPERCASE}_LIBRARY}) + endif () + + if (NOT APPLE OR NOT ${${EXTERNAL}_UPPERCASE} MATCHES "SIXENSE") + target_link_libraries(${TARGET_NAME} ${${${EXTERNAL}_UPPERCASE}_LIBRARIES}) + elseif (APPLE AND NOT INSTALLER_BUILD) + add_definitions(-DSIXENSE_LIB_FILENAME=\"${${${EXTERNAL}_UPPERCASE}_LIBRARY_RELEASE}\") + endif () + endif () +endforeach() + +# include headers for nitpick and NitpickConfig. +include_directories("${PROJECT_SOURCE_DIR}/src") + +if (UNIX AND NOT ANDROID) + if (CMAKE_SYSTEM_NAME MATCHES "Linux") + # Linux + target_link_libraries(${TARGET_NAME} pthread atomic) + else () + # OSX + target_link_libraries(${TARGET_NAME} pthread) + endif () +endif() + +# add a custom command to copy the empty AppData High Fidelity folder (i.e. - a valid folder with no entities) +if (WIN32) + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "$/AppDataHighFidelity" + ) + + if (RELEASE_TYPE STREQUAL "DEV") + # This to enable running from the IDE + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "AppDataHighFidelity" + ) + endif () +elseif (APPLE) + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "$/AppDataHighFidelity" + ) +endif() + +if (APPLE) + # setup install of OS X nitpick bundle + install(TARGETS ${TARGET_NAME} + BUNDLE DESTINATION ${NITPICK_INSTALL_DIR} + COMPONENT ${CLIENT_COMPONENT} + ) + + # call the fixup_nitpick macro to add required bundling commands for installation + fixup_nitpick() +elseif (WIN32) + # link target to external libraries + # setup install of executable and things copied by fixup/windeployqt + install( + DIRECTORY "$/" + DESTINATION ${NITPICK_INSTALL_DIR} + COMPONENT ${CLIENT_COMPONENT} + PATTERN "*.pdb" EXCLUDE + PATTERN "*.lib" EXCLUDE + PATTERN "*.exp" EXCLUDE + ) +endif() + +if (WIN32) + set(EXTRA_DEPLOY_OPTIONS "--qmldir \"${PROJECT_SOURCE_DIR}/resources/qml\"") + + set(TARGET_INSTALL_DIR ${NITPICK_INSTALL_DIR}) + set(TARGET_INSTALL_COMPONENT ${CLIENT_COMPONENT}) + + package_libraries_for_deployment() +endif() diff --git a/tools/nitpick/README.md b/tools/nitpick/README.md index 3a664a12e9..c5e3a5e21d 100644 --- a/tools/nitpick/README.md +++ b/tools/nitpick/README.md @@ -6,89 +6,61 @@ Nitpick is a stand alone application that provides a mechanism for regression te * The result, if any test failed, is a zipped folder describing the failure. Nitpick has 5 functions, separated into separate tabs: + 1. Creating tests, MD files and recursive scripts 1. Windows task bar utility (Windows only) 1. Running tests 1. Evaluating the results of running tests 1. Web interface -## Build (for developers) -Nitpick is built as part of the High Fidelity build. -XXXX refers to the version number - replace as necessary. For example, replace with 3.2.11 -### Creating installers -#### Windows -1. Perform Release build -1. Verify that 7Zip is installed. -1. cd to the `build\tools\nitpick\Release` directory -1. Delete any existing installers (named nitpick-installer-###.exe) -1. Select all, right-click and select 7-Zip->Add to archive... -1. Set Archive format to 7z -1. Check "Create SFX archive -1. Enter installer name (i.e. `nitpick-installer-vXXXX.exe`) -1. Click "OK" -1. Copy created installer to https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-vXXXX.exe: aws s3 cp nitpick-installer-vXXXX.exe s3://hifi-qa/nitpick/Mac/nitpick-installer-vXXXX.exe -#### Mac -These steps assume the hifi repository has been cloned to `~/hifi`. -1. (first time) Install brew - In a terminal: `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)` -1. (First time) install create-dmg: - In a terminal: `brew install create-dmg` -1. Perform Release build -1. In a terminal: cd to the `build/tools/nitpick/Release` folder -1. Copy the quazip dynamic library (note final period): - In a terminal: `cp ~/hifi/build/ext/Xcode/quazip/project/lib/libquazip5.1.dylib .` -1. Change the loader instruction to find the dynamic library locally - In a terminal: `install_name_tool -change ~/hifi/build/ext/Xcode/quazip/project/lib/libquazip5.1.dylib libquazip5.1.dylib nitpick` -1. Delete any existing disk images. In a terminal: `rm *.dmg` -1. Create installer (note final period).In a terminal: `create-dmg --volname nitpick-installer-vXXXX nitpick-installer-vXXXX.dmg .` - Make sure to wait for completion. -1. Copy created installer to AWS: `~/Library/Python/3.7/bin/aws s3 cp nitpick-installer-vXXXX.dmg s3://hifi-qa/nitpick/Mac/nitpick-installer-vXXXX.dmg` -### Installation -#### Windows -1. (First time) download and install vc_redist.x64.exe (available at https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-vXXXX.exe) +## Installation +`nitpick` is packaged with High Fidelity PR and Development builds. +### Windows 1. (First time) download and install Python 3 from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/python-3.7.0-amd64.exe (also located at https://www.python.org/downloads/) + 1. Click the "add python to path" checkbox on the python installer 1. After installation - create an environment variable called PYTHON_PATH and set it to the folder containing the Python executable. 1. (First time) download and install AWS CLI from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/AWSCLI64PY3.msi (also available at https://aws.amazon.com/cli/ - 1. Open a new command prompt and run `aws configure` + 1. Open a new command prompt and run + `aws configure` 1. Enter the AWS account number 1. Enter the secret key 1. Leave region name and ouput format as default [None] - 1. Install the latest release of Boto3 via pip: `pip install boto3` + 1. Install the latest release of Boto3 via pip: + `pip install boto3` -1. Download the installer by browsing to [here]() -1. Double click on the installer and install to a convenient location -![](./setup_7z.PNG) - -1. __To run nitpick, double click **nitpick.exe**__ -#### Mac +1. (First time) Download adb (Android Debug Bridge) from *https://dl.google.com/android/repository/platform-tools-latest-windows.zip* + 1. Copy the downloaded file to (for example) **C:\adb** and extract in place. + Verify you see *adb.exe* in **C:\adb\platform-tools\\**. + 1. Create an environment variable named ADB_PATH and set its value to the installation location (e.g. **C:\adb**) +### Mac 1. (first time) Install brew - In a terminal: `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)` + In a terminal: + `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` + Note that you will need to press RETURN again, and will then be asked for your password. 1. (First time) install Qt: - In a terminal: `brew install qt` -1. (First time) install Python from https://www.python.org/downloads/release/python-370/ (**macOS 64-bit installer** or **macOS 64-bit/32-bit installer**) - 1. After installation - In a terminal: run `open "/Applications/Python 3.6/Install Certificates.command"`. This is needed because the Mac Python supplied no longer links with the deprecated Apple-supplied system OpenSSL libraries but rather supplies a private copy of OpenSSL 1.0.2 which does not automatically access the system default root certificates. + In a terminal: +`brew install qt` +1. (First time) install Python from https://www.python.org/downloads/release/python-370/ (*macOS 64-bit installer* or *macOS 64-bit/32-bit installer*) + 1. After installation - In a terminal: run + `open "/Applications/Python 3.7/Install Certificates.command"`. +This is needed because the Mac Python supplied no longer links with the deprecated Apple-supplied system OpenSSL libraries but rather supplies a private copy of OpenSSL 1.0.2 which does not automatically access the system default root certificates. 1. Verify that `/usr/local/bin/python3` exists. 1. (First time - AWS interface) Install pip with the script provided by the Python Packaging Authority: -In a terminal: `curl -O https://bootstrap.pypa.io/get-pip.py` -In a terminal: `python3 get-pip.py --user` + In a terminal: + `curl -O https://bootstrap.pypa.io/get-pip.py` + In a terminal: + `python3 get-pip.py --user` 1. Use pip to install the AWS CLI. - `pip3 install awscli --upgrade --user` + `pip3 install awscli --upgrade --user` This will install aws in your user. For user XXX, aws will be located in ~/Library/Python/3.7/bin - 1. Open a new command prompt and run `~/Library/Python/3.7/bin/aws configure` + 1. Open a new command prompt and run + `~/Library/Python/3.7/bin/aws configure` 1. Enter the AWS account number 1. Enter the secret key 1. Leave region name and ouput format as default [None] - 1. Install the latest release of Boto3 via pip: pip3 install boto3 -1. Download the installer by browsing to [here](). -1. Double-click on the downloaded image to mount it -1. Create a folder for the nitpick files (e.g. ~/nitpick) - If this folder exists then delete all it's contents. -1. Copy the downloaded files to the folder - In a terminal: - `cd ~/nitpick` - `cp -r /Volumes/nitpick-installer-vXXXX/* .` - -1. __To run nitpick, cd to the folder that you copied to and run `./nitpick`__ + 1. Install the latest release of Boto3 via pip: pip3 install boto3 +1. (First time)Install adb (the Android Debug Bridge) - in a terminal: + `brew cask install android-platform-tools` # Usage ## Create ![](./Create.PNG) @@ -167,7 +139,7 @@ nitpick.runRecursive(); In this case all recursive scripts, from the selected folder down, are created. Running this function in the tests root folder will create (or update) all the recursive scripts. -## Windows +## Windows (only) ![](./Windows.PNG) This tab is Windows-specific. It provides buttons to hide and show the task bar. diff --git a/tools/nitpick/compiledResources/resources.rcc b/tools/nitpick/compiledResources/resources.rcc new file mode 100644 index 0000000000..15f51ed7f4 Binary files /dev/null and b/tools/nitpick/compiledResources/resources.rcc differ diff --git a/tools/nitpick/icon/nitpick.icns b/tools/nitpick/icon/nitpick.icns new file mode 100644 index 0000000000..332539af2a Binary files /dev/null and b/tools/nitpick/icon/nitpick.icns differ diff --git a/tools/nitpick/icon/nitpick.ico b/tools/nitpick/icon/nitpick.ico new file mode 100644 index 0000000000..e3d852cb41 Binary files /dev/null and b/tools/nitpick/icon/nitpick.ico differ diff --git a/tools/nitpick/src/AWSInterface.h b/tools/nitpick/src/AWSInterface.h index fda250b115..63c48580f5 100644 --- a/tools/nitpick/src/AWSInterface.h +++ b/tools/nitpick/src/AWSInterface.h @@ -16,7 +16,7 @@ #include #include -#include "ui/BusyWindow.h" +#include "BusyWindow.h" #include "PythonInterface.h" diff --git a/tools/nitpick/src/ui/BusyWindow.cpp b/tools/nitpick/src/BusyWindow.cpp similarity index 100% rename from tools/nitpick/src/ui/BusyWindow.cpp rename to tools/nitpick/src/BusyWindow.cpp diff --git a/tools/nitpick/src/ui/BusyWindow.h b/tools/nitpick/src/BusyWindow.h similarity index 100% rename from tools/nitpick/src/ui/BusyWindow.h rename to tools/nitpick/src/BusyWindow.h diff --git a/tools/nitpick/src/ui/MismatchWindow.cpp b/tools/nitpick/src/MismatchWindow.cpp similarity index 100% rename from tools/nitpick/src/ui/MismatchWindow.cpp rename to tools/nitpick/src/MismatchWindow.cpp diff --git a/tools/nitpick/src/ui/MismatchWindow.h b/tools/nitpick/src/MismatchWindow.h similarity index 97% rename from tools/nitpick/src/ui/MismatchWindow.h rename to tools/nitpick/src/MismatchWindow.h index 30c29076b3..040e0b8bf1 100644 --- a/tools/nitpick/src/ui/MismatchWindow.h +++ b/tools/nitpick/src/MismatchWindow.h @@ -12,7 +12,7 @@ #include "ui_MismatchWindow.h" -#include "../common.h" +#include "common.h" class MismatchWindow : public QDialog, public Ui::MismatchWindow { Q_OBJECT diff --git a/tools/nitpick/src/ui/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp similarity index 65% rename from tools/nitpick/src/ui/Nitpick.cpp rename to tools/nitpick/src/Nitpick.cpp index 0bd397715b..fa53730ce0 100644 --- a/tools/nitpick/src/ui/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -26,7 +26,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _signalMapper = new QSignalMapper(); - connect(_ui.actionClose, &QAction::triggered, this, &Nitpick::on_closeButton_clicked); + connect(_ui.actionClose, &QAction::triggered, this, &Nitpick::on_closePushbutton_clicked); connect(_ui.actionAbout, &QAction::triggered, this, &Nitpick::about); connect(_ui.actionContent, &QAction::triggered, this, &Nitpick::content); @@ -35,10 +35,12 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.tabWidget->removeTab(1); #endif - _ui.statusLabel->setText(""); - _ui.plainTextEdit->setReadOnly(true); + _ui.statusLabelOnDesktop->setText(""); + _ui.statusLabelOnMobile->setText(""); + + _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v1.3.2"); + setWindowTitle("Nitpick - v2.1.0"); } Nitpick::~Nitpick() { @@ -48,8 +50,12 @@ Nitpick::~Nitpick() { delete _test; } - if (_testRunner) { - delete _testRunner; + if (_testRunnerDesktop) { + delete _testRunnerDesktop; + } + + if (_testRunnerMobile) { + delete _testRunnerMobile; } } @@ -80,10 +86,38 @@ void Nitpick::setup() { timeEdits.emplace_back(_ui.timeEdit3); timeEdits.emplace_back(_ui.timeEdit4); - if (_testRunner) { - delete _testRunner; + // Create the two test runners + if (_testRunnerDesktop) { + delete _testRunnerDesktop; } - _testRunner = new TestRunner(dayCheckboxes, timeEditCheckboxes, timeEdits, _ui.workingFolderLabel, _ui.checkBoxServerless, _ui.checkBoxRunLatest, _ui.urlLineEdit, _ui.runNowButton); + _testRunnerDesktop = new TestRunnerDesktop( + dayCheckboxes, + timeEditCheckboxes, + timeEdits, + _ui.workingFolderRunOnDesktopLabel, + _ui.checkBoxServerless, + _ui.runLatestOnDesktopCheckBox, + _ui.urlOnDesktopLineEdit, + _ui.runNowPushbutton, + _ui.statusLabelOnDesktop + ); + + if (_testRunnerMobile) { + delete _testRunnerMobile; + } + _testRunnerMobile = new TestRunnerMobile( + _ui.workingFolderRunOnMobileLabel, + _ui.connectDevicePushbutton, + _ui.pullFolderPushbutton, + _ui.detectedDeviceLabel, + _ui.folderLineEdit, + _ui.downloadAPKPushbutton, + _ui.installAPKPushbutton, + _ui.runInterfacePushbutton, + _ui.runLatestOnMobileCheckBox, + _ui.urlOnMobileLineEdit, + _ui.statusLabelOnMobile + ); } void Nitpick::startTestsEvaluation(const bool isRunningFromCommandLine, @@ -98,9 +132,9 @@ void Nitpick::startTestsEvaluation(const bool isRunningFromCommandLine, void Nitpick::on_tabWidget_currentChanged(int index) { // Enable the GitHub edit boxes as required #ifdef Q_OS_WIN - if (index == 0 || index == 2 || index == 3) { + if (index == 0 || index == 2 || index == 3 || index == 4) { #else - if (index == 0 || index == 1 || index == 2) { + if (index == 0 || index == 1 || index == 2 || index == 3) { #endif _ui.userLineEdit->setDisabled(false); _ui.branchLineEdit->setDisabled(false); @@ -110,43 +144,43 @@ void Nitpick::on_tabWidget_currentChanged(int index) { } } -void Nitpick::on_evaluateTestsButton_clicked() { +void Nitpick::on_evaluateTestsPushbutton_clicked() { _test->startTestsEvaluation(false, false); } -void Nitpick::on_createRecursiveScriptButton_clicked() { +void Nitpick::on_createRecursiveScriptPushbutton_clicked() { _test->createRecursiveScript(); } -void Nitpick::on_createAllRecursiveScriptsButton_clicked() { +void Nitpick::on_createAllRecursiveScriptsPushbutton_clicked() { _test->createAllRecursiveScripts(); } -void Nitpick::on_createTestsButton_clicked() { +void Nitpick::on_createTestsPushbutton_clicked() { _test->createTests(); } -void Nitpick::on_createMDFileButton_clicked() { +void Nitpick::on_createMDFilePushbutton_clicked() { _test->createMDFile(); } -void Nitpick::on_createAllMDFilesButton_clicked() { +void Nitpick::on_createAllMDFilesPushbutton_clicked() { _test->createAllMDFiles(); } -void Nitpick::on_createTestAutoScriptButton_clicked() { +void Nitpick::on_createTestAutoScriptPushbutton_clicked() { _test->createTestAutoScript(); } -void Nitpick::on_createAllTestAutoScriptsButton_clicked() { +void Nitpick::on_createAllTestAutoScriptsPushbutton_clicked() { _test->createAllTestAutoScripts(); } -void Nitpick::on_createTestsOutlineButton_clicked() { +void Nitpick::on_createTestsOutlinePushbutton_clicked() { _test->createTestsOutline(); } -void Nitpick::on_createTestRailTestCasesButton_clicked() { +void Nitpick::on_createTestRailTestCasesPushbutton_clicked() { _test->createTestRailTestCases(); } @@ -154,29 +188,29 @@ void Nitpick::on_createTestRailRunButton_clicked() { _test->createTestRailRun(); } -void Nitpick::on_setWorkingFolderButton_clicked() { - _testRunner->setWorkingFolder(); +void Nitpick::on_setWorkingFolderRunOnDesktopPushbutton_clicked() { + _testRunnerDesktop->setWorkingFolderAndEnableControls(); } void Nitpick::enableRunTabControls() { - _ui.runNowButton->setEnabled(true); + _ui.runNowPushbutton->setEnabled(true); _ui.daysGroupBox->setEnabled(true); _ui.timesGroupBox->setEnabled(true); } -void Nitpick::on_runNowButton_clicked() { - _testRunner->run(); +void Nitpick::on_runNowPushbutton_clicked() { + _testRunnerDesktop->run(); } -void Nitpick::on_checkBoxRunLatest_clicked() { - _ui.urlLineEdit->setEnabled(!_ui.checkBoxRunLatest->isChecked()); +void Nitpick::on_runLatestOnDesktopCheckBox_clicked() { + _ui.urlOnDesktopLineEdit->setEnabled(!_ui.runLatestOnDesktopCheckBox->isChecked()); } void Nitpick::automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures) { - _testRunner->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures); + _testRunnerDesktop->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures); } -void Nitpick::on_updateTestRailRunResultsButton_clicked() { +void Nitpick::on_updateTestRailRunResultsPushbutton_clicked() { _test->updateTestRailRunResult(); } @@ -184,7 +218,7 @@ void Nitpick::on_updateTestRailRunResultsButton_clicked() { // if (uState & ABS_AUTOHIDE) on_showTaskbarButton_clicked(); // else on_hideTaskbarButton_clicked(); // -void Nitpick::on_hideTaskbarButton_clicked() { +void Nitpick::on_hideTaskbarPushbutton_clicked() { #ifdef Q_OS_WIN APPBARDATA abd = { sizeof abd }; UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd); @@ -194,7 +228,7 @@ void Nitpick::on_hideTaskbarButton_clicked() { #endif } -void Nitpick::on_showTaskbarButton_clicked() { +void Nitpick::on_showTaskbarPushbutton_clicked() { #ifdef Q_OS_WIN APPBARDATA abd = { sizeof abd }; UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd); @@ -204,7 +238,7 @@ void Nitpick::on_showTaskbarButton_clicked() { #endif } -void Nitpick::on_closeButton_clicked() { +void Nitpick::on_closePushbutton_clicked() { exit(0); } @@ -216,7 +250,7 @@ void Nitpick::on_createXMLScriptRadioButton_clicked() { _test->setTestRailCreateMode(XML); } -void Nitpick::on_createWebPagePushButton_clicked() { +void Nitpick::on_createWebPagePushbutton_clicked() { _test->createWebPage(_ui.updateAWSCheckBox, _ui.awsURLLineEdit); } @@ -273,9 +307,13 @@ void Nitpick::saveFile(int index) { disconnect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int))); if (_caller == _test) { _test->finishTestsEvaluation(); - } else if (_caller == _testRunner) { - _testRunner->downloadComplete(); + } else if (_caller == _testRunnerDesktop) { + _testRunnerDesktop->downloadComplete(); + } else if (_caller == _testRunnerMobile) { + _testRunnerMobile->downloadComplete(); } + + _ui.progressBar->setVisible(false); } else { _ui.progressBar->setValue(_numberOfFilesDownloaded); } @@ -305,10 +343,35 @@ QString Nitpick::getSelectedBranch() { return _ui.branchLineEdit->text(); } -void Nitpick::updateStatusLabel(const QString& status) { - _ui.statusLabel->setText(status); -} - void Nitpick::appendLogWindow(const QString& message) { _ui.plainTextEdit->appendPlainText(message); } + +// Test on Mobile +void Nitpick::on_setWorkingFolderRunOnMobilePushbutton_clicked() { + _testRunnerMobile->setWorkingFolderAndEnableControls(); +} + +void Nitpick::on_connectDevicePushbutton_clicked() { + _testRunnerMobile->connectDevice(); +} + +void Nitpick::on_runLatestOnMobileCheckBox_clicked() { + _ui.urlOnMobileLineEdit->setEnabled(!_ui.runLatestOnMobileCheckBox->isChecked()); +} + +void Nitpick::on_downloadAPKPushbutton_clicked() { + _testRunnerMobile->downloadAPK(); +} + +void Nitpick::on_installAPKPushbutton_clicked() { + _testRunnerMobile->installAPK(); +} + +void Nitpick::on_runInterfacePushbutton_clicked() { + _testRunnerMobile->runInterface(); +} + +void Nitpick::on_pullFolderPushbutton_clicked() { + _testRunnerMobile->pullFolder(); +} diff --git a/tools/nitpick/src/ui/Nitpick.h b/tools/nitpick/src/Nitpick.h similarity index 59% rename from tools/nitpick/src/ui/Nitpick.h rename to tools/nitpick/src/Nitpick.h index 08e41e0a90..29726be3bd 100644 --- a/tools/nitpick/src/ui/Nitpick.h +++ b/tools/nitpick/src/Nitpick.h @@ -15,11 +15,13 @@ #include #include "ui_Nitpick.h" -#include "../Downloader.h" -#include "../Test.h" +#include "Downloader.h" +#include "Test.h" -#include "../TestRunner.h" -#include "../AWSInterface.h" +#include "TestRunnerDesktop.h" +#include "TestRunnerMobile.h" + +#include "AWSInterface.h" class Nitpick : public QMainWindow { Q_OBJECT @@ -49,54 +51,66 @@ public: void enableRunTabControls(); - void updateStatusLabel(const QString& status); void appendLogWindow(const QString& message); private slots: + void on_closePushbutton_clicked(); + void on_tabWidget_currentChanged(int index); - void on_evaluateTestsButton_clicked(); - void on_createRecursiveScriptButton_clicked(); - void on_createAllRecursiveScriptsButton_clicked(); - void on_createTestsButton_clicked(); + void on_evaluateTestsPushbutton_clicked(); + void on_createRecursiveScriptPushbutton_clicked(); + void on_createAllRecursiveScriptsPushbutton_clicked(); + void on_createTestsPushbutton_clicked(); - void on_createMDFileButton_clicked(); - void on_createAllMDFilesButton_clicked(); + void on_createMDFilePushbutton_clicked(); + void on_createAllMDFilesPushbutton_clicked(); - void on_createTestAutoScriptButton_clicked(); - void on_createAllTestAutoScriptsButton_clicked(); + void on_createTestAutoScriptPushbutton_clicked(); + void on_createAllTestAutoScriptsPushbutton_clicked(); - void on_createTestsOutlineButton_clicked(); + void on_createTestsOutlinePushbutton_clicked(); - void on_createTestRailTestCasesButton_clicked(); + void on_createTestRailTestCasesPushbutton_clicked(); void on_createTestRailRunButton_clicked(); - void on_setWorkingFolderButton_clicked(); - void on_runNowButton_clicked(); + void on_setWorkingFolderRunOnDesktopPushbutton_clicked(); + void on_runNowPushbutton_clicked(); - void on_checkBoxRunLatest_clicked(); + void on_runLatestOnDesktopCheckBox_clicked(); - void on_updateTestRailRunResultsButton_clicked(); + void on_updateTestRailRunResultsPushbutton_clicked(); - void on_hideTaskbarButton_clicked(); - void on_showTaskbarButton_clicked(); + void on_hideTaskbarPushbutton_clicked(); + void on_showTaskbarPushbutton_clicked(); void on_createPythonScriptRadioButton_clicked(); void on_createXMLScriptRadioButton_clicked(); - void on_createWebPagePushButton_clicked(); - - void on_closeButton_clicked(); + void on_createWebPagePushbutton_clicked(); void saveFile(int index); void about(); void content(); + // Run on Mobile controls + void on_setWorkingFolderRunOnMobilePushbutton_clicked(); + void on_connectDevicePushbutton_clicked(); + void on_runLatestOnMobileCheckBox_clicked(); + + void on_downloadAPKPushbutton_clicked(); + void on_installAPKPushbutton_clicked(); + void on_runInterfacePushbutton_clicked(); + + void on_pullFolderPushbutton_clicked(); + private: Ui::NitpickClass _ui; Test* _test{ nullptr }; - TestRunner* _testRunner{ nullptr }; + + TestRunnerDesktop* _testRunnerDesktop{ nullptr }; + TestRunnerMobile* _testRunnerMobile{ nullptr }; AWSInterface _awsInterface; diff --git a/tools/nitpick/src/PythonInterface.cpp b/tools/nitpick/src/PythonInterface.cpp index 9832ac9f8d..9e2fec005f 100644 --- a/tools/nitpick/src/PythonInterface.cpp +++ b/tools/nitpick/src/PythonInterface.cpp @@ -16,13 +16,13 @@ PythonInterface::PythonInterface() { #ifdef Q_OS_WIN if (QProcessEnvironment::systemEnvironment().contains("PYTHON_PATH")) { - QString _pythonPath = QProcessEnvironment::systemEnvironment().value("PYTHON_PATH"); - if (!QFile::exists(_pythonPath + "/" + _pythonExe)) { - QMessageBox::critical(0, _pythonExe, QString("Python executable not found in ") + _pythonPath); + QString pythonPath = QProcessEnvironment::systemEnvironment().value("PYTHON_PATH"); + if (!QFile::exists(pythonPath + "/" + _pythonExe)) { + QMessageBox::critical(0, _pythonExe, QString("Python executable not found in ") + pythonPath); exit(-1); } - _pythonCommand = _pythonPath + "/" + _pythonExe; + _pythonCommand = pythonPath + "/" + _pythonExe; } else { QMessageBox::critical(0, "PYTHON_PATH not defined", "Please set PYTHON_PATH to directory containing the Python executable"); @@ -31,7 +31,7 @@ PythonInterface::PythonInterface() { #elif defined Q_OS_MAC _pythonCommand = "/usr/local/bin/python3"; if (!QFile::exists(_pythonCommand)) { - QMessageBox::critical(0, "PYTHON_PATH not defined", + QMessageBox::critical(0, "python not found", "python3 not found at " + _pythonCommand); exit(-1); } diff --git a/tools/nitpick/src/Test.cpp b/tools/nitpick/src/Test.cpp index 19c49eac42..20d5ad89a6 100644 --- a/tools/nitpick/src/Test.cpp +++ b/tools/nitpick/src/Test.cpp @@ -19,7 +19,7 @@ #include #include -#include "ui/Nitpick.h" +#include "Nitpick.h" extern Nitpick* nitpick; #include @@ -758,47 +758,66 @@ void Test::createAllRecursiveScripts() { return; } + createAllRecursiveScripts(_testsRootDirectory); createRecursiveScript(_testsRootDirectory, false); - - QDirIterator it(_testsRootDirectory, QDirIterator::Subdirectories); - while (it.hasNext()) { - QString directory = it.next(); - - // Only process directories - QDir dir; - if (!isAValidDirectory(directory)) { - continue; - } - - // Only process directories that have sub-directories - bool hasNoSubDirectories{ true }; - QDirIterator it2(directory, QDirIterator::Subdirectories); - while (it2.hasNext()) { - QString directory2 = it2.next(); - - // Only process directories - QDir dir; - if (isAValidDirectory(directory2)) { - hasNoSubDirectories = false; - break; - } - } - - if (!hasNoSubDirectories) { - createRecursiveScript(directory, false); - } - } - QMessageBox::information(0, "Success", "Scripts have been created"); } -void Test::createRecursiveScript(const QString& topLevelDirectory, bool interactiveMode) { - const QString recursiveTestsScriptName("testRecursive.js"); - const QString recursiveTestsFilename(topLevelDirectory + "/" + recursiveTestsScriptName); +void Test::createAllRecursiveScripts(const QString& directory) { + QDirIterator it(directory, QDirIterator::Subdirectories); + + while (it.hasNext()) { + QString nextDirectory = it.next(); + if (isAValidDirectory(nextDirectory)) { + createAllRecursiveScripts(nextDirectory); + createRecursiveScript(nextDirectory, false); + } + } +} + +void Test::createRecursiveScript(const QString& directory, bool interactiveMode) { + // If folder contains a test, then we are at a leaf + const QString testPathname{ directory + "/" + TEST_FILENAME }; + if (QFileInfo(testPathname).exists()) { + return; + } + + // Directories are included in reverse order. The nitpick scripts use a stack mechanism, + // so this ensures that the tests run in alphabetical order (a convenience when debugging) + QStringList directories; + QDirIterator it(directory); + while (it.hasNext()) { + QString subDirectory = it.next(); + + // Only process directories + if (!isAValidDirectory(subDirectory)) { + continue; + } + + const QString testPathname{ subDirectory + "/" + TEST_FILENAME }; + if (QFileInfo(testPathname).exists()) { + // Current folder contains a test script + directories.push_front(testPathname); + } + + const QString testRecursivePathname{ subDirectory + "/" + TEST_RECURSIVE_FILENAME }; + if (QFileInfo(testRecursivePathname).exists()) { + // Current folder contains a recursive script + directories.push_front(testRecursivePathname); + } + } + + // If 'directories' is empty, this means that this recursive script has no tests to call, so it is redundant + if (directories.length() == 0) { + return; + } + + // Open the recursive script file + const QString recursiveTestsFilename(directory + "/" + TEST_RECURSIVE_FILENAME); QFile recursiveTestsFile(recursiveTestsFilename); if (!recursiveTestsFile.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Failed to create \"" + recursiveTestsScriptName + "\" in directory \"" + topLevelDirectory + "\""); + "Failed to create \"" + TEST_RECURSIVE_FILENAME + "\" in directory \"" + directory + "\""); exit(-1); } @@ -812,79 +831,26 @@ void Test::createRecursiveScript(const QString& topLevelDirectory, bool interact QString user = nitpick->getSelectedUser(); textStream << "PATH_TO_THE_REPO_PATH_UTILS_FILE = \"https://raw.githubusercontent.com/" + user + "/hifi_tests/" + branch + - "/tests/utils/branchUtils.js\";" - << endl; - textStream << "Script.include(PATH_TO_THE_REPO_PATH_UTILS_FILE);" << endl; - textStream << "var nitpick = createNitpick(Script.resolvePath(\".\"));" << endl << endl; + "/tests/utils/branchUtils.js\";" + << endl; + textStream << "Script.include(PATH_TO_THE_REPO_PATH_UTILS_FILE);" << endl << endl; - textStream << "var testsRootPath = nitpick.getTestsRootPath();" << endl << endl; - - // Wait 10 seconds before starting - textStream << "if (typeof Test !== 'undefined') {" << endl; - textStream << " Test.wait(10000);" << endl; - textStream << "};" << endl << endl; + textStream << "if (typeof nitpick === 'undefined') nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; + textStream << "if (typeof testsRootPath === 'undefined') testsRootPath = nitpick.getTestsRootPath();" << endl << endl; textStream << "nitpick.enableRecursive();" << endl; textStream << "nitpick.enableAuto();" << endl << endl; - // This is used to verify that the recursive test contains at least one test - bool testFound{ false }; - - // Directories are included in reverse order. The nitpick scripts use a stack mechanism, - // so this ensures that the tests run in alphabetical order (a convenience when debugging) - QStringList directories; - - // First test if top-level folder has a test.js file - const QString testPathname{ topLevelDirectory + "/" + TEST_FILENAME }; - QFileInfo fileInfo(testPathname); - if (fileInfo.exists()) { - // Current folder contains a test - directories.push_front(testPathname); - - testFound = true; - } - - QDirIterator it(topLevelDirectory, QDirIterator::Subdirectories); - while (it.hasNext()) { - QString directory = it.next(); - - // Only process directories - QDir dir(directory); - if (!isAValidDirectory(directory)) { - continue; - } - - const QString testPathname{ directory + "/" + TEST_FILENAME }; - QFileInfo fileInfo(testPathname); - if (fileInfo.exists()) { - // Current folder contains a test - directories.push_front(testPathname); - - testFound = true; - } - } - - if (interactiveMode && !testFound) { - QMessageBox::information(0, "Failure", "No \"" + TEST_FILENAME + "\" files found"); - recursiveTestsFile.close(); - return; - } - - // If 'directories' is empty, this means that this recursive script has no tests to call, so it is redundant - // The script will be closed and deleted - if (directories.length() == 0) { - recursiveTestsFile.close(); - QFile::remove(recursiveTestsFilename); - return; - } - // Now include the test scripts for (int i = 0; i < directories.length(); ++i) { includeTest(textStream, directories.at(i)); } textStream << endl; - textStream << "nitpick.runRecursive();" << endl; + textStream << "if (typeof runningRecursive === 'undefined') {" << endl; + textStream << " runningRecursive = true;" << endl; + textStream << " nitpick.runRecursive();" << endl; + textStream << "}" << endl << endl; recursiveTestsFile.close(); } @@ -928,7 +894,6 @@ void Test::createTestsOutline() { QString directory = it.next(); // Only process directories - QDir dir; if (!isAValidDirectory(directory)) { continue; } diff --git a/tools/nitpick/src/Test.h b/tools/nitpick/src/Test.h index 9ef7c5627a..842e4bdb48 100644 --- a/tools/nitpick/src/Test.h +++ b/tools/nitpick/src/Test.h @@ -18,7 +18,7 @@ #include "AWSInterface.h" #include "ImageComparer.h" -#include "ui/MismatchWindow.h" +#include "MismatchWindow.h" #include "TestRailInterface.h" class Step { @@ -72,9 +72,11 @@ public: void updateTestRailRunResult(); - void createRecursiveScript(); void createAllRecursiveScripts(); - void createRecursiveScript(const QString& topLevelDirectory, bool interactiveMode); + void createAllRecursiveScripts(const QString& directory); + + void createRecursiveScript(); + void createRecursiveScript(const QString& directory, bool interactiveMode); int compareImageLists(); int checkTextResults(); @@ -109,7 +111,8 @@ private: bool _isRunningFromCommandLine{ false }; bool _isRunningInAutomaticTestRun{ false }; - const QString TEST_FILENAME { "test.js" }; + const QString TEST_FILENAME{ "test.js" }; + const QString TEST_RECURSIVE_FILENAME{ "testRecursive.js" }; const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; diff --git a/tools/nitpick/src/TestRailInterface.h b/tools/nitpick/src/TestRailInterface.h index 6843ca0142..566600e71a 100644 --- a/tools/nitpick/src/TestRailInterface.h +++ b/tools/nitpick/src/TestRailInterface.h @@ -11,10 +11,10 @@ #ifndef hifi_test_testrail_interface_h #define hifi_test_testrail_interface_h -#include "ui/BusyWindow.h" -#include "ui/TestRailTestCasesSelectorWindow.h" -#include "ui/TestRailRunSelectorWindow.h" -#include "ui/TestRailResultsSelectorWindow.h" +#include "BusyWindow.h" +#include "TestRailTestCasesSelectorWindow.h" +#include "TestRailRunSelectorWindow.h" +#include "TestRailResultsSelectorWindow.h" #include #include diff --git a/tools/nitpick/src/ui/TestRailResultsSelectorWindow.cpp b/tools/nitpick/src/TestRailResultsSelectorWindow.cpp similarity index 100% rename from tools/nitpick/src/ui/TestRailResultsSelectorWindow.cpp rename to tools/nitpick/src/TestRailResultsSelectorWindow.cpp diff --git a/tools/nitpick/src/ui/TestRailResultsSelectorWindow.h b/tools/nitpick/src/TestRailResultsSelectorWindow.h similarity index 100% rename from tools/nitpick/src/ui/TestRailResultsSelectorWindow.h rename to tools/nitpick/src/TestRailResultsSelectorWindow.h diff --git a/tools/nitpick/src/ui/TestRailRunSelectorWindow.cpp b/tools/nitpick/src/TestRailRunSelectorWindow.cpp similarity index 100% rename from tools/nitpick/src/ui/TestRailRunSelectorWindow.cpp rename to tools/nitpick/src/TestRailRunSelectorWindow.cpp diff --git a/tools/nitpick/src/ui/TestRailRunSelectorWindow.h b/tools/nitpick/src/TestRailRunSelectorWindow.h similarity index 100% rename from tools/nitpick/src/ui/TestRailRunSelectorWindow.h rename to tools/nitpick/src/TestRailRunSelectorWindow.h diff --git a/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.cpp b/tools/nitpick/src/TestRailTestCasesSelectorWindow.cpp similarity index 100% rename from tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.cpp rename to tools/nitpick/src/TestRailTestCasesSelectorWindow.cpp diff --git a/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.h b/tools/nitpick/src/TestRailTestCasesSelectorWindow.h similarity index 100% rename from tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.h rename to tools/nitpick/src/TestRailTestCasesSelectorWindow.h diff --git a/tools/nitpick/src/TestRunner.cpp b/tools/nitpick/src/TestRunner.cpp index 9aca2bf3e6..54246de80b 100644 --- a/tools/nitpick/src/TestRunner.cpp +++ b/tools/nitpick/src/TestRunner.cpp @@ -1,7 +1,7 @@ // // TestRunner.cpp // -// Created by Nissim Hadar on 1 Sept 2018. +// Created by Nissim Hadar on 23 Jan 2019. // Copyright 2013 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -9,70 +9,12 @@ // #include "TestRunner.h" -#include -#include -#include +#include -#include "ui/Nitpick.h" +#include "Nitpick.h" extern Nitpick* nitpick; -#ifdef Q_OS_WIN -#include -#include -#endif - -// TODO: for debug -#include - -TestRunner::TestRunner(std::vector dayCheckboxes, - std::vector timeEditCheckboxes, - std::vector timeEdits, - QLabel* workingFolderLabel, - QCheckBox* runServerless, - QCheckBox* runLatest, - QLineEdit* url, - QPushButton* runNow, - QObject* parent) : - QObject(parent) { - _dayCheckboxes = dayCheckboxes; - _timeEditCheckboxes = timeEditCheckboxes; - _timeEdits = timeEdits; - _workingFolderLabel = workingFolderLabel; - _runServerless = runServerless; - _runLatest = runLatest; - _url = url; - _runNow = runNow; - - _installerThread = new QThread(); - _installerWorker = new Worker(); - - _installerWorker->moveToThread(_installerThread); - _installerThread->start(); - connect(this, SIGNAL(startInstaller()), _installerWorker, SLOT(runCommand())); - connect(_installerWorker, SIGNAL(commandComplete()), this, SLOT(installationComplete())); - - _interfaceThread = new QThread(); - _interfaceWorker = new Worker(); - - _interfaceThread->start(); - _interfaceWorker->moveToThread(_interfaceThread); - connect(this, SIGNAL(startInterface()), _interfaceWorker, SLOT(runCommand())); - connect(_interfaceWorker, SIGNAL(commandComplete()), this, SLOT(interfaceExecutionComplete())); -} - -TestRunner::~TestRunner() { - delete _installerThread; - delete _installerWorker; - - delete _interfaceThread; - delete _interfaceWorker; - - if (_timer) { - delete _timer; - } -} - -void TestRunner::setWorkingFolder() { +void TestRunner::setWorkingFolder(QLabel* workingFolderLabel) { // Everything will be written to this folder QString previousSelection = _workingFolder; QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); @@ -80,8 +22,8 @@ void TestRunner::setWorkingFolder() { parent += "/"; } - _workingFolder = QFileDialog::getExistingDirectory(nullptr, "Please select a temporary folder for installation", parent, - QFileDialog::ShowDirsOnly); + _workingFolder = QFileDialog::getExistingDirectory(nullptr, "Please select a working folder for temporary files", parent, + QFileDialog::ShowDirsOnly); // If user canceled then restore previous selection and return if (_workingFolder == "") { @@ -89,643 +31,25 @@ void TestRunner::setWorkingFolder() { return; } -#ifdef Q_OS_WIN - _installationFolder = _workingFolder + "/High Fidelity"; -#elif defined Q_OS_MAC - _installationFolder = _workingFolder + "/High_Fidelity"; -#endif + workingFolderLabel->setText(QDir::toNativeSeparators(_workingFolder)); + // This file is used for debug purposes. _logFile.setFileName(_workingFolder + "/log.txt"); - - nitpick->enableRunTabControls(); - _workingFolderLabel->setText(QDir::toNativeSeparators(_workingFolder)); - - _timer = new QTimer(this); - connect(_timer, SIGNAL(timeout()), this, SLOT(checkTime())); - _timer->start(30 * 1000); //time specified in ms - -#ifdef Q_OS_MAC - // Create MAC shell scripts - QFile script; - - // This script waits for a process to start - script.setFileName(_workingFolder + "/waitForStart.sh"); - if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open 'waitForStart.sh'"); - exit(-1); - } - - script.write("#!/bin/sh\n\n"); - script.write("PROCESS=\"$1\"\n"); - script.write("until (pgrep -x $PROCESS >nul)\n"); - script.write("do\n"); - script.write("\techo waiting for \"$1\" to start\n"); - script.write("\tsleep 2\n"); - script.write("done\n"); - script.write("echo \"$1\" \"started\"\n"); - script.close(); - script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); - - // The Mac shell command returns immediately. This little script waits for a process to finish - script.setFileName(_workingFolder + "/waitForFinish.sh"); - if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open 'waitForFinish.sh'"); - exit(-1); - } - - script.write("#!/bin/sh\n\n"); - script.write("PROCESS=\"$1\"\n"); - script.write("while (pgrep -x $PROCESS >nul)\n"); - script.write("do\n"); - script.write("\techo waiting for \"$1\" to finish\n"); - script.write("\tsleep 2\n"); - script.write("done\n"); - script.write("echo \"$1\" \"finished\"\n"); - script.close(); - script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); - - // Create an AppleScript to resize Interface. This is needed so that snapshots taken - // with the primary camera will be the correct size. - // This will be run from a normal shell script - script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.scpt"); - if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open 'setInterfaceSizeAndPosition.scpt'"); - exit(-1); - } - - script.write("set width to 960\n"); - script.write("set height to 540\n"); - script.write("set x to 100\n"); - script.write("set y to 100\n\n"); - script.write("tell application \"System Events\" to tell application process \"interface\" to tell window 1 to set {size, position} to {{width, height}, {x, y}}\n"); - - script.close(); - script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); - - script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.sh"); - if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open 'setInterfaceSizeAndPosition.sh'"); - exit(-1); - } - - script.write("#!/bin/sh\n\n"); - script.write("echo resizing interface\n"); - script.write(("osascript " + _workingFolder + "/setInterfaceSizeAndPosition.scpt\n").toStdString().c_str()); - script.write("echo resize complete\n"); - script.close(); - script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); -#endif } -void TestRunner::run() { - _runNow->setEnabled(false); - - _testStartDateTime = QDateTime::currentDateTime(); - _automatedTestIsRunning = true; - - // Initial setup - _branch = nitpick->getSelectedBranch(); - _user = nitpick->getSelectedUser(); - - // This will be restored at the end of the tests - saveExistingHighFidelityAppDataFolder(); - +void TestRunner::downloadBuildXml(void* caller) { // Download the latest High Fidelity build XML. // Note that this is not needed for PR builds (or whenever `Run Latest` is unchecked) // It is still downloaded, to simplify the flow + buildXMLDownloaded = false; + QStringList urls; QStringList filenames; urls << DEV_BUILD_XML_URL; filenames << DEV_BUILD_XML_FILENAME; - updateStatusLabel("Downloading Build XML"); - - buildXMLDownloaded = false; - nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); - - // `downloadComplete` will run after download has completed -} - -void TestRunner::downloadComplete() { - if (!buildXMLDownloaded) { - // Download of Build XML has completed - buildXMLDownloaded = true; - - // Download the High Fidelity installer - QStringList urls; - QStringList filenames; - if (_runLatest->isChecked()) { - parseBuildInformation(); - - _installerFilename = INSTALLER_FILENAME_LATEST; - - urls << _buildInformation.url; - filenames << _installerFilename; - } else { - QString urlText = _url->text(); - urls << urlText; - _installerFilename = getInstallerNameFromURL(urlText); - filenames << _installerFilename; - } - - updateStatusLabel("Downloading installer"); - - nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); - - // `downloadComplete` will run again after download has completed - - } else { - // Download of Installer has completed - appendLog(QString("Tests started at ") + QString::number(_testStartDateTime.time().hour()) + ":" + - QString("%1").arg(_testStartDateTime.time().minute(), 2, 10, QChar('0')) + ", on " + - _testStartDateTime.date().toString("ddd, MMM d, yyyy")); - - updateStatusLabel("Installing"); - - // Kill any existing processes that would interfere with installation - killProcesses(); - - runInstaller(); - } -} - -void TestRunner::runInstaller() { - // Qt cannot start an installation process using QProcess::start (Qt Bug 9761) - // To allow installation, the installer is run using the `system` command - - QStringList arguments{ QStringList() << QString("/S") << QString("/D=") + QDir::toNativeSeparators(_installationFolder) }; - - QString installerFullPath = _workingFolder + "/" + _installerFilename; - - QString commandLine; -#ifdef Q_OS_WIN - commandLine = "\"" + QDir::toNativeSeparators(installerFullPath) + "\"" + " /S /D=" + QDir::toNativeSeparators(_installationFolder); -#elif defined Q_OS_MAC - // Create installation shell script - QFile script; - script.setFileName(_workingFolder + "/install_app.sh"); - if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open 'install_app.sh'"); - exit(-1); - } - - if (!QDir().exists(_installationFolder)) { - QDir().mkdir(_installationFolder); - } - - // This script installs High Fidelity. It is run as "yes | install_app.sh... so "yes" is killed at the end - script.write("#!/bin/sh\n\n"); - script.write("VOLUME=`hdiutil attach \"$1\" | grep Volumes | awk '{print $3}'`\n"); - - QString folderName {"High Fidelity"}; - if (!_runLatest->isChecked()) { - folderName += QString(" - ") + getPRNumberFromURL(_url->text()); - } - - script.write((QString("cp -rf \"$VOLUME/") + folderName + "/interface.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str()); - script.write((QString("cp -rf \"$VOLUME/") + folderName + "/Sandbox.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str()); - - script.write("hdiutil detach \"$VOLUME\"\n"); - script.write("killall yes\n"); - script.close(); - script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); - commandLine = "yes | " + _workingFolder + "/install_app.sh " + installerFullPath; -#endif - appendLog(commandLine); - _installerWorker->setCommandLine(commandLine); - emit startInstaller(); -} - -void TestRunner::installationComplete() { - verifyInstallationSucceeded(); - - createSnapshotFolder(); - - updateStatusLabel("Running tests"); - - if (!_runServerless->isChecked()) { - startLocalServerProcesses(); - } - - runInterfaceWithTestScript(); -} - -void TestRunner::verifyInstallationSucceeded() { - // Exit if the executables are missing. - // On Windows, the reason is probably that UAC has blocked the installation. This is treated as a critical error -#ifdef Q_OS_WIN - QFileInfo interfaceExe(QDir::toNativeSeparators(_installationFolder) + "\\interface.exe"); - QFileInfo assignmentClientExe(QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe"); - QFileInfo domainServerExe(QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe"); - - if (!interfaceExe.exists() || !assignmentClientExe.exists() || !domainServerExe.exists()) { - QMessageBox::critical(0, "Installation of High Fidelity has failed", "Please verify that UAC has been disabled"); - exit(-1); - } -#endif -} - -void TestRunner::saveExistingHighFidelityAppDataFolder() { - QString dataDirectory{ "NOT FOUND" }; -#ifdef Q_OS_WIN - dataDirectory = qgetenv("USERPROFILE") + "\\AppData\\Roaming"; -#elif defined Q_OS_MAC - dataDirectory = QDir::homePath() + "/Library/Application Support"; -#endif - if (_runLatest->isChecked()) { - _appDataFolder = dataDirectory + "/High Fidelity"; - } else { - // We are running a PR build - _appDataFolder = dataDirectory + "/High Fidelity - " + getPRNumberFromURL(_url->text()); - } - - _savedAppDataFolder = dataDirectory + "/" + UNIQUE_FOLDER_NAME; - if (QDir(_savedAppDataFolder).exists()) { - _savedAppDataFolder.removeRecursively(); - } - if (_appDataFolder.exists()) { - // The original folder is saved in a unique name - _appDataFolder.rename(_appDataFolder.path(), _savedAppDataFolder.path()); - } - - // Copy an "empty" AppData folder (i.e. no entities) - copyFolder(QDir::currentPath() + "/AppDataHighFidelity", _appDataFolder.path()); -} - -void TestRunner::createSnapshotFolder() { - _snapshotFolder = _workingFolder + "/" + SNAPSHOT_FOLDER_NAME; - - // Just delete all PNGs from the folder if it already exists - if (QDir(_snapshotFolder).exists()) { - // Note that we cannot use just a `png` filter, as the filenames include periods - // Also, delete any `jpg` and `txt` files - // The idea is to leave only previous zipped result folders - QDirIterator it(_snapshotFolder); - while (it.hasNext()) { - QString filename = it.next(); - if (filename.right(4) == ".png" || filename.right(4) == ".jpg" || filename.right(4) == ".txt") { - QFile::remove(filename); - } - } - - } else { - QDir().mkdir(_snapshotFolder); - } -} - -void TestRunner::killProcesses() { -#ifdef Q_OS_WIN - try { - QStringList processesToKill = QStringList() << "interface.exe" - << "assignment-client.exe" - << "domain-server.exe" - << "server-console.exe"; - - // Loop until all pending processes to kill have actually died - QStringList pendingProcessesToKill; - do { - pendingProcessesToKill.clear(); - - // Get list of running tasks - HANDLE processSnapHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (processSnapHandle == INVALID_HANDLE_VALUE) { - throw("Process snapshot creation failure"); - } - - PROCESSENTRY32 processEntry32; - processEntry32.dwSize = sizeof(PROCESSENTRY32); - if (!Process32First(processSnapHandle, &processEntry32)) { - CloseHandle(processSnapHandle); - throw("Process32First failed"); - } - - // Kill any task in the list - do { - foreach (QString process, processesToKill) - if (QString(processEntry32.szExeFile) == process) { - QString commandLine = "taskkill /im " + process + " /f >nul"; - system(commandLine.toStdString().c_str()); - pendingProcessesToKill << process; - } - } while (Process32Next(processSnapHandle, &processEntry32)); - - QThread::sleep(2); - } while (!pendingProcessesToKill.isEmpty()); - - } catch (QString errorMessage) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); - exit(-1); - } catch (...) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); - exit(-1); - } -#elif defined Q_OS_MAC - QString commandLine; - - commandLine = QString("killall interface") + "; " + _workingFolder +"/waitForFinish.sh interface"; - system(commandLine.toStdString().c_str()); - - commandLine = QString("killall Sandbox") + "; " + _workingFolder +"/waitForFinish.sh Sandbox"; - system(commandLine.toStdString().c_str()); - - commandLine = QString("killall Console") + "; " + _workingFolder +"/waitForFinish.sh Console"; - system(commandLine.toStdString().c_str()); -#endif -} - -void TestRunner::startLocalServerProcesses() { - QString commandLine; - -#ifdef Q_OS_WIN - commandLine = - "start \"domain-server.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe\""; - system(commandLine.toStdString().c_str()); - - commandLine = - "start \"assignment-client.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe\" -n 6"; - system(commandLine.toStdString().c_str()); - -#elif defined Q_OS_MAC - commandLine = "open \"" +_installationFolder + "/Sandbox.app\""; - system(commandLine.toStdString().c_str()); -#endif - - // Give server processes time to stabilize - QThread::sleep(20); -} - -void TestRunner::runInterfaceWithTestScript() { - QString url = QString("hifi://localhost"); - if (_runServerless->isChecked()) { - // Move to an empty area - url = "file:///~serverless/tutorial.json"; - } else { - url = "hifi://localhost"; - } - - QString deleteScript = - QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/utils/deleteNearbyEntities.js"; - - QString testScript = - QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js"; - - QString commandLine; -#ifdef Q_OS_WIN - QString exeFile; - // First, run script to delete any entities in test area - // Note that this will run to completion before continuing - exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; - commandLine = "start /wait \"\" " + exeFile + - " --url " + url + - " --no-updater" + - " --no-login-suggestion" - " --testScript " + deleteScript + " quitWhenFinished"; - - system(commandLine.toStdString().c_str()); - - // Now run the test suite - exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; - commandLine = exeFile + - " --url " + url + - " --no-updater" + - " --no-login-suggestion" - " --testScript " + testScript + " quitWhenFinished" + - " --testResultsLocation " + _snapshotFolder; - - _interfaceWorker->setCommandLine(commandLine); - emit startInterface(); -#elif defined Q_OS_MAC - QFile script; - script.setFileName(_workingFolder + "/runInterfaceTests.sh"); - if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open 'runInterfaceTests.sh'"); - exit(-1); - } - - script.write("#!/bin/sh\n\n"); - - // First, run script to delete any entities in test area - commandLine = - "open -W \"" +_installationFolder + "/interface.app\" --args" + - " --url " + url + - " --no-updater" + - " --no-login-suggestion" - " --testScript " + deleteScript + " quitWhenFinished\n"; - - script.write(commandLine.toStdString().c_str()); - - // On The Mac, we need to resize Interface. The Interface window opens a few seconds after the process - // has started. - // Before starting interface, start a process that will resize interface 10s after it opens - commandLine = _workingFolder +"/waitForStart.sh interface && sleep 10 && " + _workingFolder +"/setInterfaceSizeAndPosition.sh &\n"; - script.write(commandLine.toStdString().c_str()); - - commandLine = - "open \"" +_installationFolder + "/interface.app\" --args" + - " --url " + url + - " --no-updater" + - " --no-login-suggestion" - " --testScript " + testScript + " quitWhenFinished" + - " --testResultsLocation " + _snapshotFolder + - " && " + _workingFolder +"/waitForFinish.sh interface\n"; - - script.write(commandLine.toStdString().c_str()); - - script.close(); - script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); - - commandLine = _workingFolder + "/runInterfaceTests.sh"; - _interfaceWorker->setCommandLine(commandLine); - - emit startInterface(); -#endif - - // Helpful for debugging - appendLog(commandLine); -} - -void TestRunner::interfaceExecutionComplete() { - QFileInfo testCompleted(QDir::toNativeSeparators(_snapshotFolder) +"/tests_completed.txt"); - if (!testCompleted.exists()) { - QMessageBox::critical(0, "Tests not completed", "Interface seems to have crashed before completion of the test scripts\nExisting images will be evaluated"); - } - - evaluateResults(); - - killProcesses(); - - // The High Fidelity AppData folder will be restored after evaluation has completed -} - -void TestRunner::evaluateResults() { - updateStatusLabel("Evaluating results"); - nitpick->startTestsEvaluation(false, true, _snapshotFolder, _branch, _user); -} - -void TestRunner::automaticTestRunEvaluationComplete(QString zippedFolder, int numberOfFailures) { - addBuildNumberToResults(zippedFolder); - restoreHighFidelityAppDataFolder(); - - updateStatusLabel("Testing complete"); - - QDateTime currentDateTime = QDateTime::currentDateTime(); - - QString completionText = QString("Tests completed at ") + QString::number(currentDateTime.time().hour()) + ":" + - QString("%1").arg(currentDateTime.time().minute(), 2, 10, QChar('0')) + ", on " + - currentDateTime.date().toString("ddd, MMM d, yyyy"); - - if (numberOfFailures == 0) { - completionText += "; no failures"; - } else if (numberOfFailures == 1) { - completionText += "; 1 failure"; - } else { - completionText += QString("; ") + QString::number(numberOfFailures) + " failures"; - } - appendLog(completionText); - - _automatedTestIsRunning = false; - - _runNow->setEnabled(true); -} - -void TestRunner::addBuildNumberToResults(QString zippedFolderName) { - QString augmentedFilename; - if (!_runLatest->isChecked()) { - augmentedFilename = zippedFolderName.replace("local", getPRNumberFromURL(_url->text())); - } else { - augmentedFilename = zippedFolderName.replace("local", _buildInformation.build); - } - QFile::rename(zippedFolderName, augmentedFilename); -} - -void TestRunner::restoreHighFidelityAppDataFolder() { - _appDataFolder.removeRecursively(); - - if (_savedAppDataFolder != QDir()) { - _appDataFolder.rename(_savedAppDataFolder.path(), _appDataFolder.path()); - } -} - -// Copies a folder recursively -void TestRunner::copyFolder(const QString& source, const QString& destination) { - try { - if (!QFileInfo(source).isDir()) { - // just a file copy - QFile::copy(source, destination); - } else { - QDir destinationDir(destination); - if (!destinationDir.cdUp()) { - throw("'source '" + source + "'seems to be a root folder"); - } - - if (!destinationDir.mkdir(QFileInfo(destination).fileName())) { - throw("Could not create destination folder '" + destination + "'"); - } - - QStringList fileNames = - QDir(source).entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System); - - foreach (const QString& fileName, fileNames) { - copyFolder(QString(source + "/" + fileName), QString(destination + "/" + fileName)); - } - } - } catch (QString errorMessage) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); - exit(-1); - } catch (...) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); - exit(-1); - } -} - -void TestRunner::checkTime() { - // No processing is done if a test is running - if (_automatedTestIsRunning) { - return; - } - - QDateTime now = QDateTime::currentDateTime(); - - // Check day of week - if (!_dayCheckboxes.at(now.date().dayOfWeek() - 1)->isChecked()) { - return; - } - - // Check the time - bool timeToRun{ false }; - - for (size_t i = 0; i < std::min(_timeEditCheckboxes.size(), _timeEdits.size()); ++i) { - if (_timeEditCheckboxes[i]->isChecked() && (_timeEdits[i]->time().hour() == now.time().hour()) && - (_timeEdits[i]->time().minute() == now.time().minute())) { - timeToRun = true; - break; - } - } - - if (timeToRun) { - run(); - } -} - -void TestRunner::updateStatusLabel(const QString& message) { - nitpick->updateStatusLabel(message); -} - -void TestRunner::appendLog(const QString& message) { - if (!_logFile.open(QIODevice::Append | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open the log file"); - exit(-1); - } - - _logFile.write(message.toStdString().c_str()); - _logFile.write("\n"); - _logFile.close(); - - nitpick->appendLogWindow(message); -} - -QString TestRunner::getInstallerNameFromURL(const QString& url) { - // An example URL: https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe - // On Mac, replace `exe` with `dmg` - try { - QStringList urlParts = url.split("/"); - return urlParts[urlParts.size() - 1]; - } catch (QString errorMessage) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); - exit(-1); - } catch (...) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); - exit(-1); - } -} - -QString TestRunner::getPRNumberFromURL(const QString& url) { - try { - QStringList urlParts = url.split("/"); - QStringList filenameParts = urlParts[urlParts.size() - 1].split("-"); - if (filenameParts.size() <= 3) { -#ifdef Q_OS_WIN - throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe`"; -#elif defined Q_OS_MAC - throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.dmg`"; -#endif - } - return filenameParts[filenameParts.size() - 2]; - } catch (QString errorMessage) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); - exit(-1); - } catch (...) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); - exit(-1); - } + nitpick->downloadFiles(urls, _workingFolder, filenames, caller); } void TestRunner::parseBuildInformation() { @@ -802,15 +126,48 @@ void TestRunner::parseBuildInformation() { } _buildInformation.url = element.text(); - } catch (QString errorMessage) { + } + catch (QString errorMessage) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); exit(-1); - } catch (...) { + } + catch (...) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); exit(-1); } } +QString TestRunner::getInstallerNameFromURL(const QString& url) { + // An example URL: https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe + // On Mac, replace `exe` with `dmg` + try { + QStringList urlParts = url.split("/"); + return urlParts[urlParts.size() - 1]; + } + catch (QString errorMessage) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); + exit(-1); + } + catch (...) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); + exit(-1); + } +} + +void TestRunner::appendLog(const QString& message) { + if (!_logFile.open(QIODevice::Append | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open the log file"); + exit(-1); + } + + _logFile.write(message.toStdString().c_str()); + _logFile.write("\n"); + _logFile.close(); + + nitpick->appendLogWindow(message); +} + void Worker::setCommandLine(const QString& commandLine) { _commandLine = commandLine; } diff --git a/tools/nitpick/src/TestRunner.h b/tools/nitpick/src/TestRunner.h index 00f0f66ecf..d2468ec2fa 100644 --- a/tools/nitpick/src/TestRunner.h +++ b/tools/nitpick/src/TestRunner.h @@ -1,7 +1,7 @@ // // TestRunner.h // -// Created by Nissim Hadar on 1 Sept 2018. +// Created by Nissim Hadar on 23 Jan 2019. // Copyright 2013 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -16,10 +16,9 @@ #include #include #include -#include -#include #include -#include + +class Worker; class BuildInformation { public: @@ -27,67 +26,28 @@ public: QString url; }; -class Worker; - -class TestRunner : public QObject { - Q_OBJECT +class TestRunner { public: - explicit TestRunner(std::vector dayCheckboxes, - std::vector timeEditCheckboxes, - std::vector timeEdits, - QLabel* workingFolderLabel, - QCheckBox* runServerless, - QCheckBox* runLatest, - QLineEdit* url, - QPushButton* runNow, - QObject* parent = 0); + void setWorkingFolder(QLabel* workingFolderLabel); + void downloadBuildXml(void* caller); + void parseBuildInformation(); + QString getInstallerNameFromURL(const QString& url); - ~TestRunner(); - - void setWorkingFolder(); - - void run(); - - void downloadComplete(); - void runInstaller(); - void verifyInstallationSucceeded(); - - void saveExistingHighFidelityAppDataFolder(); - void restoreHighFidelityAppDataFolder(); - - void createSnapshotFolder(); - - void killProcesses(); - void startLocalServerProcesses(); - - void runInterfaceWithTestScript(); - - void evaluateResults(); - void automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures); - void addBuildNumberToResults(QString zippedFolderName); - - void copyFolder(const QString& source, const QString& destination); - - void updateStatusLabel(const QString& message); void appendLog(const QString& message); - QString getInstallerNameFromURL(const QString& url); - QString getPRNumberFromURL(const QString& url); +protected: + QLabel* _workingFolderLabel; + QLabel* _statusLabel; + QLineEdit* _url; + QCheckBox* _runLatest; - void parseBuildInformation(); + QString _workingFolder; -private slots: - void checkTime(); - void installationComplete(); - void interfaceExecutionComplete(); + const QString DEV_BUILD_XML_URL{ "https://highfidelity.com/dev-builds.xml" }; + const QString DEV_BUILD_XML_FILENAME{ "dev-builds.xml" }; -signals: - void startInstaller(); - void startInterface(); - void startResize(); - -private: - bool _automatedTestIsRunning{ false }; + bool buildXMLDownloaded; + BuildInformation _buildInformation; #ifdef Q_OS_WIN const QString INSTALLER_FILENAME_LATEST{ "HighFidelity-Beta-latest-dev.exe" }; @@ -97,47 +57,10 @@ private: const QString INSTALLER_FILENAME_LATEST{ "" }; #endif - QString _installerURL; - QString _installerFilename; - const QString DEV_BUILD_XML_URL{ "https://highfidelity.com/dev-builds.xml" }; - const QString DEV_BUILD_XML_FILENAME{ "dev-builds.xml" }; - - bool buildXMLDownloaded; - - QDir _appDataFolder; - QDir _savedAppDataFolder; - - QString _workingFolder; - QString _installationFolder; - QString _snapshotFolder; - - const QString UNIQUE_FOLDER_NAME{ "fgadhcUDHSFaidsfh3478JJJFSDFIUSOEIrf" }; - const QString SNAPSHOT_FOLDER_NAME{ "snapshots" }; - - QString _branch; - QString _user; - - std::vector _dayCheckboxes; - std::vector _timeEditCheckboxes; - std::vector _timeEdits; - QLabel* _workingFolderLabel; - QCheckBox* _runServerless; - QCheckBox* _runLatest; - QLineEdit* _url; - QPushButton* _runNow; - QTimer* _timer; - - QFile _logFile; - QDateTime _testStartDateTime; - QThread* _installerThread; - QThread* _interfaceThread; - - Worker* _installerWorker; - Worker* _interfaceWorker; - - BuildInformation _buildInformation; +private: + QFile _logFile; }; class Worker : public QObject { @@ -150,10 +73,9 @@ public slots: signals: void commandComplete(); - void startInstaller(); - void startInterface(); - + private: QString _commandLine; }; -#endif // hifi_testRunner_h + +#endif diff --git a/tools/nitpick/src/TestRunnerDesktop.cpp b/tools/nitpick/src/TestRunnerDesktop.cpp new file mode 100644 index 0000000000..50cb6f9a07 --- /dev/null +++ b/tools/nitpick/src/TestRunnerDesktop.cpp @@ -0,0 +1,681 @@ +// +// TestRunnerDesktop.cpp +// +// Created by Nissim Hadar on 1 Sept 2018. +// 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 "TestRunnerDesktop.h" + +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#include +#endif + +#include "Nitpick.h" +extern Nitpick* nitpick; + +TestRunnerDesktop::TestRunnerDesktop( + std::vector dayCheckboxes, + std::vector timeEditCheckboxes, + std::vector timeEdits, + QLabel* workingFolderLabel, + QCheckBox* runServerless, + QCheckBox* runLatest, + QLineEdit* url, + QPushButton* runNow, + QLabel* statusLabel, + + QObject* parent +) : QObject(parent) +{ + _dayCheckboxes = dayCheckboxes; + _timeEditCheckboxes = timeEditCheckboxes; + _timeEdits = timeEdits; + _workingFolderLabel = workingFolderLabel; + _runServerless = runServerless; + _runLatest = runLatest; + _url = url; + _runNow = runNow; + _statusLabel = statusLabel; + + _installerThread = new QThread(); + _installerWorker = new InstallerWorker(); + + _installerWorker->moveToThread(_installerThread); + _installerThread->start(); + connect(this, SIGNAL(startInstaller()), _installerWorker, SLOT(runCommand())); + connect(_installerWorker, SIGNAL(commandComplete()), this, SLOT(installationComplete())); + + _interfaceThread = new QThread(); + _interfaceWorker = new InterfaceWorker(); + + _interfaceThread->start(); + _interfaceWorker->moveToThread(_interfaceThread); + connect(this, SIGNAL(startInterface()), _interfaceWorker, SLOT(runCommand())); + connect(_interfaceWorker, SIGNAL(commandComplete()), this, SLOT(interfaceExecutionComplete())); +} + +TestRunnerDesktop::~TestRunnerDesktop() { + delete _installerThread; + delete _installerWorker; + + delete _interfaceThread; + delete _interfaceWorker; + + if (_timer) { + delete _timer; + } +} + +void TestRunnerDesktop::setWorkingFolderAndEnableControls() { + setWorkingFolder(_workingFolderLabel); + +#ifdef Q_OS_WIN + _installationFolder = _workingFolder + "/High Fidelity"; +#elif defined Q_OS_MAC + _installationFolder = _workingFolder + "/High_Fidelity"; +#endif + + nitpick->enableRunTabControls(); + + _timer = new QTimer(this); + connect(_timer, SIGNAL(timeout()), this, SLOT(checkTime())); + _timer->start(30 * 1000); //time specified in ms + +#ifdef Q_OS_MAC + // Create MAC shell scripts + QFile script; + + // This script waits for a process to start + script.setFileName(_workingFolder + "/waitForStart.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'waitForStart.sh'"); + exit(-1); + } + + script.write("#!/bin/sh\n\n"); + script.write("PROCESS=\"$1\"\n"); + script.write("until (pgrep -x $PROCESS >nul)\n"); + script.write("do\n"); + script.write("\techo waiting for \"$1\" to start\n"); + script.write("\tsleep 2\n"); + script.write("done\n"); + script.write("echo \"$1\" \"started\"\n"); + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + + // The Mac shell command returns immediately. This little script waits for a process to finish + script.setFileName(_workingFolder + "/waitForFinish.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'waitForFinish.sh'"); + exit(-1); + } + + script.write("#!/bin/sh\n\n"); + script.write("PROCESS=\"$1\"\n"); + script.write("while (pgrep -x $PROCESS >nul)\n"); + script.write("do\n"); + script.write("\techo waiting for \"$1\" to finish\n"); + script.write("\tsleep 2\n"); + script.write("done\n"); + script.write("echo \"$1\" \"finished\"\n"); + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + + // Create an AppleScript to resize Interface. This is needed so that snapshots taken + // with the primary camera will be the correct size. + // This will be run from a normal shell script + script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.scpt"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'setInterfaceSizeAndPosition.scpt'"); + exit(-1); + } + + script.write("set width to 960\n"); + script.write("set height to 540\n"); + script.write("set x to 100\n"); + script.write("set y to 100\n\n"); + script.write("tell application \"System Events\" to tell application process \"interface\" to tell window 1 to set {size, position} to {{width, height}, {x, y}}\n"); + + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + + script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'setInterfaceSizeAndPosition.sh'"); + exit(-1); + } + + script.write("#!/bin/sh\n\n"); + script.write("echo resizing interface\n"); + script.write(("osascript " + _workingFolder + "/setInterfaceSizeAndPosition.scpt\n").toStdString().c_str()); + script.write("echo resize complete\n"); + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); +#endif +} + +void TestRunnerDesktop::run() { + _runNow->setEnabled(false); + + _testStartDateTime = QDateTime::currentDateTime(); + _automatedTestIsRunning = true; + + // Initial setup + _branch = nitpick->getSelectedBranch(); + _user = nitpick->getSelectedUser(); + + // This will be restored at the end of the tests + saveExistingHighFidelityAppDataFolder(); + + _statusLabel->setText("Downloading Build XML"); + downloadBuildXml((void*)this); + + // `downloadComplete` will run after download has completed +} + +void TestRunnerDesktop::downloadComplete() { + if (!buildXMLDownloaded) { + // Download of Build XML has completed + buildXMLDownloaded = true; + + // Download the High Fidelity installer + QStringList urls; + QStringList filenames; + if (_runLatest->isChecked()) { + parseBuildInformation(); + + _installerFilename = INSTALLER_FILENAME_LATEST; + + urls << _buildInformation.url; + filenames << _installerFilename; + } else { + QString urlText = _url->text(); + urls << urlText; + _installerFilename = getInstallerNameFromURL(urlText); + filenames << _installerFilename; + } + + _statusLabel->setText("Downloading installer"); + + nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); + + // `downloadComplete` will run again after download has completed + + } else { + // Download of Installer has completed + appendLog(QString("Tests started at ") + QString::number(_testStartDateTime.time().hour()) + ":" + + QString("%1").arg(_testStartDateTime.time().minute(), 2, 10, QChar('0')) + ", on " + + _testStartDateTime.date().toString("ddd, MMM d, yyyy")); + + _statusLabel->setText("Installing"); + + // Kill any existing processes that would interfere with installation + killProcesses(); + + runInstaller(); + } +} + +void TestRunnerDesktop::runInstaller() { + // Qt cannot start an installation process using QProcess::start (Qt Bug 9761) + // To allow installation, the installer is run using the `system` command + + QStringList arguments{ QStringList() << QString("/S") << QString("/D=") + QDir::toNativeSeparators(_installationFolder) }; + + QString installerFullPath = _workingFolder + "/" + _installerFilename; + + QString commandLine; +#ifdef Q_OS_WIN + commandLine = "\"" + QDir::toNativeSeparators(installerFullPath) + "\"" + " /S /D=" + QDir::toNativeSeparators(_installationFolder); +#elif defined Q_OS_MAC + // Create installation shell script + QFile script; + script.setFileName(_workingFolder + "/install_app.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'install_app.sh'"); + exit(-1); + } + + if (!QDir().exists(_installationFolder)) { + QDir().mkdir(_installationFolder); + } + + // This script installs High Fidelity. It is run as "yes | install_app.sh... so "yes" is killed at the end + script.write("#!/bin/sh\n\n"); + script.write("VOLUME=`hdiutil attach \"$1\" | grep Volumes | awk '{print $3}'`\n"); + + QString folderName {"High Fidelity"}; + if (!_runLatest->isChecked()) { + folderName += QString(" - ") + getPRNumberFromURL(_url->text()); + } + + script.write((QString("cp -rf \"$VOLUME/") + folderName + "/interface.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str()); + script.write((QString("cp -rf \"$VOLUME/") + folderName + "/Sandbox.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str()); + + script.write("hdiutil detach \"$VOLUME\"\n"); + script.write("killall yes\n"); + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + commandLine = "yes | " + _workingFolder + "/install_app.sh " + installerFullPath; +#endif + appendLog(commandLine); + _installerWorker->setCommandLine(commandLine); + emit startInstaller(); +} + +void TestRunnerDesktop::installationComplete() { + verifyInstallationSucceeded(); + + createSnapshotFolder(); + + _statusLabel->setText("Running tests"); + + if (!_runServerless->isChecked()) { + startLocalServerProcesses(); + } + + runInterfaceWithTestScript(); +} + +void TestRunnerDesktop::verifyInstallationSucceeded() { + // Exit if the executables are missing. + // On Windows, the reason is probably that UAC has blocked the installation. This is treated as a critical error +#ifdef Q_OS_WIN + QFileInfo interfaceExe(QDir::toNativeSeparators(_installationFolder) + "\\interface.exe"); + QFileInfo assignmentClientExe(QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe"); + QFileInfo domainServerExe(QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe"); + + if (!interfaceExe.exists() || !assignmentClientExe.exists() || !domainServerExe.exists()) { + QMessageBox::critical(0, "Installation of High Fidelity has failed", "Please verify that UAC has been disabled"); + exit(-1); + } +#endif +} + +void TestRunnerDesktop::saveExistingHighFidelityAppDataFolder() { + QString dataDirectory{ "NOT FOUND" }; +#ifdef Q_OS_WIN + dataDirectory = qgetenv("USERPROFILE") + "\\AppData\\Roaming"; +#elif defined Q_OS_MAC + dataDirectory = QDir::homePath() + "/Library/Application Support"; +#endif + if (_runLatest->isChecked()) { + _appDataFolder = dataDirectory + "/High Fidelity"; + } else { + // We are running a PR build + _appDataFolder = dataDirectory + "/High Fidelity - " + getPRNumberFromURL(_url->text()); + } + + _savedAppDataFolder = dataDirectory + "/" + UNIQUE_FOLDER_NAME; + if (QDir(_savedAppDataFolder).exists()) { + _savedAppDataFolder.removeRecursively(); + } + if (_appDataFolder.exists()) { + // The original folder is saved in a unique name + _appDataFolder.rename(_appDataFolder.path(), _savedAppDataFolder.path()); + } + + // Copy an "empty" AppData folder (i.e. no entities) + QDir canonicalAppDataFolder; +#ifdef Q_OS_WIN + canonicalAppDataFolder = QDir::currentPath() + "/AppDataHighFidelity"; +#elif defined Q_OS_MAC + canonicalAppDataFolder = QCoreApplication::applicationDirPath() + "/AppDataHighFidelity"; +#endif + if (canonicalAppDataFolder.exists()) { + copyFolder(canonicalAppDataFolder.path(), _appDataFolder.path()); + } else { + QMessageBox::critical(0, "Internal error", "The nitpick AppData folder cannot be found at:\n" + canonicalAppDataFolder.path()); + exit(-1); + } +} + +void TestRunnerDesktop::createSnapshotFolder() { + _snapshotFolder = _workingFolder + "/" + SNAPSHOT_FOLDER_NAME; + + // Just delete all PNGs from the folder if it already exists + if (QDir(_snapshotFolder).exists()) { + // Note that we cannot use just a `png` filter, as the filenames include periods + // Also, delete any `jpg` and `txt` files + // The idea is to leave only previous zipped result folders + QDirIterator it(_snapshotFolder); + while (it.hasNext()) { + QString filename = it.next(); + if (filename.right(4) == ".png" || filename.right(4) == ".jpg" || filename.right(4) == ".txt") { + QFile::remove(filename); + } + } + + } else { + QDir().mkdir(_snapshotFolder); + } +} + +void TestRunnerDesktop::killProcesses() { +#ifdef Q_OS_WIN + try { + QStringList processesToKill = QStringList() << "interface.exe" + << "assignment-client.exe" + << "domain-server.exe" + << "server-console.exe"; + + // Loop until all pending processes to kill have actually died + QStringList pendingProcessesToKill; + do { + pendingProcessesToKill.clear(); + + // Get list of running tasks + HANDLE processSnapHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (processSnapHandle == INVALID_HANDLE_VALUE) { + throw("Process snapshot creation failure"); + } + + PROCESSENTRY32 processEntry32; + processEntry32.dwSize = sizeof(PROCESSENTRY32); + if (!Process32First(processSnapHandle, &processEntry32)) { + CloseHandle(processSnapHandle); + throw("Process32First failed"); + } + + // Kill any task in the list + do { + foreach (QString process, processesToKill) + if (QString(processEntry32.szExeFile) == process) { + QString commandLine = "taskkill /im " + process + " /f >nul"; + system(commandLine.toStdString().c_str()); + pendingProcessesToKill << process; + } + } while (Process32Next(processSnapHandle, &processEntry32)); + + QThread::sleep(2); + } while (!pendingProcessesToKill.isEmpty()); + + } catch (QString errorMessage) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); + exit(-1); + } catch (...) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); + exit(-1); + } +#elif defined Q_OS_MAC + QString commandLine; + + commandLine = QString("killall interface") + "; " + _workingFolder +"/waitForFinish.sh interface"; + system(commandLine.toStdString().c_str()); + + commandLine = QString("killall Sandbox") + "; " + _workingFolder +"/waitForFinish.sh Sandbox"; + system(commandLine.toStdString().c_str()); + + commandLine = QString("killall Console") + "; " + _workingFolder +"/waitForFinish.sh Console"; + system(commandLine.toStdString().c_str()); +#endif +} + +void TestRunnerDesktop::startLocalServerProcesses() { + QString commandLine; + +#ifdef Q_OS_WIN + commandLine = + "start \"domain-server.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe\""; + system(commandLine.toStdString().c_str()); + + commandLine = + "start \"assignment-client.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe\" -n 6"; + system(commandLine.toStdString().c_str()); + +#elif defined Q_OS_MAC + commandLine = "open \"" +_installationFolder + "/Sandbox.app\""; + system(commandLine.toStdString().c_str()); +#endif + + // Give server processes time to stabilize + QThread::sleep(20); +} + +void TestRunnerDesktop::runInterfaceWithTestScript() { + QString url = QString("hifi://localhost"); + if (_runServerless->isChecked()) { + // Move to an empty area + url = "file:///~serverless/tutorial.json"; + } else { + url = "hifi://localhost"; + } + + QString deleteScript = + QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/utils/deleteNearbyEntities.js"; + + QString testScript = + QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js"; + + QString commandLine; +#ifdef Q_OS_WIN + QString exeFile; + // First, run script to delete any entities in test area + // Note that this will run to completion before continuing + exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; + commandLine = "start /wait \"\" " + exeFile + + " --url " + url + + " --no-updater" + + " --no-login-suggestion" + " --testScript " + deleteScript + " quitWhenFinished"; + + system(commandLine.toStdString().c_str()); + + // Now run the test suite + exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; + commandLine = exeFile + + " --url " + url + + " --no-updater" + + " --no-login-suggestion" + " --testScript " + testScript + " quitWhenFinished" + + " --testResultsLocation " + _snapshotFolder; + + _interfaceWorker->setCommandLine(commandLine); + emit startInterface(); +#elif defined Q_OS_MAC + QFile script; + script.setFileName(_workingFolder + "/runInterfaceTests.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'runInterfaceTests.sh'"); + exit(-1); + } + + script.write("#!/bin/sh\n\n"); + + // First, run script to delete any entities in test area + commandLine = + "open -W \"" +_installationFolder + "/interface.app\" --args" + + " --url " + url + + " --no-updater" + + " --no-login-suggestion" + " --testScript " + deleteScript + " quitWhenFinished\n"; + + script.write(commandLine.toStdString().c_str()); + + // On The Mac, we need to resize Interface. The Interface window opens a few seconds after the process + // has started. + // Before starting interface, start a process that will resize interface 10s after it opens + commandLine = _workingFolder +"/waitForStart.sh interface && sleep 10 && " + _workingFolder +"/setInterfaceSizeAndPosition.sh &\n"; + script.write(commandLine.toStdString().c_str()); + + commandLine = + "open \"" +_installationFolder + "/interface.app\" --args" + + " --url " + url + + " --no-updater" + + " --no-login-suggestion" + " --testScript " + testScript + " quitWhenFinished" + + " --testResultsLocation " + _snapshotFolder + + " && " + _workingFolder +"/waitForFinish.sh interface\n"; + + script.write(commandLine.toStdString().c_str()); + + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + + commandLine = _workingFolder + "/runInterfaceTests.sh"; + _interfaceWorker->setCommandLine(commandLine); + + emit startInterface(); +#endif + + // Helpful for debugging + appendLog(commandLine); +} + +void TestRunnerDesktop::interfaceExecutionComplete() { + QFileInfo testCompleted(QDir::toNativeSeparators(_snapshotFolder) +"/tests_completed.txt"); + if (!testCompleted.exists()) { + QMessageBox::critical(0, "Tests not completed", "Interface seems to have crashed before completion of the test scripts\nExisting images will be evaluated"); + } + + evaluateResults(); + + killProcesses(); + + // The High Fidelity AppData folder will be restored after evaluation has completed +} + +void TestRunnerDesktop::evaluateResults() { + _statusLabel->setText("Evaluating results"); + nitpick->startTestsEvaluation(false, true, _snapshotFolder, _branch, _user); +} + +void TestRunnerDesktop::automaticTestRunEvaluationComplete(QString zippedFolder, int numberOfFailures) { + addBuildNumberToResults(zippedFolder); + restoreHighFidelityAppDataFolder(); + + _statusLabel->setText("Testing complete"); + + QDateTime currentDateTime = QDateTime::currentDateTime(); + + QString completionText = QString("Tests completed at ") + QString::number(currentDateTime.time().hour()) + ":" + + QString("%1").arg(currentDateTime.time().minute(), 2, 10, QChar('0')) + ", on " + + currentDateTime.date().toString("ddd, MMM d, yyyy"); + + if (numberOfFailures == 0) { + completionText += "; no failures"; + } else if (numberOfFailures == 1) { + completionText += "; 1 failure"; + } else { + completionText += QString("; ") + QString::number(numberOfFailures) + " failures"; + } + appendLog(completionText); + + _automatedTestIsRunning = false; + + _runNow->setEnabled(true); +} + +void TestRunnerDesktop::addBuildNumberToResults(QString zippedFolderName) { + QString augmentedFilename; + if (!_runLatest->isChecked()) { + augmentedFilename = zippedFolderName.replace("local", getPRNumberFromURL(_url->text())); + } else { + augmentedFilename = zippedFolderName.replace("local", _buildInformation.build); + } + QFile::rename(zippedFolderName, augmentedFilename); +} + +void TestRunnerDesktop::restoreHighFidelityAppDataFolder() { + _appDataFolder.removeRecursively(); + + if (_savedAppDataFolder != QDir()) { + _appDataFolder.rename(_savedAppDataFolder.path(), _appDataFolder.path()); + } +} + +// Copies a folder recursively +void TestRunnerDesktop::copyFolder(const QString& source, const QString& destination) { + try { + if (!QFileInfo(source).isDir()) { + // just a file copy + QFile::copy(source, destination); + } else { + QDir destinationDir(destination); + if (!destinationDir.cdUp()) { + throw("'source '" + source + "'seems to be a root folder"); + } + + if (!destinationDir.mkdir(QFileInfo(destination).fileName())) { + throw("Could not create destination folder '" + destination + "'"); + } + + QStringList fileNames = + QDir(source).entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System); + + foreach (const QString& fileName, fileNames) { + copyFolder(QString(source + "/" + fileName), QString(destination + "/" + fileName)); + } + } + } catch (QString errorMessage) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); + exit(-1); + } catch (...) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); + exit(-1); + } +} + +void TestRunnerDesktop::checkTime() { + // No processing is done if a test is running + if (_automatedTestIsRunning) { + return; + } + + QDateTime now = QDateTime::currentDateTime(); + + // Check day of week + if (!_dayCheckboxes.at(now.date().dayOfWeek() - 1)->isChecked()) { + return; + } + + // Check the time + bool timeToRun{ false }; + + for (size_t i = 0; i < std::min(_timeEditCheckboxes.size(), _timeEdits.size()); ++i) { + if (_timeEditCheckboxes[i]->isChecked() && (_timeEdits[i]->time().hour() == now.time().hour()) && + (_timeEdits[i]->time().minute() == now.time().minute())) { + timeToRun = true; + break; + } + } + + if (timeToRun) { + run(); + } +} + +QString TestRunnerDesktop::getPRNumberFromURL(const QString& url) { + try { + QStringList urlParts = url.split("/"); + QStringList filenameParts = urlParts[urlParts.size() - 1].split("-"); + if (filenameParts.size() <= 3) { +#ifdef Q_OS_WIN + throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe`"; +#elif defined Q_OS_MAC + throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.dmg`"; +#endif + } + return filenameParts[filenameParts.size() - 2]; + } catch (QString errorMessage) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); + exit(-1); + } catch (...) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); + exit(-1); + } +} diff --git a/tools/nitpick/src/TestRunnerDesktop.h b/tools/nitpick/src/TestRunnerDesktop.h new file mode 100644 index 0000000000..a8f828b9d4 --- /dev/null +++ b/tools/nitpick/src/TestRunnerDesktop.h @@ -0,0 +1,124 @@ +// +// TestRunnerDesktop.h +// +// Created by Nissim Hadar on 1 Sept 2018. +// 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_testRunnerDesktop_h +#define hifi_testRunnerDesktop_h + +#include +#include +#include +#include +#include +#include + +#include "TestRunner.h" + +class InterfaceWorker; +class InstallerWorker; + +class TestRunnerDesktop : public QObject, public TestRunner { + Q_OBJECT +public: + explicit TestRunnerDesktop( + std::vector dayCheckboxes, + std::vector timeEditCheckboxes, + std::vector timeEdits, + QLabel* workingFolderLabel, + QCheckBox* runServerless, + QCheckBox* runLatest, + QLineEdit* url, + QPushButton* runNow, + QLabel* statusLabel, + + QObject* parent = 0 + ); + + ~TestRunnerDesktop(); + + void setWorkingFolderAndEnableControls(); + + void run(); + + void downloadComplete(); + void runInstaller(); + void verifyInstallationSucceeded(); + + void saveExistingHighFidelityAppDataFolder(); + void restoreHighFidelityAppDataFolder(); + + void createSnapshotFolder(); + + void killProcesses(); + void startLocalServerProcesses(); + + void runInterfaceWithTestScript(); + + void evaluateResults(); + void automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures); + void addBuildNumberToResults(QString zippedFolderName); + + void copyFolder(const QString& source, const QString& destination); + + QString getPRNumberFromURL(const QString& url); + +private slots: + void checkTime(); + void installationComplete(); + void interfaceExecutionComplete(); + +signals: + void startInstaller(); + void startInterface(); + void startResize(); + +private: + bool _automatedTestIsRunning{ false }; + + QString _installerURL; + QString _installerFilename; + + QDir _appDataFolder; + QDir _savedAppDataFolder; + + QString _installationFolder; + QString _snapshotFolder; + + const QString UNIQUE_FOLDER_NAME{ "fgadhcUDHSFaidsfh3478JJJFSDFIUSOEIrf" }; + const QString SNAPSHOT_FOLDER_NAME{ "snapshots" }; + + QString _branch; + QString _user; + + std::vector _dayCheckboxes; + std::vector _timeEditCheckboxes; + std::vector _timeEdits; + QLabel* _workingFolderLabel; + QCheckBox* _runServerless; + QPushButton* _runNow; + QTimer* _timer; + QThread* _installerThread; + QThread* _interfaceThread; + + InstallerWorker* _installerWorker; + InterfaceWorker* _interfaceWorker; +}; + +class InstallerWorker : public Worker { + Q_OBJECT +signals: + void startInstaller(); +}; + +class InterfaceWorker : public Worker { + Q_OBJECT +signals: + void startInterface(); +}; +#endif diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp new file mode 100644 index 0000000000..e1c82854f4 --- /dev/null +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -0,0 +1,196 @@ +// +// TestRunnerMobile.cpp +// +// Created by Nissim Hadar on 22 Jan 2019. +// 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 "TestRunnerMobile.h" + +#include +#include +#include + +#include "Nitpick.h" +extern Nitpick* nitpick; + +TestRunnerMobile::TestRunnerMobile( + QLabel* workingFolderLabel, + QPushButton *connectDeviceButton, + QPushButton *pullFolderButton, + QLabel* detectedDeviceLabel, + QLineEdit *folderLineEdit, + QPushButton* downloadAPKPushbutton, + QPushButton* installAPKPushbutton, + QPushButton* runInterfacePushbutton, + QCheckBox* runLatest, + QLineEdit* url, + QLabel* statusLabel, + + QObject* parent +) : QObject(parent) +{ + _workingFolderLabel = workingFolderLabel; + _connectDeviceButton = connectDeviceButton; + _pullFolderButton = pullFolderButton; + _detectedDeviceLabel = detectedDeviceLabel; + _folderLineEdit = folderLineEdit; + _downloadAPKPushbutton = downloadAPKPushbutton; + _installAPKPushbutton = installAPKPushbutton; + _runInterfacePushbutton = runInterfacePushbutton; + _runLatest = runLatest; + _url = url; + _statusLabel = statusLabel; + + folderLineEdit->setText("/sdcard/DCIM/TEST"); + + modelNames["SM_G955U1"] = "Samsung S8+ unlocked"; + + // Find ADB (Android Debugging Bridge) +#ifdef Q_OS_WIN + if (QProcessEnvironment::systemEnvironment().contains("ADB_PATH")) { + QString adbExePath = QProcessEnvironment::systemEnvironment().value("ADB_PATH") + "/platform-tools"; + if (!QFile::exists(adbExePath + "/" + _adbExe)) { + QMessageBox::critical(0, _adbExe, QString("ADB executable not found in ") + adbExePath); + exit(-1); + } + + _adbCommand = adbExePath + "/" + _adbExe; + } else { + QMessageBox::critical(0, "ADB_PATH not defined", + "Please set ADB_PATH to directory containing the `adb` executable"); + exit(-1); + } +#elif defined Q_OS_MAC + _adbCommand = "/usr/local/bin/adb"; + if (!QFile::exists(_adbCommand)) { + QMessageBox::critical(0, "adb not found", + "python3 not found at " + _adbCommand); + exit(-1); + } +#endif +} + +TestRunnerMobile::~TestRunnerMobile() { +} + +void TestRunnerMobile::setWorkingFolderAndEnableControls() { + setWorkingFolder(_workingFolderLabel); + + _connectDeviceButton->setEnabled(true); +} + +void TestRunnerMobile::connectDevice() { +#if defined Q_OS_WIN || defined Q_OS_MAC + QString devicesFullFilename{ _workingFolder + "/devices.txt" }; + QString command = _adbCommand + " devices -l > " + devicesFullFilename; + system(command.toStdString().c_str()); + + if (!QFile::exists(devicesFullFilename)) { + QMessageBox::critical(0, "Internal error", "devicesFullFilename not found"); + exit (-1); + } + + // Device should be in second line + QFile devicesFile(devicesFullFilename); + devicesFile.open(QIODevice::ReadOnly | QIODevice::Text); + QString line1 = devicesFile.readLine(); + QString line2 = devicesFile.readLine(); + + const QString DEVICE{ "device" }; + if (line2.contains(DEVICE)) { + // Make sure only 1 device + QString line3 = devicesFile.readLine(); + if (line3.contains(DEVICE)) { + QMessageBox::critical(0, "Too many devices detected", "Tests will run only if a single device is attached"); + + } else { + // Line looks like this: 988a1b47335239434b device product:dream2qlteue model:SM_G955U1 device:dream2qlteue transport_id:2 + QStringList tokens = line2.split(QRegExp("[\r\n\t ]+")); + QString deviceID = tokens[0]; + + QString modelID = tokens[3].split(':')[1]; + QString modelName = "UKNOWN"; + if (modelNames.count(modelID) == 1) { + modelName = modelNames[modelID]; + } + + _detectedDeviceLabel->setText(modelName + " [" + deviceID + "]"); + _pullFolderButton->setEnabled(true); + _folderLineEdit->setEnabled(true); + _downloadAPKPushbutton->setEnabled(true); + } + } +#endif +} + +void TestRunnerMobile::downloadAPK() { + downloadBuildXml((void*)this); +} + + +void TestRunnerMobile::downloadComplete() { + if (!buildXMLDownloaded) { + // Download of Build XML has completed + buildXMLDownloaded = true; + + // Download the High Fidelity installer + QStringList urls; + QStringList filenames; + if (_runLatest->isChecked()) { + parseBuildInformation(); + + _installerFilename = INSTALLER_FILENAME_LATEST; + + + // Replace the `exe` extension with `apk` + _installerFilename = _installerFilename.replace(_installerFilename.length() - 3, 3, "apk"); + _buildInformation.url = _buildInformation.url.replace(_buildInformation.url.length() - 3, 3, "apk"); + + urls << _buildInformation.url; + filenames << _installerFilename; + } else { + QString urlText = _url->text(); + urls << urlText; + _installerFilename = getInstallerNameFromURL(urlText); + filenames << _installerFilename; + } + + _statusLabel->setText("Downloading installer"); + + nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); + } else { + _statusLabel->setText("Installer download complete"); + _installAPKPushbutton->setEnabled(true); + } +} + +void TestRunnerMobile::installAPK() { +#if defined Q_OS_WIN || defined Q_OS_MAC + _statusLabel->setText("Installing"); + QString command = _adbCommand + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; + system(command.toStdString().c_str()); + _statusLabel->setText("Installation complete"); + _runInterfacePushbutton->setEnabled(true); +#endif +} + +void TestRunnerMobile::runInterface() { +#if defined Q_OS_WIN || defined Q_OS_MAC + _statusLabel->setText("Starting Interface"); + QString command = _adbCommand + " shell monkey -p io.highfidelity.hifiinterface -v 1"; + system(command.toStdString().c_str()); + _statusLabel->setText("Interface started"); +#endif +} + +void TestRunnerMobile::pullFolder() { +#if defined Q_OS_WIN || defined Q_OS_MAC + _statusLabel->setText("Pulling folder"); + QString command = _adbCommand + " pull " + _folderLineEdit->text() + " " + _workingFolder + _installerFilename; + system(command.toStdString().c_str()); + _statusLabel->setText("Pull complete"); +#endif +} diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h new file mode 100644 index 0000000000..247f864976 --- /dev/null +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -0,0 +1,74 @@ +// +// TestRunnerMobile.h +// +// Created by Nissim Hadar on 22 Jan 2019. +// 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_testRunnerMobile_h +#define hifi_testRunnerMobile_h + +#include +#include +#include +#include + +#include "TestRunner.h" + +class TestRunnerMobile : public QObject, public TestRunner { + Q_OBJECT +public: + explicit TestRunnerMobile( + QLabel* workingFolderLabel, + QPushButton *connectDeviceButton, + QPushButton *pullFolderButton, + QLabel* detectedDeviceLabel, + QLineEdit *folderLineEdit, + QPushButton* downloadAPKPushbutton, + QPushButton* installAPKPushbutton, + QPushButton* runInterfacePushbutton, + QCheckBox* runLatest, + QLineEdit* url, + QLabel* statusLabel, + + QObject* parent = 0 + ); + ~TestRunnerMobile(); + + void setWorkingFolderAndEnableControls(); + void connectDevice(); + + void downloadComplete(); + void downloadAPK(); + void runInterface(); + + void installAPK(); + + void pullFolder(); + +private: + QPushButton* _connectDeviceButton; + QPushButton* _pullFolderButton; + QLabel* _detectedDeviceLabel; + QLineEdit* _folderLineEdit; + QPushButton* _downloadAPKPushbutton; + QPushButton* _installAPKPushbutton; + QPushButton* _runInterfacePushbutton; + +#ifdef Q_OS_WIN + const QString _adbExe{ "adb.exe" }; +#else + // Both Mac and Linux use "adb" + const QString _adbExe{ "adb" }; +#endif + + QString _installerFilename; + + QString _adbCommand; + + std::map modelNames; +}; +#endif diff --git a/tools/nitpick/src/main.cpp b/tools/nitpick/src/main.cpp index 089a72e6ce..a2784a40b3 100644 --- a/tools/nitpick/src/main.cpp +++ b/tools/nitpick/src/main.cpp @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include -#include "ui/Nitpick.h" +#include "Nitpick.h" #include diff --git a/tools/nitpick/src/ui/BusyWindow.ui b/tools/nitpick/ui/BusyWindow.ui similarity index 100% rename from tools/nitpick/src/ui/BusyWindow.ui rename to tools/nitpick/ui/BusyWindow.ui diff --git a/tools/nitpick/src/ui/MismatchWindow.ui b/tools/nitpick/ui/MismatchWindow.ui similarity index 100% rename from tools/nitpick/src/ui/MismatchWindow.ui rename to tools/nitpick/ui/MismatchWindow.ui diff --git a/tools/nitpick/src/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui similarity index 72% rename from tools/nitpick/src/ui/Nitpick.ui rename to tools/nitpick/ui/Nitpick.ui index 78f7dcf2bf..16aaa9594d 100644 --- a/tools/nitpick/src/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -20,7 +20,7 @@ Nitpick - + 470 @@ -49,10 +49,10 @@ Create - + - 195 + 210 60 220 40 @@ -62,7 +62,7 @@ Create Tests - + 70 @@ -75,7 +75,7 @@ Create MD file - + 320 @@ -85,13 +85,13 @@ - Create all MD files + Create all MD files - + - 195 + 210 120 220 40 @@ -101,7 +101,7 @@ Create Tests Outline - + 70 @@ -114,7 +114,7 @@ Create Recursive Script - + 320 @@ -124,10 +124,10 @@ - Create all Recursive Scripts + Create all Recursive Scripts - + 70 @@ -140,7 +140,7 @@ Create testAuto script - + 320 @@ -158,7 +158,7 @@ Windows - + 200 @@ -171,7 +171,7 @@ Hide Windows Taskbar - + 200 @@ -187,9 +187,9 @@ - Run + Test on Desktop - + false @@ -420,26 +420,26 @@ - + 10 20 - 161 - 28 + 160 + 30 Set Working Folder - + 190 20 - 321 - 31 + 320 + 30 @@ -469,7 +469,7 @@ Status: - + 350 @@ -501,7 +501,7 @@ false - + 20 @@ -533,7 +533,7 @@ URL - + false @@ -547,6 +547,201 @@ + + + Test on Mobile + + + + false + + + + 10 + 90 + 160 + 30 + + + + Connect Device + + + + + + 190 + 96 + 320 + 30 + + + + (not detected) + + + + + + 10 + 20 + 160 + 30 + + + + Set Working Folder + + + + + + 190 + 20 + 320 + 30 + + + + (not set...) + + + + + false + + + + 460 + 410 + 160 + 30 + + + + Pull folder + + + + + false + + + + 10 + 410 + 440 + 30 + + + + + + false + + + + 170 + 170 + 451 + 21 + + + + + + + 20 + 170 + 120 + 20 + + + + <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> + + + Run Latest + + + true + + + + + false + + + + 10 + 210 + 160 + 30 + + + + Download APK + + + + + + 290 + 20 + 41 + 31 + + + + Status: + + + + + + 340 + 20 + 271 + 31 + + + + ####### + + + + + false + + + + 10 + 250 + 160 + 30 + + + + Install APK + + + + + false + + + + 10 + 300 + 160 + 30 + + + + Run Interface + + + Evaluate @@ -567,7 +762,7 @@ Interactive Mode - + 330 @@ -585,7 +780,7 @@ Web Interface - + 240 @@ -614,7 +809,7 @@ true - + 240 @@ -627,7 +822,7 @@ Create Run - + 240 @@ -678,7 +873,7 @@ Amazon Web Services - + true @@ -719,10 +914,10 @@ groupBox - updateTestRailRunResultsButton + updateTestRailRunResultsPushbutton createPythonScriptRadioButton - createTestRailRunButton - createTestRailTestCasesButton + createTestRailRunPushbutton + createTestRailTestCasesPushbutton createXMLScriptRadioButton groupBox_2 @@ -851,17 +1046,17 @@ userLineEdit branchLineEdit - createTestsButton - createMDFileButton - createAllMDFilesButton - createTestsOutlineButton - createRecursiveScriptButton - createAllRecursiveScriptsButton - createTestAutoScriptButton - createAllTestAutoScriptsButton - hideTaskbarButton - showTaskbarButton - runNowButton + createTestsPushbutton + createMDFilePushbutton + createAllMDFilesPushbutton + createTestsOutlinePushbutton + createRecursiveScriptPushbutton + createAllRecursiveScriptsPushbutton + createTestAutoScriptPushbutton + createAllTestAutoScriptsPushbutton + hideTaskbarPushbutton + showTaskbarPushbutton + runNowPushbutton sundayCheckBox wednesdayCheckBox tuesdayCheckBox @@ -877,22 +1072,22 @@ timeEdit2checkBox timeEdit3checkBox timeEdit4checkBox - setWorkingFolderButton + setWorkingFolderRunOnDesktopPushbutton plainTextEdit checkBoxServerless - checkBoxRunLatest - urlLineEdit + runLatestOnDesktopCheckBox + urlOnDesktopLineEdit checkBoxInteractiveMode - evaluateTestsButton - updateTestRailRunResultsButton + evaluateTestsPushbutton + updateTestRailRunResultsPushbutton createPythonScriptRadioButton - createTestRailRunButton - createTestRailTestCasesButton + createTestRailRunPushbutton + createTestRailTestCasesPushbutton createXMLScriptRadioButton - createWebPagePushButton + createWebPagePushbutton updateAWSCheckBox awsURLLineEdit - closeButton + closePushbutton tabWidget diff --git a/tools/nitpick/src/ui/TestRailResultsSelectorWindow.ui b/tools/nitpick/ui/TestRailResultsSelectorWindow.ui similarity index 100% rename from tools/nitpick/src/ui/TestRailResultsSelectorWindow.ui rename to tools/nitpick/ui/TestRailResultsSelectorWindow.ui diff --git a/tools/nitpick/src/ui/TestRailRunSelectorWindow.ui b/tools/nitpick/ui/TestRailRunSelectorWindow.ui similarity index 100% rename from tools/nitpick/src/ui/TestRailRunSelectorWindow.ui rename to tools/nitpick/ui/TestRailRunSelectorWindow.ui diff --git a/tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.ui b/tools/nitpick/ui/TestRailTestCasesSelectorWindow.ui similarity index 100% rename from tools/nitpick/src/ui/TestRailTestCasesSelectorWindow.ui rename to tools/nitpick/ui/TestRailTestCasesSelectorWindow.ui