diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index f18aa8c71b..c5048ea9d8 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -63,12 +63,16 @@ const QString DomainMetadata::Descriptors::Hours::CLOSE = "close"; DomainMetadata::DomainMetadata(QObject* domainServer) : QObject(domainServer) { // set up the structure necessary for casting during parsing (see parseHours, esp.) _metadata[USERS] = QVariantMap {}; - _metadata[DESCRIPTORS] = QVariantMap { - { Descriptors::HOURS, QVariantMap { - { Descriptors::Hours::WEEKDAY, QVariantList { QVariantMap{} } }, - { Descriptors::Hours::WEEKEND, QVariantList { QVariantMap{} } } - } } - }; + _metadata[DESCRIPTORS] = QVariantMap { { + Descriptors::HOURS, QVariantMap { + { Descriptors::Hours::WEEKDAY, QVariantList { + QVariantList{ QVariant{}, QVariant{} } } + }, + { Descriptors::Hours::WEEKEND, QVariantList { + QVariantList{ QVariant{}, QVariant{} } } + } + } + } }; assert(dynamic_cast(domainServer)); DomainServer* server = static_cast(domainServer); @@ -96,33 +100,37 @@ QJsonObject DomainMetadata::get(const QString& group) { return QJsonObject::fromVariantMap(_metadata[group].toMap()); } +// merge delta into target +// target should be of the form [ OpenTime, CloseTime ], +// delta should be of the form [ { open: Time, close: Time } ] void parseHours(QVariant delta, QVariant& target) { using Hours = DomainMetadata::Descriptors::Hours; - // hours should be of the form [ { open: Time, close: Time } ] assert(target.canConvert()); auto& targetList = *static_cast(target.data()); // if/when multiple ranges are allowed, this list will need to be iterated - assert(targetList[0].canConvert()); - auto& targetMap = *static_cast(targetList[0].data()); + assert(targetList[0].canConvert()); + auto& hours = *static_cast(targetList[0].data()); - auto deltaMap = delta.toList()[0].toMap(); - if (deltaMap.isEmpty()) { + auto deltaHours = delta.toList()[0].toMap(); + if (deltaHours.isEmpty()) { return; } // merge delta into base - auto open = deltaMap.find(Hours::OPEN); - if (open != deltaMap.end()) { - targetMap[Hours::OPEN] = open.value(); + static const int OPEN_INDEX = 0; + static const int CLOSE_INDEX = 1; + auto open = deltaHours.find(Hours::OPEN); + if (open != deltaHours.end()) { + hours[OPEN_INDEX] = open.value(); } - assert(targetMap[Hours::OPEN].canConvert()); - auto close = deltaMap.find(Hours::CLOSE); - if (close != deltaMap.end()) { - targetMap[Hours::CLOSE] = close.value(); + assert(hours[OPEN_INDEX].canConvert()); + auto close = deltaHours.find(Hours::CLOSE); + if (close != deltaHours.end()) { + hours[CLOSE_INDEX] = close.value(); } - assert(targetMap[Hours::CLOSE].canConvert()); + assert(hours[CLOSE_INDEX].canConvert()); } void DomainMetadata::descriptorsChanged() { diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index dc3ca3755e..6be672900a 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -1,18 +1,26 @@ { "name": "Vive to Standard", "channels": [ - { "from": "Vive.LY", "when": "Vive.LSOuter", "filters": ["invert"], "to": "Standard.LY" }, - { "from": "Vive.LX", "when": "Vive.LSOuter", "to": "Standard.LX" }, - - { "from": "Vive.LT", "to": "Standard.LT" }, + { "from": "Vive.LY", "when": "Vive.LSY", "filters": ["invert"], "to": "Standard.LY" }, + { "from": "Vive.LX", "when": "Vive.LSX", "to": "Standard.LX" }, + { + "from": "Vive.LT", "to": "Standard.LT", + "filters": [ + { "type": "deadZone", "min": 0.05 } + ] + }, { "from": "Vive.LeftGrip", "to": "Standard.LeftGrip" }, { "from": "Vive.LS", "to": "Standard.LS" }, { "from": "Vive.LSTouch", "to": "Standard.LSTouch" }, - { "from": "Vive.RY", "when": "Vive.RSOuter", "filters": ["invert"], "to": "Standard.RY" }, - { "from": "Vive.RX", "when": "Vive.RSOuter", "to": "Standard.RX" }, - - { "from": "Vive.RT", "to": "Standard.RT" }, + { "from": "Vive.RY", "when": "Vive.RSY", "filters": ["invert"], "to": "Standard.RY" }, + { "from": "Vive.RX", "when": "Vive.RSX", "to": "Standard.RX" }, + { + "from": "Vive.RT", "to": "Standard.RT", + "filters": [ + { "type": "deadZone", "min": 0.05 } + ] + }, { "from": "Vive.RightGrip", "to": "Standard.RightGrip" }, { "from": "Vive.RS", "to": "Standard.RS" }, { "from": "Vive.RSTouch", "to": "Standard.RSTouch" }, diff --git a/interface/resources/icons/hud-01.svg b/interface/resources/icons/hud-01.svg new file mode 100644 index 0000000000..4929389268 --- /dev/null +++ b/interface/resources/icons/hud-01.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 7f107e44e9..a48804faba 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -20,9 +20,10 @@ Window { objectName: "AddressBarDialog" frame: HiddenFrame {} + hideBackground: true - visible: false - destroyOnInvisible: false + shown: false + destroyOnHidden: false resizable: false scale: 1.25 // Make this dialog a little larger than normal @@ -145,14 +146,14 @@ Window { if (addressLine.text !== "") { addressBarDialog.loadAddress(addressLine.text) } - root.visible = false; + root.shown = false; } Keys.onPressed: { switch (event.key) { case Qt.Key_Escape: case Qt.Key_Back: - root.visible = false + root.shown = false event.accepted = true break case Qt.Key_Enter: diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index 6d2e8e7ba0..c9b6305258 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -15,15 +15,15 @@ import Qt.labs.settings 1.0 import "styles-uit" import "controls-uit" as HifiControls -import "windows-uit" +import "windows" import "dialogs" -Window { +ScrollingWindow { id: root objectName: "AssetServer" title: "Asset Browser" resizable: true - destroyOnInvisible: true + destroyOnHidden: true implicitWidth: 384; implicitHeight: 640 minSize: Qt.vector2d(200, 300) diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 89ab333a0d..8c8cf05444 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -2,22 +2,24 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 import QtWebEngine 1.1 -import "controls" -import "styles" +import "controls-uit" +import "styles-uit" import "windows" -Window { +ScrollingWindow { id: root HifiConstants { id: hifi } title: "Browser" resizable: true - destroyOnInvisible: true + destroyOnHidden: true width: 800 height: 600 property alias webView: webview - + x: 100 + y: 100 + Component.onCompleted: { - visible = true + shown = true addressBar.text = webview.url } @@ -30,15 +32,9 @@ Window { Item { id:item - anchors.fill: parent - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: webview.top - color: "white" - } - + width: pane.contentWidth + implicitHeight: pane.scrollHeight + Row { id: buttons spacing: 4 @@ -46,25 +42,37 @@ Window { anchors.topMargin: 8 anchors.left: parent.left anchors.leftMargin: 8 - FontAwesome { - id: back; text: "\uf0a8"; size: 48; enabled: webview.canGoBack; + HiFiGlyphs { + id: back; + enabled: webview.canGoBack; + text: hifi.glyphs.backward color: enabled ? hifi.colors.text : hifi.colors.disabledText + size: 48 MouseArea { anchors.fill: parent; onClicked: webview.goBack() } } - FontAwesome { - id: forward; text: "\uf0a9"; size: 48; enabled: webview.canGoForward; + + HiFiGlyphs { + id: forward; + enabled: webview.canGoForward; + text: hifi.glyphs.forward color: enabled ? hifi.colors.text : hifi.colors.disabledText - MouseArea { anchors.fill: parent; onClicked: webview.goBack() } + size: 48 + MouseArea { anchors.fill: parent; onClicked: webview.goForward() } } - FontAwesome { - id: reload; size: 48; text: webview.loading ? "\uf057" : "\uf021" - MouseArea { anchors.fill: parent; onClicked: webview.loading ? webview.stop() : webview.reload() } + + HiFiGlyphs { + id: reload; + enabled: webview.canGoForward; + text: webview.loading ? hifi.glyphs.close : hifi.glyphs.reload + color: enabled ? hifi.colors.text : hifi.colors.disabledText + size: 48 + MouseArea { anchors.fill: parent; onClicked: webview.goForward() } } } - Border { + Item { + id: border height: 48 - radius: 8 anchors.top: parent.top anchors.topMargin: 8 anchors.right: parent.right @@ -86,15 +94,18 @@ Window { onSourceChanged: console.log("Icon url: " + source) } } - - TextInput { + + TextField { id: addressBar anchors.right: parent.right anchors.rightMargin: 8 anchors.left: barIcon.right anchors.leftMargin: 0 anchors.verticalCenter: parent.verticalCenter - + focus: true + colorScheme: hifi.colorSchemes.dark + placeholderText: "Enter URL" + Component.onCompleted: scriptsModel.filterRegExp = new RegExp("^.*$", "i") Keys.onPressed: { switch(event.key) { case Qt.Key_Enter: @@ -110,7 +121,7 @@ Window { } } - WebView { + WebEngineView { id: webview url: "http://highfidelity.com" anchors.top: buttons.bottom @@ -119,7 +130,7 @@ Window { anchors.left: parent.left anchors.right: parent.right onLoadingChanged: { - if (loadRequest.status == WebEngineView.LoadSucceededStatus) { + if (loadRequest.status === WebEngineView.LoadSucceededStatus) { addressBar.text = loadRequest.url } } @@ -127,7 +138,7 @@ Window { console.log("New icon: " + icon) } - profile: desktop.browserProfile + //profile: desktop.browserProfile } diff --git a/interface/resources/qml/InfoView.qml b/interface/resources/qml/InfoView.qml index c5dba7e1f3..f18969fb2f 100644 --- a/interface/resources/qml/InfoView.qml +++ b/interface/resources/qml/InfoView.qml @@ -12,9 +12,9 @@ import QtQuick 2.5 import Hifi 1.0 as Hifi import "controls-uit" -import "windows-uit" as Windows +import "windows" as Windows -Windows.Window { +Windows.ScrollingWindow { id: root width: 800 height: 800 diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index 1b25b75608..f75e83e36e 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -14,7 +14,7 @@ import "controls" import "styles" import "windows" -Window { +ScrollingWindow { id: root HifiConstants { id: hifi } objectName: "LoginDialog" @@ -22,8 +22,9 @@ Window { width: loginDialog.implicitWidth // FIXME make movable anchors.centerIn: parent - destroyOnInvisible: false - visible: false + destroyOnHidden: false + hideBackground: true + shown: false LoginDialog { id: loginDialog @@ -268,8 +269,8 @@ Window { } } - onVisibleChanged: { - if (!visible) { + onShownChanged: { + if (!shown) { username.text = "" password.text = "" loginDialog.statusText = "" @@ -282,7 +283,7 @@ Window { switch (event.key) { case Qt.Key_Escape: case Qt.Key_Back: - root.visible = false; + root.shown = false; event.accepted = true; break; diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index ae052879db..542b44b95e 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -13,16 +13,16 @@ import QtQuick.Controls 1.4 import QtWebEngine 1.1 import QtWebChannel 1.0 -import "windows-uit" as Windows +import "windows" as Windows import "controls-uit" as Controls import "styles-uit" -Windows.Window { +Windows.ScrollingWindow { id: root HifiConstants { id: hifi } title: "WebWindow" resizable: true - visible: false + shown: false // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false property alias source: webview.url diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index 0420cd2e88..7be747a3ad 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -14,7 +14,7 @@ Windows.Window { HifiConstants { id: hifi } title: "QmlWindow" resizable: true - visible: false + shown: false focus: true property var channel; // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer diff --git a/interface/resources/qml/ToolWindow.qml b/interface/resources/qml/ToolWindow.qml index aaff43b146..bbfc74493d 100644 --- a/interface/resources/qml/ToolWindow.qml +++ b/interface/resources/qml/ToolWindow.qml @@ -15,18 +15,18 @@ import QtWebEngine 1.1 import QtWebChannel 1.0 import Qt.labs.settings 1.0 -import "windows-uit" +import "windows" import "controls-uit" import "styles-uit" -Window { +ScrollingWindow { id: toolWindow resizable: true objectName: "ToolWindow" destroyOnCloseButton: false - destroyOnInvisible: false + destroyOnHidden: false closable: true - visible: false + shown: false title: "Edit" property alias tabView: tabView implicitWidth: 520; implicitHeight: 695 @@ -142,7 +142,7 @@ Window { return; } } - visible = false; + shown = false; } function findIndexForUrl(source) { @@ -172,7 +172,7 @@ Window { var tab = tabView.getTab(index); if (newVisible) { - toolWindow.visible = true + toolWindow.shown = true tab.enabled = true } else { tab.enabled = false; diff --git a/interface/resources/qml/UpdateDialog.qml b/interface/resources/qml/UpdateDialog.qml index 4cb5b206c6..91dc210eda 100644 --- a/interface/resources/qml/UpdateDialog.qml +++ b/interface/resources/qml/UpdateDialog.qml @@ -3,11 +3,11 @@ import QtQuick 2.3 import QtQuick.Controls 1.3 import QtQuick.Controls.Styles 1.3 import QtGraphicalEffects 1.0 -import "controls" -import "styles" +import "controls-uit" +import "styles-uit" import "windows" -Window { +ScrollingWindow { id: root HifiConstants { id: hifi } objectName: "UpdateDialog" diff --git a/interface/resources/qml/controls-uit/AttachmentsTable.qml b/interface/resources/qml/controls-uit/AttachmentsTable.qml index ce93b8f4df..7d0280b72d 100644 --- a/interface/resources/qml/controls-uit/AttachmentsTable.qml +++ b/interface/resources/qml/controls-uit/AttachmentsTable.qml @@ -15,7 +15,7 @@ import QtQuick.XmlListModel 2.0 import "../styles-uit" import "../controls-uit" as HifiControls -import "../windows-uit" +import "../windows" import "../hifi/models" TableView { diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 62a72e3d8c..9f10cfc64a 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -52,6 +52,7 @@ FocusScope { readonly property real menu: 8000 } + QtObject { id: d @@ -64,7 +65,7 @@ FocusScope { var oldChildren = expectedChildren; var newChildren = d.getRepositionChildren(); - if (oldRecommendedRect != Qt.rect(0,0,0,0) + if (oldRecommendedRect != Qt.rect(0,0,0,0) && oldRecommendedRect != Qt.rect(0,0,1,1) && (oldRecommendedRect != newRecommendedRect || oldChildren != newChildren) ) { @@ -93,6 +94,17 @@ FocusScope { return item; } + function findMatchingChildren(item, predicate) { + var results = []; + for (var i in item.children) { + var child = item.children[i]; + if (predicate(child)) { + results.push(child); + } + } + return results; + } + function isTopLevelWindow(item) { return item.topLevelWindow; } @@ -106,19 +118,9 @@ FocusScope { } function getTopLevelWindows(predicate) { - var currentWindows = []; - if (!desktop) { - console.log("Could not find desktop for " + item) - return currentWindows; - } - - for (var i = 0; i < desktop.children.length; ++i) { - var child = desktop.children[i]; - if (isTopLevelWindow(child) && (!predicate || predicate(child))) { - currentWindows.push(child) - } - } - return currentWindows; + return findMatchingChildren(desktop, function(child) { + return (isTopLevelWindow(child) && (!predicate || predicate(child))); + }); } function getDesktopWindow(item) { @@ -227,19 +229,9 @@ FocusScope { } function getRepositionChildren(predicate) { - var currentWindows = []; - if (!desktop) { - console.log("Could not find desktop"); - return currentWindows; - } - - for (var i = 0; i < desktop.children.length; ++i) { - var child = desktop.children[i]; - if (child.shouldReposition === true && (!predicate || predicate(child))) { - currentWindows.push(child) - } - } - return currentWindows; + return findMatchingChildren(desktop, function(child) { + return (child.shouldReposition === true && (!predicate || predicate(child))); + }); } function repositionAll() { @@ -265,6 +257,63 @@ FocusScope { } } + property bool pinned: false + property var hiddenChildren: [] + + function togglePinned() { + pinned = !pinned + } + + function setPinned(newPinned) { + pinned = newPinned + } + + property real unpinnedAlpha: 1.0; + + Behavior on unpinnedAlpha { + NumberAnimation { + easing.type: Easing.Linear; + duration: 300 + } + } + + state: "NORMAL" + states: [ + State { + name: "NORMAL" + PropertyChanges { target: desktop; unpinnedAlpha: 1.0 } + }, + State { + name: "PINNED" + PropertyChanges { target: desktop; unpinnedAlpha: 0.0 } + } + ] + + transitions: [ + Transition { + NumberAnimation { properties: "unpinnedAlpha"; duration: 300 } + } + ] + + onPinnedChanged: { + if (pinned) { + nullFocus.focus = true; + nullFocus.forceActiveFocus(); + + // recalculate our non-pinned children + hiddenChildren = d.findMatchingChildren(desktop, function(child){ + return !d.isTopLevelWindow(child) && child.visible && !child.pinned; + }); + + hiddenChildren.forEach(function(child){ + child.opacity = Qt.binding(function(){ return desktop.unpinnedAlpha }); + }); + } + state = pinned ? "PINNED" : "NORMAL" + } + + onShowDesktop: pinned = false + function raise(item) { var targetWindow = d.getDesktopWindow(item); if (!targetWindow) { @@ -422,7 +471,6 @@ FocusScope { event.accepted = false; } - function unfocusWindows() { var windows = d.getTopLevelWindows(); for (var i = 0; i < windows.length; ++i) { @@ -433,6 +481,8 @@ FocusScope { FocusHack { id: focusHack; } + FocusScope { id: nullFocus; } + Rectangle { id: focusDebugger; objectName: "focusDebugger" diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index f57d20de51..5372028da5 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -18,7 +18,7 @@ import QtQuick.Dialogs 1.2 as OriginalDialogs import ".." import "../controls-uit" import "../styles-uit" -import "../windows-uit" +import "../windows" import "fileDialog" @@ -729,7 +729,7 @@ ModalWindow { Action { id: cancelAction text: "Cancel" - onTriggered: { canceled(); root.visible = false; } + onTriggered: { canceled(); root.shown = false; } } } diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index 30f492e36a..d390ea08bf 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -14,7 +14,7 @@ import QtQuick.Dialogs 1.2 as OriginalDialogs import "../controls-uit" import "../styles-uit" -import "../windows-uit" +import "../windows" import "messageDialog" @@ -24,7 +24,7 @@ ModalWindow { implicitWidth: 640 implicitHeight: 320 destroyOnCloseButton: true - destroyOnInvisible: true + destroyOnHidden: true visible: true signal selected(int button); diff --git a/interface/resources/qml/dialogs/PreferencesDialog.qml b/interface/resources/qml/dialogs/PreferencesDialog.qml index 40cc713397..5278118a22 100644 --- a/interface/resources/qml/dialogs/PreferencesDialog.qml +++ b/interface/resources/qml/dialogs/PreferencesDialog.qml @@ -13,14 +13,14 @@ import QtQuick.Controls 1.4 import "../controls-uit" as HifiControls import "../styles-uit" -import "../windows-uit" +import "../windows" import "preferences" -Window { +ScrollingWindow { id: root title: "Preferences" resizable: true - destroyOnInvisible: true + destroyOnHidden: true width: 500 height: 577 property var sections: [] diff --git a/interface/resources/qml/dialogs/QueryDialog.qml b/interface/resources/qml/dialogs/QueryDialog.qml index 0c7772dc94..05cb347169 100644 --- a/interface/resources/qml/dialogs/QueryDialog.qml +++ b/interface/resources/qml/dialogs/QueryDialog.qml @@ -14,7 +14,7 @@ import QtQuick.Dialogs 1.2 as OriginalDialogs import "../controls-uit" import "../styles-uit" -import "../windows-uit" +import "../windows" ModalWindow { id: root diff --git a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml index e5bc9b80ef..16d25b3c4c 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml @@ -12,7 +12,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtWebEngine 1.1 -import "../../windows-uit" as Windows +import "../../windows" as Windows import "../../controls-uit" as Controls import "../../styles-uit" @@ -23,15 +23,10 @@ Windows.Window { resizable: true modality: Qt.ApplicationModal - Item { - width: pane.contentWidth - implicitHeight: pane.scrollHeight - - Controls.WebView { - id: webview - anchors.fill: parent - url: "https://metaverse.highfidelity.com/marketplace?category=avatars" - focus: true - } + Controls.WebView { + id: webview + anchors.fill: parent + url: "https://metaverse.highfidelity.com/marketplace?category=avatars" + focus: true } } diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 59278a17b4..169542c0f0 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -1,14 +1,18 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtWebEngine 1.1; +import Qt.labs.settings 1.0 -import "../desktop" +import "../desktop" as OriginalDesktop import ".." +import "." +import "./toolbars" -Desktop { +OriginalDesktop.Desktop { id: desktop MouseArea { + id: hoverWatch anchors.fill: parent hoverEnabled: true propagateComposedEvents: true @@ -18,13 +22,6 @@ Desktop { acceptedButtons: Qt.NoButton } - Component.onCompleted: { - WebEngine.settings.javascriptCanOpenWindows = true; - WebEngine.settings.javascriptCanAccessClipboard = false; - WebEngine.settings.spatialNavigationEnabled = false; - WebEngine.settings.localContentCanAccessRemoteUrls = true; - } - // The tool window, one instance property alias toolWindow: toolWindow ToolWindow { id: toolWindow } @@ -47,7 +44,42 @@ Desktop { } } + property var toolbars: ({}) + Component { id: toolbarBuilder; Toolbar { } } + + Component.onCompleted: { + WebEngine.settings.javascriptCanOpenWindows = true; + WebEngine.settings.javascriptCanAccessClipboard = false; + WebEngine.settings.spatialNavigationEnabled = false; + WebEngine.settings.localContentCanAccessRemoteUrls = true; + + var sysToolbar = desktop.getToolbar("com.highfidelity.interface.toolbar.system"); + var toggleHudButton = sysToolbar.addButton({ + objectName: "hudToggle", + imageURL: "../../../icons/hud-01.svg", + visible: true, + pinned: true, + }); + + toggleHudButton.yOffset = Qt.binding(function(){ + return desktop.pinned ? 50 : 0 + }); + toggleHudButton.clicked.connect(function(){ + console.log("Clicked on hud button") + var overlayMenuItem = "Overlays" + MenuInterface.setIsOptionChecked(overlayMenuItem, !MenuInterface.isOptionChecked(overlayMenuItem)); + }); + } + + // Create or fetch a toolbar with the given name + function getToolbar(name) { + var result = toolbars[name]; + if (!result) { + result = toolbars[name] = toolbarBuilder.createObject(desktop, {}); + result.objectName = name; + } + return result; + } } - diff --git a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml index 437e02e149..15467f8021 100755 --- a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml +++ b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml @@ -6,17 +6,17 @@ import QtQuick.Controls.Styles 1.4 import "../../styles-uit" import "../../controls-uit" as HifiControls -import "../../windows-uit" +import "../../windows" import "attachments" -Window { +ScrollingWindow { id: root title: "Attachments" objectName: "AttachmentsDialog" width: 600 height: 600 resizable: true - destroyOnInvisible: true + destroyOnHidden: true minSize: Qt.vector2d(400, 500) HifiConstants { id: hifi } diff --git a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml index b2de108545..aeffb8e4bf 100644 --- a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml +++ b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml @@ -9,9 +9,9 @@ import "../models" import "../../styles-uit" import "../../controls-uit" as HifiControls -import "../../windows-uit" +import "../../windows" -Window { +ScrollingWindow { id: root resizable: true width: 600 diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index 94b8c1905f..5457caccf1 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -15,14 +15,14 @@ import Qt.labs.settings 1.0 import "../../styles-uit" import "../../controls-uit" as HifiControls -import "../../windows-uit" +import "../../windows" -Window { +ScrollingWindow { id: root objectName: "RunningScripts" title: "Running Scripts" resizable: true - destroyOnInvisible: true + destroyOnHidden: true implicitWidth: 424 implicitHeight: isHMD ? 695 : 728 minSize: Qt.vector2d(424, 300) @@ -34,6 +34,9 @@ Window { property var runningScriptsModel: ListModel { } property bool isHMD: false + onVisibleChanged: console.log("Running scripts visible changed to " + visible) + onShownChanged: console.log("Running scripts visible changed to " + visible) + Settings { category: "Overlay.RunningScripts" property alias x: root.x diff --git a/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml b/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml index f99b770a78..3dacb3b39c 100644 --- a/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml +++ b/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml @@ -7,7 +7,7 @@ import "../../windows" import "../../js/Utils.js" as Utils import "../models" -Window { +ScrollingWindow { id: root resizable: true width: 516 diff --git a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml index 1277c459ce..04e3934535 100755 --- a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml +++ b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml @@ -8,7 +8,7 @@ import "." import ".." import "../../../styles-uit" import "../../../controls-uit" as HifiControls -import "../../../windows-uit" +import "../../../windows" Item { height: column.height + 2 * 8 diff --git a/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml b/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml index e1d7b6d4a3..3d109cc2a5 100644 --- a/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml +++ b/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml @@ -3,7 +3,7 @@ import QtQuick.Controls 1.4 import "../../../styles-uit" import "../../../controls-uit" as HifiControls -import "../../../windows-uit" +import "../../../windows" Item { id: root diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml new file mode 100644 index 0000000000..75c06e4199 --- /dev/null +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -0,0 +1,151 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 + +import "../../windows" +import "." + +Window { + id: window + frame: ToolFrame { + horizontalSpacers: horizontal + verticalSpacers: !horizontal + } + hideBackground: true + resizable: false + destroyOnCloseButton: false + destroyOnHidden: false + closable: false + shown: true + width: content.width + height: content.height + visible: true + // Disable this window from being able to call 'desktop.raise() and desktop.showDesktop' + activator: Item {} + property bool horizontal: true + property real buttonSize: 50; + property var buttons: [] + property var container: horizontal ? row : column + + Settings { + category: "toolbar/" + window.objectName + property alias x: window.x + property alias y: window.y + } + + onHorizontalChanged: { + var newParent = horizontal ? row : column; + for (var i in buttons) { + var child = buttons[i]; + child.parent = newParent; + if (horizontal) { + child.y = 0 + } else { + child.x = 0 + } + } + } + + Item { + id: content + implicitHeight: horizontal ? row.height : column.height + implicitWidth: horizontal ? row.width : column.width + + Row { + id: row + spacing: 6 + } + + Column { + id: column + spacing: 6 + } + + Component { id: toolbarButtonBuilder; ToolbarButton { } } + + Connections { + target: desktop + onPinnedChanged: { + if (!window.pinned) { + return; + } + var newPinned = desktop.pinned; + for (var i in buttons) { + var child = buttons[i]; + if (desktop.pinned) { + if (!child.pinned) { + child.visible = false; + } + } else { + child.visible = true; + } + } + } + } + } + + + function findButtonIndex(name) { + if (!name) { + return -1; + } + + for (var i in buttons) { + var child = buttons[i]; + if (child.objectName === name) { + return i; + } + } + return -1; + } + + function findButton(name) { + var index = findButtonIndex(name); + if (index < 0) { + return; + } + return buttons[index]; + } + + function addButton(properties) { + properties = properties || {} + + // If a name is specified, then check if there's an existing button with that name + // and return it if so. This will allow multiple clients to listen to a single button, + // and allow scripts to be idempotent so they don't duplicate buttons if they're reloaded + var result = findButton(properties.objectName); + if (result) { + return result; + } + properties.toolbar = this; + properties.opacity = 0; + result = toolbarButtonBuilder.createObject(container, properties); + buttons.push(result); + result.opacity = 1; + updatePinned(); + return result; + } + + function removeButton(name) { + var index = findButtonIndex(name); + if (index < -1) { + console.warn("Tried to remove non-existent button " + name); + return; + } + buttons[index].destroy(); + buttons.splice(index, 1); + updatePinned(); + } + + function updatePinned() { + var newPinned = false; + for (var i in buttons) { + var child = buttons[i]; + if (child.pinned) { + newPinned = true; + break; + } + } + pinned = newPinned; + } +} diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml new file mode 100644 index 0000000000..a3be4533d2 --- /dev/null +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -0,0 +1,65 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Item { + id: button + property alias imageURL: image.source + property alias alpha: button.opacity + property var subImage; + property int yOffset: 0 + property int buttonState: 0 + property var toolbar; + property real size: 50 // toolbar ? toolbar.buttonSize : 50 + width: size; height: size + property bool pinned: false + clip: true + + Behavior on opacity { + NumberAnimation { + duration: 150 + easing.type: Easing.InOutCubic + } + } + + property alias fadeTargetProperty: button.opacity + + onFadeTargetPropertyChanged: { + visible = (fadeTargetProperty !== 0.0); + } + + onVisibleChanged: { + if ((!visible && fadeTargetProperty != 0.0) || (visible && fadeTargetProperty == 0.0)) { + var target = visible; + visible = !visible; + fadeTargetProperty = target ? 1.0 : 0.0; + return; + } + } + + + onButtonStateChanged: { + yOffset = size * buttonState + } + + Component.onCompleted: { + if (subImage) { + if (subImage.y) { + yOffset = subImage.y; + } + } + } + + signal clicked() + + Image { + id: image + y: -button.yOffset; + width: parent.width + } + + MouseArea { + anchors.fill: parent + onClicked: button.clicked(); + } +} + diff --git a/interface/resources/qml/windows-uit/DefaultFrame.qml b/interface/resources/qml/windows-uit/DefaultFrame.qml deleted file mode 100644 index 84f435480b..0000000000 --- a/interface/resources/qml/windows-uit/DefaultFrame.qml +++ /dev/null @@ -1,119 +0,0 @@ -// -// DefaultFrame.qml -// -// Created by Bradley Austin Davis on 12 Jan 2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtGraphicalEffects 1.0 - -import "." -import "../styles-uit" - -Frame { - HifiConstants { id: hifi } - - Rectangle { - // Dialog frame - id: frameContent - - readonly property int iconSize: hifi.dimensions.frameIconSize - readonly property int frameMargin: 9 - readonly property int frameMarginLeft: frameMargin - readonly property int frameMarginRight: frameMargin - readonly property int frameMarginTop: 2 * frameMargin + iconSize - readonly property int frameMarginBottom: iconSize + 11 - - anchors { - topMargin: -frameMarginTop - leftMargin: -frameMarginLeft - rightMargin: -frameMarginRight - bottomMargin: -frameMarginBottom - } - anchors.fill: parent - color: hifi.colors.baseGrayHighlight40 - border { - width: hifi.dimensions.borderWidth - color: hifi.colors.faintGray50 - } - radius: hifi.dimensions.borderRadius - - // Enable dragging of the window - MouseArea { - anchors.fill: parent - drag.target: window - } - - Row { - id: controlsRow - anchors { - right: parent.right; - top: parent.top; - topMargin: frameContent.frameMargin + 1 // Move down a little to visually align with the title - rightMargin: frameContent.frameMarginRight; - } - spacing: frameContent.iconSize / 4 - - HiFiGlyphs { - // "Pin" button - visible: false - text: (frame.pinned && !pinClickArea.containsMouse) || (!frame.pinned && pinClickArea.containsMouse) ? hifi.glyphs.pinInverted : hifi.glyphs.pin - color: pinClickArea.containsMouse && !pinClickArea.pressed ? hifi.colors.redHighlight : hifi.colors.white - size: frameContent.iconSize - MouseArea { - id: pinClickArea - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - onClicked: { frame.pin(); mouse.accepted = false; } - } - } - - HiFiGlyphs { - // "Close" button - visible: window ? window.closable : false - text: closeClickArea.containsPress ? hifi.glyphs.closeInverted : hifi.glyphs.close - color: closeClickArea.containsMouse ? hifi.colors.redHighlight : hifi.colors.white - size: frameContent.iconSize - MouseArea { - id: closeClickArea - anchors.fill: parent - hoverEnabled: true - onClicked: window.visible = false; - } - } - } - - RalewayRegular { - // Title - id: titleText - anchors { - left: parent.left - leftMargin: frameContent.frameMarginLeft + hifi.dimensions.contentMargin.x - right: controlsRow.left - rightMargin: frameContent.iconSize - top: parent.top - topMargin: frameContent.frameMargin - } - text: window ? window.title : "" - color: hifi.colors.white - size: hifi.fontSizes.overlayTitle - } - - DropShadow { - source: titleText - anchors.fill: titleText - horizontalOffset: 2 - verticalOffset: 2 - samples: 2 - color: hifi.colors.baseGrayShadow60 - visible: (window && window.focus) - cached: true - } - } -} - diff --git a/interface/resources/qml/windows-uit/Fadable.qml b/interface/resources/qml/windows-uit/Fadable.qml deleted file mode 100644 index 34990c2147..0000000000 --- a/interface/resources/qml/windows-uit/Fadable.qml +++ /dev/null @@ -1,60 +0,0 @@ -// -// Fadable.qml -// -// Created by Bradley Austin Davis on 15 Jan 2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtGraphicalEffects 1.0 - -import "../styles-uit" - -// Enable window visibility transitions -FocusScope { - id: root - HifiConstants { id: hifi } - - Component.onCompleted: { - fadeTargetProperty = visible ? 1.0 : 0.0 - } - - // The target property to animate, usually scale or opacity - property alias fadeTargetProperty: root.opacity - // always start the property at 0 to enable fade in on creation - fadeTargetProperty: 0 - // DO NOT set visible to false or when derived types override it it - // will short circuit the fade in on initial visibility - // visible: false <--- NO - - // Some dialogs should be destroyed when they become - // invisible, so handle that - onVisibleChanged: { - // If someone directly set the visibility to false - // toggle it back on and use the targetVisible flag to transition - // via fading. - if ((!visible && fadeTargetProperty != 0.0) || (visible && fadeTargetProperty == 0.0)) { - var target = visible; - visible = !visible; - fadeTargetProperty = target ? 1.0 : 0.0; - return; - } - } - - // The actual animator - Behavior on fadeTargetProperty { - NumberAnimation { - duration: hifi.effects.fadeInDuration - easing.type: Easing.InOutCubic - } - } - - // Once we're transparent, disable the dialog's visibility - onFadeTargetPropertyChanged: { - visible = (fadeTargetProperty != 0.0); - } -} diff --git a/interface/resources/qml/windows-uit/Frame.qml b/interface/resources/qml/windows-uit/Frame.qml deleted file mode 100644 index 9519a44cf0..0000000000 --- a/interface/resources/qml/windows-uit/Frame.qml +++ /dev/null @@ -1,133 +0,0 @@ -// -// Frame.qml -// -// Created by Bradley Austin Davis on 12 Jan 2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtGraphicalEffects 1.0 - -import "../styles-uit" -import "../js/Utils.js" as Utils - -Item { - id: frame - HifiConstants { id: hifi } - - default property var decoration - - property bool gradientsSupported: desktop.gradientsSupported - - readonly property int frameMarginLeft: frameContent.frameMarginLeft - readonly property int frameMarginRight: frameContent.frameMarginRight - readonly property int frameMarginTop: frameContent.frameMarginTop - readonly property int frameMarginBottom: frameContent.frameMarginBottom - - // Frames always fill their parents, but their decorations may extend - // beyond the window via negative margin sizes - anchors.fill: parent - - children: [ - focusShadow, - decoration, - sizeOutline, - debugZ, - sizeDrag - ] - - Text { - id: debugZ - visible: DebugQML - text: window ? "Z: " + window.z : "" - y: window ? window.height + 4 : 0 - } - - function deltaSize(dx, dy) { - var newSize = Qt.vector2d(window.width + dx, window.height + dy); - newSize = Utils.clampVector(newSize, window.minSize, window.maxSize); - window.width = newSize.x - window.height = newSize.y - } - - RadialGradient { - id: focusShadow - width: 1.66 * window.width - height: 1.66 * window.height - x: (window.width - width) / 2 - y: window.height / 2 - 0.375 * height - visible: gradientsSupported && window && window.focus && pane.visible - gradient: Gradient { - // GradientStop position 0.5 is at full circumference of circle that fits inside the square. - GradientStop { position: 0.0; color: "#ff000000" } // black, 100% opacity - GradientStop { position: 0.333; color: "#1f000000" } // black, 12% opacity - GradientStop { position: 0.5; color: "#00000000" } // black, 0% opacity - GradientStop { position: 1.0; color: "#00000000" } - } - cached: true - } - - Rectangle { - id: sizeOutline - x: -frameMarginLeft - y: -frameMarginTop - width: window ? window.width + frameMarginLeft + frameMarginRight + 2 : 0 - height: window ? window.height + frameMarginTop + frameMarginBottom + 2 : 0 - color: hifi.colors.baseGrayHighlight15 - border.width: 3 - border.color: hifi.colors.white50 - radius: hifi.dimensions.borderRadius - visible: window ? !pane.visible : false - } - - MouseArea { - // Resize handle - id: sizeDrag - width: hifi.dimensions.frameIconSize - height: hifi.dimensions.frameIconSize - enabled: window ? window.resizable : false - hoverEnabled: true - x: window ? window.width + frameMarginRight - hifi.dimensions.frameIconSize : 0 - y: window ? window.height + 4 : 0 - property vector2d pressOrigin - property vector2d sizeOrigin - property bool hid: false - onPressed: { - //console.log("Pressed on size") - pressOrigin = Qt.vector2d(mouseX, mouseY) - sizeOrigin = Qt.vector2d(window.content.width, window.content.height) - hid = false; - } - onReleased: { - if (hid) { - pane.visible = true - frameContent.visible = true - hid = false; - } - } - onPositionChanged: { - if (pressed) { - if (pane.visible) { - pane.visible = false; - frameContent.visible = false - hid = true; - } - var delta = Qt.vector2d(mouseX, mouseY).minus(pressOrigin); - frame.deltaSize(delta.x, delta.y) - } - } - HiFiGlyphs { - visible: sizeDrag.enabled - x: -11 // Move a little to visually align - y: window.modality == Qt.ApplicationModal ? -6 : -4 - text: hifi.glyphs.resizeHandle - size: hifi.dimensions.frameIconSize + 10 - color: sizeDrag.containsMouse || sizeDrag.pressed - ? hifi.colors.white - : (window.colorScheme == hifi.colorSchemes.dark ? hifi.colors.white50 : hifi.colors.lightGrayText80) - } - } -} diff --git a/interface/resources/qml/windows-uit/ModalFrame.qml b/interface/resources/qml/windows-uit/ModalFrame.qml deleted file mode 100644 index 211353b5f3..0000000000 --- a/interface/resources/qml/windows-uit/ModalFrame.qml +++ /dev/null @@ -1,98 +0,0 @@ -// -// ModalFrame.qml -// -// Created by Bradley Austin Davis on 15 Jan 2016 -// 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 "." -import "../controls-uit" -import "../styles-uit" - -Frame { - HifiConstants { id: hifi } - - Rectangle { - id: frameContent - - readonly property bool hasTitle: window.title != "" - - readonly property int frameMarginLeft: hifi.dimensions.modalDialogMargin.x - readonly property int frameMarginRight: hifi.dimensions.modalDialogMargin.x - readonly property int frameMarginTop: hifi.dimensions.modalDialogMargin.y + (frameContent.hasTitle ? hifi.dimensions.modalDialogTitleHeight + 10 : 0) - readonly property int frameMarginBottom: hifi.dimensions.modalDialogMargin.y - - signal frameClicked(); - - anchors { - fill: parent - topMargin: -frameMarginTop - leftMargin: -frameMarginLeft - rightMargin: -frameMarginRight - bottomMargin: -frameMarginBottom - } - - border { - width: hifi.dimensions.borderWidth - color: hifi.colors.lightGrayText80 - } - radius: hifi.dimensions.borderRadius - color: hifi.colors.faintGray - - // Enable dragging of the window - MouseArea { - anchors.fill: parent - drag.target: window - enabled: window.draggable - onClicked: window.frameClicked(); - } - - Item { - visible: frameContent.hasTitle - anchors.fill: parent - anchors { - topMargin: -parent.anchors.topMargin - leftMargin: -parent.anchors.leftMargin - rightMargin: -parent.anchors.rightMargin - } - - Item { - width: title.width + (icon.text !== "" ? icon.width + hifi.dimensions.contentSpacing.x : 0) - x: (parent.width - width) / 2 - - onWidthChanged: window.titleWidth = width - - HiFiGlyphs { - id: icon - text: window.iconText ? window.iconText : "" - size: window.iconSize ? window.iconSize : 30 - color: hifi.colors.lightGray - visible: text != "" - anchors.verticalCenter: title.verticalCenter - anchors.left: parent.left - } - RalewayRegular { - id: title - text: window.title - elide: Text.ElideRight - color: hifi.colors.baseGrayHighlight - size: hifi.fontSizes.overlayTitle - y: -hifi.dimensions.modalDialogTitleHeight - anchors.right: parent.right - } - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - height: 1 - color: hifi.colors.lightGray - } - } - } -} diff --git a/interface/resources/qml/windows-uit/ModalWindow.qml b/interface/resources/qml/windows-uit/ModalWindow.qml deleted file mode 100644 index 144165e4e1..0000000000 --- a/interface/resources/qml/windows-uit/ModalWindow.qml +++ /dev/null @@ -1,28 +0,0 @@ -// -// ModalWindow.qml -// -// Created by Bradley Austin Davis on 22 Jan 2016 -// 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 "." - -Window { - id: window - modality: Qt.ApplicationModal - destroyOnCloseButton: true - destroyOnInvisible: true - frame: ModalFrame { } - - property int colorScheme: hifi.colorSchemes.light - property bool draggable: false - - signal frameClicked(); - - anchors.centerIn: draggable ? undefined : parent -} diff --git a/interface/resources/qml/windows-uit/Window.qml b/interface/resources/qml/windows-uit/Window.qml deleted file mode 100644 index d614b21ce2..0000000000 --- a/interface/resources/qml/windows-uit/Window.qml +++ /dev/null @@ -1,343 +0,0 @@ -// -// Window.qml -// -// Created by Bradley Austin Davis on 12 Jan 2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import QtGraphicalEffects 1.0 - -import "." -import "../styles-uit" - -// FIXME how do I set the initial position of a window without -// overriding places where the a individual client of the window -// might be setting the position with a Settings{} element? - -// FIXME how to I enable dragging without allowing the window to lay outside -// of the desktop? How do I ensure when the desktop resizes all the windows -// are still at least partially visible? -Fadable { - id: window - HifiConstants { id: hifi } - - // The Window size is the size of the content, while the frame - // decorations can extend outside it. - implicitHeight: content ? content.height : 0 - implicitWidth: content ? content.width : 0 - x: desktop.invalid_position; y: desktop.invalid_position; - enabled: visible - - signal windowDestroyed(); - - property int modality: Qt.NonModal - readonly property bool topLevelWindow: true - property string title - // Should the window be closable control? - property bool closable: true - // Should the window try to remain on top of other windows? - property bool alwaysOnTop: false - // Should hitting the close button hide or destroy the window? - property bool destroyOnCloseButton: true - // Should hiding the window destroy it or just hide it? - property bool destroyOnInvisible: false - // FIXME support for pinned / unpinned pending full design - // property bool pinnable: false - // property bool pinned: false - property bool resizable: false - property bool gradientsSupported: desktop.gradientsSupported - property int colorScheme: hifi.colorSchemes.dark - - property vector2d minSize: Qt.vector2d(100, 100) - property vector2d maxSize: Qt.vector2d(1280, 800) - - // The content to place inside the window, determined by the client - default property var content - - property var footer: Item { } // Optional static footer at the bottom of the dialog. - - function setDefaultFocus() {} // Default function; can be overridden by dialogs. - - property var rectifier: Timer { - property bool executing: false; - interval: 100 - repeat: false - running: false - - onTriggered: { - executing = true; - x = Math.floor(x); - y = Math.floor(y); - executing = false; - } - - function begin() { - if (!executing) { - restart(); - } - } - } - onXChanged: rectifier.begin(); - onYChanged: rectifier.begin(); - - // This mouse area serves to raise the window. To function, it must live - // in the window and have a higher Z-order than the content, but follow - // the position and size of frame decoration - property var activator: MouseArea { - width: frame.decoration.width - height: frame.decoration.height - x: frame.decoration.anchors.leftMargin - y: frame.decoration.anchors.topMargin - propagateComposedEvents: true - acceptedButtons: Qt.AllButtons - enabled: window.visible - onPressed: { - //console.log("Pressed on activator area"); - window.raise(); - mouse.accepted = false; - } - } - - // This mouse area serves to swallow mouse events while the mouse is over the window - // to prevent things like mouse wheel events from reaching the application and changing - // the camera if the user is scrolling through a list and gets to the end. - property var swallower: MouseArea { - width: frame.decoration.width - height: frame.decoration.height - x: frame.decoration.anchors.leftMargin - y: frame.decoration.anchors.topMargin - hoverEnabled: true - acceptedButtons: Qt.AllButtons - enabled: window.visible - onClicked: {} - onDoubleClicked: {} - onPressAndHold: {} - onReleased: {} - onWheel: {} - } - - // Default to a standard frame. Can be overriden to provide custom - // frame styles, like a full desktop frame to simulate a modal window - property var frame: DefaultFrame { } - - // Scrollable window content. - property var pane: Item { - property bool isScrolling: scrollView.height < scrollView.contentItem.height - property int contentWidth: scrollView.width - (isScrolling ? 10 : 0) - property int scrollHeight: scrollView.height - - anchors.fill: parent - anchors.rightMargin: isScrolling ? 11 : 0 - - Rectangle { - id: contentBackground - anchors.fill: parent - anchors.rightMargin: parent.isScrolling ? 11 : 0 - color: hifi.colors.baseGray - visible: modality != Qt.ApplicationModal - } - - LinearGradient { - visible: gradientsSupported && modality != Qt.ApplicationModal - anchors.top: contentBackground.bottom - anchors.left: contentBackground.left - width: contentBackground.width - 1 - height: 4 - start: Qt.point(0, 0) - end: Qt.point(0, 4) - gradient: Gradient { - GradientStop { position: 0.0; color: hifi.colors.darkGray } - GradientStop { position: 1.0; color: hifi.colors.darkGray0 } - } - cached: true - } - - ScrollView { - id: scrollView - contentItem: content - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - verticalScrollBarPolicy: Qt.ScrollBarAsNeeded - anchors.fill: parent - anchors.rightMargin: parent.isScrolling ? 1 : 0 - anchors.bottomMargin: footer.height > 0 ? footerPane.height : 0 - - style: ScrollViewStyle { - - padding.right: -7 // Move to right away from content. - - handle: Item { - implicitWidth: 8 - Rectangle { - radius: 4 - color: hifi.colors.white30 - anchors { - fill: parent - leftMargin: 2 // Finesse size and position. - topMargin: 1 - bottomMargin: 1 - } - } - } - - scrollBarBackground: Item { - implicitWidth: 10 - Rectangle { - color: hifi.colors.darkGray30 - radius: 4 - anchors { - fill: parent - topMargin: -1 // Finesse size - bottomMargin: -2 - } - } - } - - incrementControl: Item { - visible: false - } - - decrementControl: Item { - visible: false - } - } - } - - Rectangle { - // Optional non-scrolling footer. - id: footerPane - anchors { - left: parent.left - bottom: parent.bottom - } - width: parent.contentWidth - height: footer.height + 2 * hifi.dimensions.contentSpacing.y + 3 - color: hifi.colors.baseGray - visible: footer.height > 0 - - Item { - // Horizontal rule. - anchors.fill: parent - - Rectangle { - width: parent.width - height: 1 - y: 1 // Stop displaying content just above horizontal rule/=. - color: hifi.colors.baseGrayShadow - } - - Rectangle { - width: parent.width - height: 1 - y: 2 - color: hifi.colors.baseGrayHighlight - } - } - - Item { - anchors.fill: parent - anchors.topMargin: 3 // Horizontal rule. - children: [ footer ] - } - } - } - - children: [ swallower, frame, pane, activator ] - - Component.onCompleted: { - window.parentChanged.connect(raise); - raise(); - setDefaultFocus(); - centerOrReposition(); - } - Component.onDestruction: { - window.parentChanged.disconnect(raise); // Prevent warning on shutdown - windowDestroyed(); - } - - onVisibleChanged: { - if (!visible && destroyOnInvisible) { - destroy(); - return; - } - if (visible) { - raise(); - } - enabled = visible - - if (visible && parent) { - centerOrReposition(); - } - } - - function centerOrReposition() { - if (x == desktop.invalid_position && y == desktop.invalid_position) { - desktop.centerOnVisible(window); - } else { - desktop.repositionOnVisible(window); - } - } - - function raise() { - if (visible && parent) { - desktop.raise(window) - } - } - - function pin() { -// pinned = ! pinned - } - - // our close function performs the same way as the OffscreenUI class: - // don't do anything but manipulate the targetVisible flag and let the other - // mechanisms decide if the window should be destroyed after the close - // animation completes - // FIXME using this close function messes up the visibility signals received by the - // type and it's derived types -// function close() { -// console.log("Closing " + window) -// if (destroyOnCloseButton) { -// destroyOnInvisible = true -// } -// visible = false; -// } - - function framedRect() { - if (!frame || !frame.decoration) { - return Qt.rect(0, 0, window.width, window.height) - } - return Qt.rect(frame.decoration.anchors.leftMargin, frame.decoration.anchors.topMargin, - window.width - frame.decoration.anchors.leftMargin - frame.decoration.anchors.rightMargin, - window.height - frame.decoration.anchors.topMargin - frame.decoration.anchors.bottomMargin) - } - - Keys.onPressed: { - switch(event.key) { - case Qt.Key_Control: - case Qt.Key_Shift: - case Qt.Key_Meta: - case Qt.Key_Alt: - break; - - case Qt.Key_W: - if (window.closable && (event.modifiers === Qt.ControlModifier)) { - visible = false - event.accepted = true - } - // fall through - - default: - // Consume unmodified keyboard entries while the window is focused, to prevent them - // from propagating to the application - if (event.modifiers === Qt.NoModifier) { - event.accepted = true; - } - break; - } - } -} diff --git a/interface/resources/qml/windows/DefaultFrame.qml b/interface/resources/qml/windows/DefaultFrame.qml index c58f9ca545..242209dbe0 100644 --- a/interface/resources/qml/windows/DefaultFrame.qml +++ b/interface/resources/qml/windows/DefaultFrame.qml @@ -1,20 +1,48 @@ +// +// DefaultFrame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 +import QtGraphicalEffects 1.0 import "." -import "../controls" +import "../styles-uit" Frame { - id: frame - - property bool wideTopMargin: (window && (window.closable || window.title)); + HifiConstants { id: hifi } Rectangle { - anchors { margins: -iconSize; topMargin: -iconSize * (wideTopMargin ? 2 : 1); } - anchors.fill: parent; - color: "#7f7f7f7f"; - radius: 3; + // Dialog frame + id: frameContent - // Allow dragging of the window + readonly property int iconSize: hifi.dimensions.frameIconSize + readonly property int frameMargin: 9 + readonly property int frameMarginLeft: frameMargin + readonly property int frameMarginRight: frameMargin + readonly property int frameMarginTop: 2 * frameMargin + iconSize + readonly property int frameMarginBottom: iconSize + 11 + + anchors { + topMargin: -frameMarginTop + leftMargin: -frameMarginLeft + rightMargin: -frameMarginRight + bottomMargin: -frameMarginBottom + } + anchors.fill: parent + color: hifi.colors.baseGrayHighlight40 + border { + width: hifi.dimensions.borderWidth + color: hifi.colors.faintGray50 + } + radius: hifi.dimensions.borderRadius + + // Enable dragging of the window MouseArea { anchors.fill: parent drag.target: window @@ -22,48 +50,70 @@ Frame { Row { id: controlsRow - anchors { right: parent.right; top: parent.top; rightMargin: iconSize; topMargin: iconSize / 2; } - spacing: iconSize / 4 - FontAwesome { - visible: false - text: "\uf08d" - style: Text.Outline; styleColor: "white" - size: frame.iconSize - rotation: !frame.parent ? 90 : frame.parent.pinned ? 0 : 90 - color: frame.pinned ? "red" : "black" + anchors { + right: parent.right; + top: parent.top; + topMargin: frameContent.frameMargin + 1 // Move down a little to visually align with the title + rightMargin: frameContent.frameMarginRight; + } + spacing: frameContent.iconSize / 4 + + HiFiGlyphs { + // "Pin" button + visible: window.pinnable + text: window.pinned ? hifi.glyphs.pinInverted : hifi.glyphs.pin + color: pinClickArea.pressed ? hifi.colors.redHighlight : hifi.colors.white + size: frameContent.iconSize MouseArea { + id: pinClickArea anchors.fill: parent + hoverEnabled: true propagateComposedEvents: true - onClicked: { frame.pin(); mouse.accepted = false; } + onClicked: window.pinned = !window.pinned; } } - FontAwesome { + + HiFiGlyphs { + // "Close" button visible: window ? window.closable : false - text: closeClickArea.containsMouse ? "\uf057" : "\uf05c" - style: Text.Outline; - styleColor: "white" - color: closeClickArea.containsMouse ? "red" : "black" - size: frame.iconSize + text: closeClickArea.containsPress ? hifi.glyphs.closeInverted : hifi.glyphs.close + color: closeClickArea.containsMouse ? hifi.colors.redHighlight : hifi.colors.white + size: frameContent.iconSize MouseArea { id: closeClickArea anchors.fill: parent hoverEnabled: true - onClicked: window.visible = false; + onClicked: window.shown = false; } } } - Text { + RalewayRegular { + // Title id: titleText - anchors { left: parent.left; leftMargin: iconSize; right: controlsRow.left; rightMargin: iconSize; top: parent.top; topMargin: iconSize / 2; } + anchors { + left: parent.left + leftMargin: frameContent.frameMarginLeft + hifi.dimensions.contentMargin.x + right: controlsRow.left + rightMargin: frameContent.iconSize + top: parent.top + topMargin: frameContent.frameMargin + } text: window ? window.title : "" - elide: Text.ElideRight - font.bold: true - color: (window && window.focus) ? "white" : "gray" - style: Text.Outline; - styleColor: "black" + color: hifi.colors.white + size: hifi.fontSizes.overlayTitle + } + + DropShadow { + source: titleText + anchors.fill: titleText + horizontalOffset: 2 + verticalOffset: 2 + samples: 2 + color: hifi.colors.baseGrayShadow60 + visible: (window && window.focus) + cached: true } } - } diff --git a/interface/resources/qml/windows/Fadable.qml b/interface/resources/qml/windows/Fadable.qml index 0352966bd0..38cd4bf1f9 100644 --- a/interface/resources/qml/windows/Fadable.qml +++ b/interface/resources/qml/windows/Fadable.qml @@ -1,8 +1,18 @@ +// +// Fadable.qml +// +// Created by Bradley Austin Davis on 15 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 import QtQuick.Controls 1.4 import QtGraphicalEffects 1.0 -import "." -import "../styles" + +import "../styles-uit" // Enable window visibility transitions FocusScope { @@ -13,6 +23,7 @@ FocusScope { fadeTargetProperty = visible ? 1.0 : 0.0 } + property var completionCallback; // The target property to animate, usually scale or opacity property alias fadeTargetProperty: root.opacity // always start the property at 0 to enable fade in on creation @@ -33,6 +44,13 @@ FocusScope { fadeTargetProperty = target ? 1.0 : 0.0; return; } + + // Now handle completions + if (completionCallback) { + completionCallback(); + completionCallback = undefined; + } + } // The actual animator @@ -43,8 +61,17 @@ FocusScope { } } - // Once we're transparent, disable the dialog's visibility onFadeTargetPropertyChanged: { visible = (fadeTargetProperty != 0.0); } + + function fadeIn(callback) { + completionCallback = callback; + fadeTargetProperty = 1.0; + } + + function fadeOut(callback) { + completionCallback = callback; + fadeTargetProperty = 0.0; + } } diff --git a/interface/resources/qml/windows/Frame.qml b/interface/resources/qml/windows/Frame.qml index 20bf669b9a..88d8c3ad41 100644 --- a/interface/resources/qml/windows/Frame.qml +++ b/interface/resources/qml/windows/Frame.qml @@ -1,24 +1,42 @@ -import QtQuick 2.5 +// +// Frame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// -import "../controls" +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "../styles-uit" import "../js/Utils.js" as Utils Item { id: frame + HifiConstants { id: hifi } + + default property var decoration + + property bool gradientsSupported: desktop.gradientsSupported + + readonly property int frameMarginLeft: frameContent.frameMarginLeft + readonly property int frameMarginRight: frameContent.frameMarginRight + readonly property int frameMarginTop: frameContent.frameMarginTop + readonly property int frameMarginBottom: frameContent.frameMarginBottom + // Frames always fill their parents, but their decorations may extend // beyond the window via negative margin sizes anchors.fill: parent - // Convenience accessor for the window - property alias window: frame.parent - readonly property int iconSize: 24 - default property var decoration; - children: [ + focusShadow, decoration, sizeOutline, debugZ, - sizeDrag, + sizeDrag ] Text { @@ -35,57 +53,81 @@ Item { window.height = newSize.y } + RadialGradient { + id: focusShadow + width: 1.66 * window.width + height: 1.66 * window.height + x: (window.width - width) / 2 + y: window.height / 2 - 0.375 * height + visible: gradientsSupported && window && window.focus && window.content.visible + gradient: Gradient { + // GradientStop position 0.5 is at full circumference of circle that fits inside the square. + GradientStop { position: 0.0; color: "#ff000000" } // black, 100% opacity + GradientStop { position: 0.333; color: "#1f000000" } // black, 12% opacity + GradientStop { position: 0.5; color: "#00000000" } // black, 0% opacity + GradientStop { position: 1.0; color: "#00000000" } + } + cached: true + } + Rectangle { id: sizeOutline - width: window ? window.width : 0 - height: window ? window.height : 0 - color: "#00000000" - border.width: 4 - radius: 10 + x: -frameMarginLeft + y: -frameMarginTop + width: window ? window.width + frameMarginLeft + frameMarginRight + 2 : 0 + height: window ? window.height + frameMarginTop + frameMarginBottom + 2 : 0 + color: hifi.colors.baseGrayHighlight15 + border.width: 3 + border.color: hifi.colors.white50 + radius: hifi.dimensions.borderRadius visible: window ? !window.content.visible : false } MouseArea { + // Resize handle id: sizeDrag - width: iconSize - height: iconSize + width: hifi.dimensions.frameIconSize + height: hifi.dimensions.frameIconSize enabled: window ? window.resizable : false - x: window ? window.width : 0 - y: window ? window.height : 0 + hoverEnabled: true + x: window ? window.width + frameMarginRight - hifi.dimensions.frameIconSize : 0 + y: window ? window.height + 4 : 0 property vector2d pressOrigin property vector2d sizeOrigin property bool hid: false onPressed: { - console.log("Pressed on size") + //console.log("Pressed on size") pressOrigin = Qt.vector2d(mouseX, mouseY) sizeOrigin = Qt.vector2d(window.content.width, window.content.height) hid = false; } onReleased: { if (hid) { - window.content.visible = true + pane.visible = true + frameContent.visible = true hid = false; } } onPositionChanged: { if (pressed) { - if (window.content.visible) { - window.content.visible = false; + if (pane.visible) { + pane.visible = false; + frameContent.visible = false hid = true; } var delta = Qt.vector2d(mouseX, mouseY).minus(pressOrigin); frame.deltaSize(delta.x, delta.y) } } - FontAwesome { + HiFiGlyphs { visible: sizeDrag.enabled - rotation: -45 - anchors { centerIn: parent } - horizontalAlignment: Text.AlignHCenter - text: "\uf07d" - size: iconSize / 3 * 2 - style: Text.Outline; styleColor: "white" + x: -11 // Move a little to visually align + y: window.modality == Qt.ApplicationModal ? -6 : -4 + text: hifi.glyphs.resizeHandle + size: hifi.dimensions.frameIconSize + 10 + color: sizeDrag.containsMouse || sizeDrag.pressed + ? hifi.colors.white + : (window.colorScheme == hifi.colorSchemes.dark ? hifi.colors.white50 : hifi.colors.lightGrayText80) } } - } diff --git a/interface/resources/qml/windows/HiddenFrame.qml b/interface/resources/qml/windows/HiddenFrame.qml index 2621b71eed..3d3fd047e2 100644 --- a/interface/resources/qml/windows/HiddenFrame.qml +++ b/interface/resources/qml/windows/HiddenFrame.qml @@ -2,7 +2,7 @@ import QtQuick 2.5 import "." -Frame { +Item { id: frame Item { anchors.fill: parent } diff --git a/interface/resources/qml/windows/ModalFrame.qml b/interface/resources/qml/windows/ModalFrame.qml index eb4641bc75..211353b5f3 100644 --- a/interface/resources/qml/windows/ModalFrame.qml +++ b/interface/resources/qml/windows/ModalFrame.qml @@ -1,36 +1,98 @@ +// +// ModalFrame.qml +// +// Created by Bradley Austin Davis on 15 Jan 2016 +// 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 "." -import "../controls" +import "../controls-uit" +import "../styles-uit" Frame { - id: frame + HifiConstants { id: hifi } - Item { - anchors.fill: parent + Rectangle { + id: frameContent - Rectangle { - id: background - anchors.fill: parent - anchors.margins: -4096 - visible: window.visible - color: "#7f7f7f7f"; - radius: 3; + readonly property bool hasTitle: window.title != "" + + readonly property int frameMarginLeft: hifi.dimensions.modalDialogMargin.x + readonly property int frameMarginRight: hifi.dimensions.modalDialogMargin.x + readonly property int frameMarginTop: hifi.dimensions.modalDialogMargin.y + (frameContent.hasTitle ? hifi.dimensions.modalDialogTitleHeight + 10 : 0) + readonly property int frameMarginBottom: hifi.dimensions.modalDialogMargin.y + + signal frameClicked(); + + anchors { + fill: parent + topMargin: -frameMarginTop + leftMargin: -frameMarginLeft + rightMargin: -frameMarginRight + bottomMargin: -frameMarginBottom } - Text { - y: -implicitHeight - iconSize / 2 - text: window.title - elide: Text.ElideRight - font.bold: true - color: window.focus ? "white" : "gray" - style: Text.Outline; - styleColor: "black" + border { + width: hifi.dimensions.borderWidth + color: hifi.colors.lightGrayText80 + } + radius: hifi.dimensions.borderRadius + color: hifi.colors.faintGray + + // Enable dragging of the window + MouseArea { + anchors.fill: parent + drag.target: window + enabled: window.draggable + onClicked: window.frameClicked(); + } + + Item { + visible: frameContent.hasTitle + anchors.fill: parent + anchors { + topMargin: -parent.anchors.topMargin + leftMargin: -parent.anchors.leftMargin + rightMargin: -parent.anchors.rightMargin + } + + Item { + width: title.width + (icon.text !== "" ? icon.width + hifi.dimensions.contentSpacing.x : 0) + x: (parent.width - width) / 2 + + onWidthChanged: window.titleWidth = width + + HiFiGlyphs { + id: icon + text: window.iconText ? window.iconText : "" + size: window.iconSize ? window.iconSize : 30 + color: hifi.colors.lightGray + visible: text != "" + anchors.verticalCenter: title.verticalCenter + anchors.left: parent.left + } + RalewayRegular { + id: title + text: window.title + elide: Text.ElideRight + color: hifi.colors.baseGrayHighlight + size: hifi.fontSizes.overlayTitle + y: -hifi.dimensions.modalDialogTitleHeight + anchors.right: parent.right + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: hifi.colors.lightGray + } } } - - - - } - diff --git a/interface/resources/qml/windows/ModalWindow.qml b/interface/resources/qml/windows/ModalWindow.qml index 32443e70e3..2d56099051 100644 --- a/interface/resources/qml/windows/ModalWindow.qml +++ b/interface/resources/qml/windows/ModalWindow.qml @@ -1,14 +1,28 @@ +// +// ModalWindow.qml +// +// Created by Bradley Austin Davis on 22 Jan 2016 +// 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 "." -Window { - id: root - anchors.centerIn: parent +ScrollingWindow { + id: window modality: Qt.ApplicationModal destroyOnCloseButton: true - destroyOnInvisible: true - frame: ModalFrame{} + destroyOnHidden: true + frame: ModalFrame { } + + property int colorScheme: hifi.colorSchemes.light + property bool draggable: false + + signal frameClicked(); + + anchors.centerIn: draggable ? undefined : parent } - - diff --git a/interface/resources/qml/windows/ScrollingWindow.qml b/interface/resources/qml/windows/ScrollingWindow.qml new file mode 100644 index 0000000000..f1dc744344 --- /dev/null +++ b/interface/resources/qml/windows/ScrollingWindow.qml @@ -0,0 +1,157 @@ +// +// Window.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtGraphicalEffects 1.0 + +import "." +import "../styles-uit" + +// FIXME how do I set the initial position of a window without +// overriding places where the a individual client of the window +// might be setting the position with a Settings{} element? + +// FIXME how to I enable dragging without allowing the window to lay outside +// of the desktop? How do I ensure when the desktop resizes all the windows +// are still at least partially visible? +Window { + id: window + HifiConstants { id: hifi } + children: [ swallower, frame, pane, activator ] + + property var footer: Item { } // Optional static footer at the bottom of the dialog. + + // Scrollable window content. + // FIXME this should not define any visual content in this type. The base window + // type should only consist of logic sized areas, with nothing drawn (although the + // default value for the frame property does include visual decorations) + property var pane: Item { + property bool isScrolling: scrollView.height < scrollView.contentItem.height + property int contentWidth: scrollView.width - (isScrolling ? 10 : 0) + property int scrollHeight: scrollView.height + + anchors.fill: parent + anchors.rightMargin: isScrolling ? 11 : 0 + + Rectangle { + id: contentBackground + anchors.fill: parent + anchors.rightMargin: parent.isScrolling ? 11 : 0 + color: hifi.colors.baseGray + visible: !window.hideBackground && modality != Qt.ApplicationModal + } + + + LinearGradient { + visible: !window.hideBackground && gradientsSupported && modality != Qt.ApplicationModal + anchors.top: contentBackground.bottom + anchors.left: contentBackground.left + width: contentBackground.width - 1 + height: 4 + start: Qt.point(0, 0) + end: Qt.point(0, 4) + gradient: Gradient { + GradientStop { position: 0.0; color: hifi.colors.darkGray } + GradientStop { position: 1.0; color: hifi.colors.darkGray0 } + } + cached: true + } + + ScrollView { + id: scrollView + contentItem: content + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + verticalScrollBarPolicy: Qt.ScrollBarAsNeeded + anchors.fill: parent + anchors.rightMargin: parent.isScrolling ? 1 : 0 + anchors.bottomMargin: footer.height > 0 ? footerPane.height : 0 + + style: ScrollViewStyle { + + padding.right: -7 // Move to right away from content. + + handle: Item { + implicitWidth: 8 + Rectangle { + radius: 4 + color: hifi.colors.white30 + anchors { + fill: parent + leftMargin: 2 // Finesse size and position. + topMargin: 1 + bottomMargin: 1 + } + } + } + + scrollBarBackground: Item { + implicitWidth: 10 + Rectangle { + color: hifi.colors.darkGray30 + radius: 4 + anchors { + fill: parent + topMargin: -1 // Finesse size + bottomMargin: -2 + } + } + } + + incrementControl: Item { + visible: false + } + + decrementControl: Item { + visible: false + } + } + } + + Rectangle { + // Optional non-scrolling footer. + id: footerPane + anchors { + left: parent.left + bottom: parent.bottom + } + width: parent.contentWidth + height: footer.height + 2 * hifi.dimensions.contentSpacing.y + 3 + color: hifi.colors.baseGray + visible: footer.height > 0 + + Item { + // Horizontal rule. + anchors.fill: parent + + Rectangle { + width: parent.width + height: 1 + y: 1 // Stop displaying content just above horizontal rule/=. + color: hifi.colors.baseGrayShadow + } + + Rectangle { + width: parent.width + height: 1 + y: 2 + color: hifi.colors.baseGrayHighlight + } + } + + Item { + anchors.fill: parent + anchors.topMargin: 3 // Horizontal rule. + children: [ footer ] + } + } + } +} diff --git a/interface/resources/qml/windows/ToolFrame.qml b/interface/resources/qml/windows/ToolFrame.qml new file mode 100644 index 0000000000..eff5fc0377 --- /dev/null +++ b/interface/resources/qml/windows/ToolFrame.qml @@ -0,0 +1,96 @@ +// +// DefaultFrame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "." +import "../styles-uit" + +Frame { + HifiConstants { id: hifi } + property bool horizontalSpacers: false + property bool verticalSpacers: false + + Rectangle { + // Dialog frame + id: frameContent + readonly property int frameMargin: 6 + readonly property int frameMarginLeft: frameMargin + (horizontalSpacers ? 12 : 0) + readonly property int frameMarginRight: frameMargin + (horizontalSpacers ? 12 : 0) + readonly property int frameMarginTop: frameMargin + (verticalSpacers ? 12 : 0) + readonly property int frameMarginBottom: frameMargin + (verticalSpacers ? 12 : 0) + + Rectangle { + visible: horizontalSpacers + anchors.left: parent.left + anchors.leftMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: 8 + height: window.height + color: "gray"; + radius: 4 + } + + Rectangle { + visible: horizontalSpacers + anchors.right: parent.right + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: 8 + height: window.height + color: "gray"; + radius: 4 + } + + Rectangle { + visible: verticalSpacers + anchors.top: parent.top + anchors.topMargin: 6 + anchors.horizontalCenter: parent.horizontalCenter + height: 8 + width: window.width + color: "gray"; + radius: 4 + } + + Rectangle { + visible: verticalSpacers + anchors.bottom: parent.bottom + anchors.bottomMargin: 6 + anchors.horizontalCenter: parent.horizontalCenter + height: 8 + width: window.width + color: "gray"; + radius: 4 + } + + anchors { + leftMargin: -frameMarginLeft + rightMargin: -frameMarginRight + topMargin: -frameMarginTop + bottomMargin: -frameMarginBottom + } + anchors.fill: parent + color: hifi.colors.baseGrayHighlight40 + border { + width: hifi.dimensions.borderWidth + color: hifi.colors.faintGray50 + } + radius: hifi.dimensions.borderRadius / 2 + + // Enable dragging of the window + MouseArea { + anchors.fill: parent + drag.target: window + } + } +} + diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 3abdbacc64..e3e70c1e74 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -1,9 +1,20 @@ +// +// Window.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 import QtGraphicalEffects 1.0 import "." -import "../styles" +import "../styles-uit" // FIXME how do I set the initial position of a window without // overriding places where the a individual client of the window @@ -15,16 +26,36 @@ import "../styles" Fadable { id: window HifiConstants { id: hifi } + + // + // Signals + // + signal windowDestroyed(); + + // + // Native properties + // + // The Window size is the size of the content, while the frame // decorations can extend outside it. implicitHeight: content ? content.height : 0 implicitWidth: content ? content.width : 0 x: desktop.invalid_position; y: desktop.invalid_position; - enabled: visible + children: [ swallower, frame, content, activator ] - signal windowDestroyed(); + // + // Custom properties + // property int modality: Qt.NonModal + // Corresponds to the window shown / hidden state AS DISTINCT from window visibility. + // Window visibility should NOT be used as a proxy for any other behavior. + property bool shown: true + // FIXME workaround to deal with the face that some visual items are defined here, + // when they should be moved to a frame derived type + property bool hideBackground: false + visible: shown + enabled: visible readonly property bool topLevelWindow: true property string title // Should the window be closable control? @@ -34,17 +65,23 @@ Fadable { // Should hitting the close button hide or destroy the window? property bool destroyOnCloseButton: true // Should hiding the window destroy it or just hide it? - property bool destroyOnInvisible: false - // FIXME support for pinned / unpinned pending full design - // property bool pinnable: false - // property bool pinned: false + property bool destroyOnHidden: false + property bool pinnable: true + property bool pinned: false property bool resizable: false + property bool gradientsSupported: desktop.gradientsSupported + property int colorScheme: hifi.colorSchemes.dark + property vector2d minSize: Qt.vector2d(100, 100) - property vector2d maxSize: Qt.vector2d(1280, 720) + property vector2d maxSize: Qt.vector2d(1280, 800) // The content to place inside the window, determined by the client default property var content + property var footer: Item { } // Optional static footer at the bottom of the dialog. + + function setDefaultFocus() {} // Default function; can be overridden by dialogs. + property var rectifier: Timer { property bool executing: false; interval: 100 @@ -65,20 +102,15 @@ Fadable { } } - - onXChanged: rectifier.begin(); - onYChanged: rectifier.begin(); - // This mouse area serves to raise the window. To function, it must live // in the window and have a higher Z-order than the content, but follow // the position and size of frame decoration property var activator: MouseArea { - width: frame.decoration.width - height: frame.decoration.height - x: frame.decoration.anchors.margins - y: frame.decoration.anchors.topMargin + width: frame.decoration ? frame.decoration.width : window.width + height: frame.decoration ? frame.decoration.height : window.height + x: frame.decoration ? frame.decoration.anchors.leftMargin : 0 + y: frame.decoration ? frame.decoration.anchors.topMargin : 0 propagateComposedEvents: true - hoverEnabled: true acceptedButtons: Qt.AllButtons enabled: window.visible onPressed: { @@ -92,10 +124,10 @@ Fadable { // to prevent things like mouse wheel events from reaching the application and changing // the camera if the user is scrolling through a list and gets to the end. property var swallower: MouseArea { - width: frame.decoration.width - height: frame.decoration.height - x: frame.decoration.anchors.margins - y: frame.decoration.anchors.topMargin + width: frame.decoration ? frame.decoration.width : window.width + height: frame.decoration ? frame.decoration.height : window.height + x: frame.decoration ? frame.decoration.anchors.leftMargin : 0 + y: frame.decoration ? frame.decoration.anchors.topMargin : 0 hoverEnabled: true acceptedButtons: Qt.AllButtons enabled: window.visible @@ -106,71 +138,119 @@ Fadable { onWheel: {} } - // Default to a standard frame. Can be overriden to provide custom // frame styles, like a full desktop frame to simulate a modal window - property var frame: DefaultFrame { } + property var frame: DefaultFrame { + //window: window + } - children: [ swallower, frame, content, activator ] - + // + // Handlers + // Component.onCompleted: { window.parentChanged.connect(raise); - raise(); - centerOrReposition(); + setDefaultFocus(); + d.centerOrReposition(); + d.updateVisibility(shown); } Component.onDestruction: { window.parentChanged.disconnect(raise); // Prevent warning on shutdown windowDestroyed(); } - function centerOrReposition() { - if (x == desktop.invalid_position && y == desktop.invalid_position) { - desktop.centerOnVisible(window); - } else { - desktop.repositionOnVisible(window); - } - } + onXChanged: rectifier.begin(); + onYChanged: rectifier.begin(); + + onShownChanged: d.updateVisibility(shown) onVisibleChanged: { - if (!visible && destroyOnInvisible) { - destroy(); - return; - } - if (visible) { - raise(); - } enabled = visible - if (visible && parent) { - centerOrReposition(); + d.centerOrReposition(); } } + QtObject { + id: d + + readonly property alias pinned: window.pinned + readonly property alias shown: window.shown + readonly property alias modality: window.modality; + + function getTargetVisibility() { + if (!window.shown) { + return false; + } + + if (modality !== Qt.NonModal) { + return true; + } + + if (pinned) { + return true; + } + + if (desktop && !desktop.pinned) { + return true; + } + + return false; + } + + // The force flag causes all windows to fade back in, because a window was shown + readonly property alias visible: window.visible + function updateVisibility(force) { + if (force && !pinned && desktop.pinned) { + // Change the pinned state (which in turn will call us again) + desktop.pinned = false; + return; + } + + var targetVisibility = getTargetVisibility(); + if (targetVisibility === visible) { + return; + } + + if (targetVisibility) { + fadeIn(function() { + if (force) { + window.raise(); + } + }); + } else { + fadeOut(function() { + if (!window.shown && window.destroyOnHidden) { + window.destroy(); + } + }); + } + } + + function centerOrReposition() { + if (x == desktop.invalid_position && y == desktop.invalid_position) { + desktop.centerOnVisible(window); + } else { + desktop.repositionOnVisible(window); + } + } + + } + + // When the desktop pinned state changes, automatically handle the current windows + Connections { target: desktop; onPinnedChanged: d.updateVisibility() } + + function raise() { if (visible && parent) { desktop.raise(window) } } - function pin() { -// pinned = ! pinned + function setPinned() { + pinned = !pinned } - // our close function performs the same way as the OffscreenUI class: - // don't do anything but manipulate the targetVisible flag and let the other - // mechanisms decide if the window should be destroyed after the close - // animation completes - // FIXME using this close function messes up the visibility signals received by the - // type and it's derived types -// function close() { -// console.log("Closing " + window) -// if (destroyOnCloseButton) { -// destroyOnInvisible = true -// } -// visible = false; -// } - function framedRect() { if (!frame || !frame.decoration) { return Qt.rect(0, 0, window.width, window.height) @@ -180,7 +260,6 @@ Fadable { window.height - frame.decoration.anchors.topMargin - frame.decoration.anchors.bottomMargin) } - Keys.onPressed: { switch(event.key) { case Qt.Key_Control: @@ -189,10 +268,9 @@ Fadable { case Qt.Key_Alt: break; - case Qt.Key_W: if (window.closable && (event.modifiers === Qt.ControlModifier)) { - visible = false + shown = false event.accepted = true } // fall through diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f553b4bbdf..37c3b361bf 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -68,6 +68,7 @@ #include #include #include +#include #include #include #include @@ -131,6 +132,7 @@ #include "scripting/WebWindowClass.h" #include "scripting/WindowScriptingInterface.h" #include "scripting/ControllerScriptingInterface.h" +#include "scripting/ToolbarScriptingInterface.h" #include "scripting/RatesScriptingInterface.h" #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" @@ -149,6 +151,8 @@ #include "InterfaceParentFinder.h" #include "FrameTimingsScriptingInterface.h" +#include +#include // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. @@ -435,7 +439,8 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - + DependencyManager::set(); + DependencyManager::set(); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) DependencyManager::set(); @@ -673,10 +678,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : accountManager->setIsAgent(true); accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL); - // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. - // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. - UserActivityLogger::getInstance().launch(applicationVersion(), _previousSessionCrashed, sessionRunTime.get()); - auto addressManager = DependencyManager::get(); // use our MyAvatar position and quat for address manager path @@ -766,6 +767,39 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // Make sure we don't time out during slow operations at startup updateHeartbeat(); + + // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. + // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. + auto gpuIdent = GPUIdent::getInstance(); + auto glContextData = getGLContextData(); + QJsonObject properties = { + { "previousSessionCrashed", _previousSessionCrashed }, + { "previousSessionRuntime", sessionRunTime.get() }, + { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, + { "kernel_type", QSysInfo::kernelType() }, + { "kernel_version", QSysInfo::kernelVersion() }, + { "os_type", QSysInfo::productType() }, + { "os_version", QSysInfo::productVersion() }, + { "gpu_name", gpuIdent->getName() }, + { "gpu_driver", gpuIdent->getDriver() }, + { "gpu_memory", static_cast(gpuIdent->getMemory()) }, + { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, + { "gl_version", glContextData["version"] }, + { "gl_vender", glContextData["vendor"] }, + { "gl_sl_version", glContextData["slVersion"] }, + { "gl_renderer", glContextData["renderer"] } + }; + auto macVersion = QSysInfo::macVersion(); + if (macVersion != QSysInfo::MV_None) { + properties["os_osx_version"] = QSysInfo::macVersion(); + } + auto windowsVersion = QSysInfo::windowsVersion(); + if (windowsVersion != QSysInfo::WV_None) { + properties["os_win_version"] = QSysInfo::windowsVersion(); + } + UserActivityLogger::getInstance().logAction("launch", properties); + + // Tell our entity edit sender about our known jurisdictions _entityEditSender.setServerJurisdictions(&_entityServerJurisdictions); _entityEditSender.setMyAvatar(getMyAvatar()); @@ -1061,6 +1095,89 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : } }); + // Add periodic checks to send user activity data + static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; + static int SEND_STATS_INTERVAL_MS = 10000; + static int NEARBY_AVATAR_RADIUS_METERS = 10; + + // Periodically send fps as a user activity event + QTimer* sendStatsTimer = new QTimer(this); + sendStatsTimer->setInterval(SEND_STATS_INTERVAL_MS); + connect(sendStatsTimer, &QTimer::timeout, this, [this]() { + QJsonObject properties = {}; + MemoryInfo memInfo; + if (getMemoryInfo(memInfo)) { + properties["system_memory_total"] = static_cast(memInfo.totalMemoryBytes); + properties["system_memory_used"] = static_cast(memInfo.usedMemoryBytes); + properties["process_memory_used"] = static_cast(memInfo.processUsedMemoryBytes); + } + + auto displayPlugin = qApp->getActiveDisplayPlugin(); + + properties["fps"] = _frameCounter.rate(); + properties["present_rate"] = displayPlugin->presentRate(); + properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate(); + properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); + properties["sim_rate"] = getAverageSimsPerSecond(); + properties["avatar_sim_rate"] = getAvatarSimrate(); + + auto bandwidthRecorder = DependencyManager::get(); + properties["packet_rate_in"] = bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond(); + properties["packet_rate_out"] = bandwidthRecorder->getCachedTotalAverageOutputPacketsPerSecond(); + properties["kbps_in"] = bandwidthRecorder->getCachedTotalAverageInputKilobitsPerSecond(); + properties["kbps_out"] = bandwidthRecorder->getCachedTotalAverageOutputKilobitsPerSecond(); + + auto nodeList = DependencyManager::get(); + SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer); + SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); + SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); + SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer); + SharedNodePointer messagesMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer); + properties["entity_ping"] = entityServerNode ? entityServerNode->getPingMs() : -1; + properties["audio_ping"] = audioMixerNode ? audioMixerNode->getPingMs() : -1; + properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1; + properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1; + properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1; + + auto loadingRequests = ResourceCache::getLoadingRequests(); + properties["active_downloads"] = loadingRequests.size(); + properties["pending_downloads"] = ResourceCache::getPendingRequestCount(); + + properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; + + UserActivityLogger::getInstance().logAction("stats", properties); + }); + sendStatsTimer->start(); + + + // Periodically check for count of nearby avatars + static int lastCountOfNearbyAvatars = -1; + QTimer* checkNearbyAvatarsTimer = new QTimer(this); + checkNearbyAvatarsTimer->setInterval(CHECK_NEARBY_AVATARS_INTERVAL_MS); + connect(checkNearbyAvatarsTimer, &QTimer::timeout, this, [this]() { + auto avatarManager = DependencyManager::get(); + int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getPosition(), + NEARBY_AVATAR_RADIUS_METERS) - 1; + if (nearbyAvatars != lastCountOfNearbyAvatars) { + lastCountOfNearbyAvatars = nearbyAvatars; + UserActivityLogger::getInstance().logAction("nearby_avatars", { { "count", nearbyAvatars } }); + } + }); + checkNearbyAvatarsTimer->start(); + + // Track user activity event when we receive a mute packet + auto onMutedByMixer = []() { + UserActivityLogger::getInstance().logAction("received_mute_packet"); + }; + connect(DependencyManager::get().data(), &AudioClient::mutedByMixer, this, onMutedByMixer); + + // Track when the address bar is opened + auto onAddressBarToggled = [this]() { + // Record time + UserActivityLogger::getInstance().logAction("opened_address_bar", { { "uptime_ms", _sessionRunTimer.elapsed() } }); + }; + connect(DependencyManager::get().data(), &DialogsManager::addressBarToggled, this, onAddressBarToggled); + // Make sure we don't time out during slow operations at startup updateHeartbeat(); @@ -1419,7 +1536,7 @@ void Application::initializeUi() { rootContext->setContextProperty("Overlays", &_overlays); rootContext->setContextProperty("Window", DependencyManager::get().data()); - rootContext->setContextProperty("Menu", MenuScriptingInterface::getInstance()); + rootContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); rootContext->setContextProperty("Stats", Stats::getInstance()); rootContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); rootContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); @@ -2070,7 +2187,8 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_X: if (isShifted && isMeta) { auto offscreenUi = DependencyManager::get(); - offscreenUi->getRootContext()->engine()->clearComponentCache(); + offscreenUi->togglePinned(); + //offscreenUi->getRootContext()->engine()->clearComponentCache(); //OffscreenUi::information("Debugging", "Component cache cleared"); // placeholder for dialogs being converted to QML. } @@ -2719,7 +2837,6 @@ void Application::idle(float nsecsElapsed) { if (firstIdle) { firstIdle = false; connect(offscreenUi.data(), &OffscreenUi::showDesktop, this, &Application::showDesktop); - _overlayConductor.setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::Overlays)); } PROFILE_RANGE(__FUNCTION__); @@ -3219,13 +3336,13 @@ void Application::updateThreads(float deltaTime) { } void Application::toggleOverlays() { - auto newOverlaysVisible = !_overlayConductor.getEnabled(); - Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, newOverlaysVisible); - _overlayConductor.setEnabled(newOverlaysVisible); + auto menu = Menu::getInstance(); + menu->setIsOptionChecked(MenuOption::Overlays, menu->isOptionChecked(MenuOption::Overlays)); } void Application::setOverlaysVisible(bool visible) { - _overlayConductor.setEnabled(visible); + auto menu = Menu::getInstance(); + menu->setIsOptionChecked(MenuOption::Overlays, true); } void Application::cycleCamera() { @@ -4210,6 +4327,7 @@ void Application::resetSensors(bool andReload) { DependencyManager::get()->reset(); DependencyManager::get()->reset(); getActiveDisplayPlugin()->resetSensors(); + _overlayConductor.centerUI(); getMyAvatar()->reset(andReload); QMetaObject::invokeMethod(DependencyManager::get().data(), "reset", Qt::QueuedConnection); } @@ -4526,6 +4644,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri RayToOverlayIntersectionResultFromScriptValue); scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, @@ -4574,6 +4693,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); + + scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); } bool Application::canAcceptURL(const QString& urlString) const { @@ -5176,6 +5297,11 @@ void Application::updateDisplayMode() { return; } + UserActivityLogger::getInstance().logAction("changed_display_mode", { + { "previous_display_mode", _displayPlugin ? _displayPlugin->getName() : "" }, + { "display_mode", newDisplayPlugin ? newDisplayPlugin->getName() : "" } + }); + auto offscreenUi = DependencyManager::get(); // Make the switch atomic from the perspective of other threads @@ -5300,9 +5426,7 @@ void Application::readArgumentsFromLocalSocket() const { } void Application::showDesktop() { - if (!_overlayConductor.getEnabled()) { - _overlayConductor.setEnabled(true); - } + Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, true); } CompositorHelper& Application::getApplicationCompositor() const { diff --git a/interface/src/Application.h b/interface/src/Application.h index 114ce27144..5beaa5b455 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -211,6 +211,8 @@ public: float getRenderResolutionScale() const; + qint64 getCurrentSessionRuntime() const { return _sessionRunTimer.elapsed(); } + bool isAboutToQuit() const { return _aboutToQuit; } // the isHMDMode is true whenever we use the interface from an HMD and not a standard flat display diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 24256fdf39..c4d985419e 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -80,7 +80,8 @@ void DiscoverabilityManager::updateLocation() { locationObject.insert(FRIENDS_ONLY_KEY_IN_LOCATION, (_mode.get() == Discoverability::Friends)); // if we have a session ID add it now, otherwise add a null value - rootObject[SESSION_ID_KEY] = _sessionID.isEmpty() ? QJsonValue() : _sessionID; + auto sessionID = accountManager->getSessionID(); + rootObject[SESSION_ID_KEY] = sessionID.isNull() ? QJsonValue() : sessionID.toString(); JSONCallbackParameters callbackParameters; callbackParameters.jsonCallbackReceiver = this; @@ -110,11 +111,8 @@ void DiscoverabilityManager::updateLocation() { callbackParameters.jsonCallbackMethod = "handleHeartbeatResponse"; QJsonObject heartbeatObject; - if (!_sessionID.isEmpty()) { - heartbeatObject[SESSION_ID_KEY] = _sessionID; - } else { - heartbeatObject[SESSION_ID_KEY] = QJsonValue(); - } + auto sessionID = accountManager->getSessionID(); + heartbeatObject[SESSION_ID_KEY] = sessionID.isNull() ? QJsonValue() : sessionID.toString(); accountManager->sendRequest(API_USER_HEARTBEAT_PATH, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, callbackParameters, @@ -126,11 +124,11 @@ void DiscoverabilityManager::handleHeartbeatResponse(QNetworkReply& requestReply auto dataObject = AccountManager::dataObjectFromResponse(requestReply); if (!dataObject.isEmpty()) { - _sessionID = dataObject[SESSION_ID_KEY].toString(); + auto sessionID = dataObject[SESSION_ID_KEY].toString(); // give that session ID to the account manager auto accountManager = DependencyManager::get(); - accountManager->setSessionID(_sessionID); + accountManager->setSessionID(sessionID); } } diff --git a/interface/src/DiscoverabilityManager.h b/interface/src/DiscoverabilityManager.h index 9a1fa7b39c..196b0cdf81 100644 --- a/interface/src/DiscoverabilityManager.h +++ b/interface/src/DiscoverabilityManager.h @@ -49,7 +49,6 @@ private: DiscoverabilityManager(); Setting::Handle _mode; - QString _sessionID; QJsonObject _lastLocationObject; }; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index b37f70f65d..6308ac6c73 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -256,8 +256,7 @@ Menu::Menu() { UNSPECIFIED_POSITION, "Advanced"); // View > Overlays - addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Overlays, 0, true, - qApp, SLOT(setOverlaysVisible(bool))); + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Overlays, 0, true); // Navigate menu ---------------------------------- MenuWrapper* navigateMenu = addMenu("Navigate"); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index be5bf7722f..6a69ee9a9a 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -722,7 +722,7 @@ void MyAvatar::saveData() { settings.setValue("displayName", _displayName); settings.setValue("collisionSoundURL", _collisionSoundURL); settings.setValue("useSnapTurn", _useSnapTurn); - settings.setValue("clearOverlayWhenDriving", _clearOverlayWhenDriving); + settings.setValue("clearOverlayWhenMoving", _clearOverlayWhenMoving); settings.endGroup(); } @@ -842,7 +842,7 @@ void MyAvatar::loadData() { setDisplayName(settings.value("displayName").toString()); setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString()); setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool()); - setClearOverlayWhenDriving(settings.value("clearOverlayWhenDriving", _clearOverlayWhenDriving).toBool()); + setClearOverlayWhenMoving(settings.value("clearOverlayWhenMoving", _clearOverlayWhenMoving).toBool()); settings.endGroup(); @@ -1248,8 +1248,7 @@ void MyAvatar::prepareForPhysicsSimulation() { _characterController.setPositionAndOrientation(getPosition(), getOrientation()); if (qApp->isHMDMode()) { - bool hasDriveInput = fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; - _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput); + _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput()); } else { _follow.deactivate(); } @@ -2134,3 +2133,7 @@ bool MyAvatar::didTeleport() { lastPosition = pos; return (changeInPosition.length() > MAX_AVATAR_MOVEMENT_PER_FRAME); } + +bool MyAvatar::hasDriveInput() const { + return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Y]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; +} diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 05afe39a32..96fa999de5 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -159,8 +159,8 @@ public: Q_INVOKABLE bool getSnapTurn() const { return _useSnapTurn; } Q_INVOKABLE void setSnapTurn(bool on) { _useSnapTurn = on; } - Q_INVOKABLE bool getClearOverlayWhenDriving() const { return _clearOverlayWhenDriving; } - Q_INVOKABLE void setClearOverlayWhenDriving(bool on) { _clearOverlayWhenDriving = on; } + Q_INVOKABLE bool getClearOverlayWhenMoving() const { return _clearOverlayWhenMoving; } + Q_INVOKABLE void setClearOverlayWhenMoving(bool on) { _clearOverlayWhenMoving = on; } Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } @@ -266,6 +266,8 @@ public: controller::Pose getLeftHandControllerPoseInAvatarFrame() const; controller::Pose getRightHandControllerPoseInAvatarFrame() const; + bool hasDriveInput() const; + Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); Q_INVOKABLE bool getCharacterControllerEnabled(); @@ -403,7 +405,7 @@ private: QString _fullAvatarModelName; QUrl _animGraphUrl {""}; bool _useSnapTurn { true }; - bool _clearOverlayWhenDriving { false }; + bool _clearOverlayWhenMoving { true }; // cache of the current HMD sensor position and orientation // in sensor space. diff --git a/interface/src/scripting/DesktopScriptingInterface.cpp b/interface/src/scripting/DesktopScriptingInterface.cpp index 843a40348e..f7bc8afe36 100644 --- a/interface/src/scripting/DesktopScriptingInterface.cpp +++ b/interface/src/scripting/DesktopScriptingInterface.cpp @@ -16,6 +16,7 @@ #include "Application.h" #include "MainWindow.h" +#include int DesktopScriptingInterface::getWidth() { QSize size = qApp->getWindow()->windowHandle()->screen()->virtualSize(); @@ -25,3 +26,8 @@ int DesktopScriptingInterface::getHeight() { QSize size = qApp->getWindow()->windowHandle()->screen()->virtualSize(); return size.height(); } + +void DesktopScriptingInterface::setOverlayAlpha(float alpha) { + qApp->getApplicationCompositor().setAlpha(alpha); +} + diff --git a/interface/src/scripting/DesktopScriptingInterface.h b/interface/src/scripting/DesktopScriptingInterface.h index be4eaadbfb..8da502cb11 100644 --- a/interface/src/scripting/DesktopScriptingInterface.h +++ b/interface/src/scripting/DesktopScriptingInterface.h @@ -22,6 +22,8 @@ class DesktopScriptingInterface : public QObject, public Dependency { Q_PROPERTY(int height READ getHeight) // Physical height of screen(s) including task bars and system menus public: + Q_INVOKABLE void setOverlayAlpha(float alpha); + int getWidth(); int getHeight(); }; diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index 7bf1547a3c..4dd43dfdf1 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -105,3 +105,25 @@ QString HMDScriptingInterface::preferredAudioInput() const { QString HMDScriptingInterface::preferredAudioOutput() const { return qApp->getActiveDisplayPlugin()->getPreferredAudioOutDevice(); } + +bool HMDScriptingInterface::setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const { + return qApp->getActiveDisplayPlugin()->setHandLaser(hands, + enabled ? DisplayPlugin::HandLaserMode::Overlay : DisplayPlugin::HandLaserMode::None, + color, direction); +} + +void HMDScriptingInterface::disableHandLasers(int hands) const { + qApp->getActiveDisplayPlugin()->setHandLaser(hands, DisplayPlugin::HandLaserMode::None); +} + +bool HMDScriptingInterface::suppressKeyboard() { + return qApp->getActiveDisplayPlugin()->suppressKeyboard(); +} + +void HMDScriptingInterface::unsuppressKeyboard() { + qApp->getActiveDisplayPlugin()->unsuppressKeyboard(); +} + +bool HMDScriptingInterface::isKeyboardVisible() { + return qApp->getActiveDisplayPlugin()->isKeyboardVisible(); +} diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index d4c7b7cc0e..2739522adf 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -1,4 +1,4 @@ -// + // HMDScriptingInterface.h // interface/src/scripting // @@ -12,6 +12,8 @@ #ifndef hifi_HMDScriptingInterface_h #define hifi_HMDScriptingInterface_h +#include + #include class QScriptContext; class QScriptEngine; @@ -31,12 +33,28 @@ public: Q_INVOKABLE glm::vec3 calculateRayUICollisionPoint(const glm::vec3& position, const glm::vec3& direction) const; Q_INVOKABLE glm::vec2 overlayFromWorldPoint(const glm::vec3& position) const; Q_INVOKABLE glm::vec3 worldPointFromOverlay(const glm::vec2& overlay) const; - Q_INVOKABLE glm::vec2 sphericalToOverlay(const glm::vec2 & sphericalPos) const; Q_INVOKABLE glm::vec2 overlayToSpherical(const glm::vec2 & overlayPos) const; Q_INVOKABLE QString preferredAudioInput() const; Q_INVOKABLE QString preferredAudioOutput() const; + Q_INVOKABLE bool setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const; + + Q_INVOKABLE void disableHandLasers(int hands) const; + /// Suppress the activation of any on-screen keyboard so that a script operation will + /// not be interrupted by a keyboard popup + /// Returns false if there is already an active keyboard displayed. + /// Clients should re-enable the keyboard when the operation is complete and ensure + /// that they balance any call to suppressKeyboard() that returns true with a corresponding + /// call to unsuppressKeyboard() within a reasonable amount of time + Q_INVOKABLE bool suppressKeyboard(); + + /// Enable the keyboard following a suppressKeyboard call + Q_INVOKABLE void unsuppressKeyboard(); + + /// Query the display plugin to determine the current VR keyboard visibility + Q_INVOKABLE bool isKeyboardVisible(); + public: HMDScriptingInterface(); static QScriptValue getHUDLookAtPosition2D(QScriptContext* context, QScriptEngine* engine); diff --git a/interface/src/scripting/ToolbarScriptingInterface.cpp b/interface/src/scripting/ToolbarScriptingInterface.cpp new file mode 100644 index 0000000000..82332b3187 --- /dev/null +++ b/interface/src/scripting/ToolbarScriptingInterface.cpp @@ -0,0 +1,115 @@ +// +// Created by Bradley Austin Davis on 2016-06-16 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ToolbarScriptingInterface.h" + +#include + +class QmlWrapper : public QObject { + Q_OBJECT +public: + QmlWrapper(QObject* qmlObject, QObject* parent = nullptr) + : QObject(parent), _qmlObject(qmlObject) { + } + + Q_INVOKABLE void writeProperty(QString propertyName, QVariant propertyValue) { + auto offscreenUi = DependencyManager::get(); + offscreenUi->executeOnUiThread([=] { + _qmlObject->setProperty(propertyName.toStdString().c_str(), propertyValue); + }); + } + + Q_INVOKABLE void writeProperties(QVariant propertyMap) { + auto offscreenUi = DependencyManager::get(); + offscreenUi->executeOnUiThread([=] { + QVariantMap map = propertyMap.toMap(); + for (const QString& key : map.keys()) { + _qmlObject->setProperty(key.toStdString().c_str(), map[key]); + } + }); + } + + Q_INVOKABLE QVariant readProperty(const QString& propertyName) { + auto offscreenUi = DependencyManager::get(); + return offscreenUi->returnFromUiThread([&]()->QVariant { + return _qmlObject->property(propertyName.toStdString().c_str()); + }); + } + + Q_INVOKABLE QVariant readProperties(const QVariant& propertyList) { + auto offscreenUi = DependencyManager::get(); + return offscreenUi->returnFromUiThread([&]()->QVariant { + QVariantMap result; + for (const QVariant& property : propertyList.toList()) { + QString propertyString = property.toString(); + result.insert(propertyString, _qmlObject->property(propertyString.toStdString().c_str())); + } + return result; + }); + } + + +protected: + QObject* _qmlObject{ nullptr }; +}; + + +class ToolbarButtonProxy : public QmlWrapper { + Q_OBJECT + +public: + ToolbarButtonProxy(QObject* qmlObject, QObject* parent = nullptr) : QmlWrapper(qmlObject, parent) { + connect(qmlObject, SIGNAL(clicked()), this, SIGNAL(clicked())); + } + +signals: + void clicked(); +}; + +class ToolbarProxy : public QmlWrapper { + Q_OBJECT + +public: + ToolbarProxy(QObject* qmlObject, QObject* parent = nullptr) : QmlWrapper(qmlObject, parent) { } + + Q_INVOKABLE QObject* addButton(const QVariant& properties) { + QVariant resultVar; + bool invokeResult = QMetaObject::invokeMethod(_qmlObject, "addButton", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, properties)); + if (!invokeResult) { + return nullptr; + } + + QObject* rawButton = qvariant_cast(resultVar); + if (!rawButton) { + return nullptr; + } + + return new ToolbarButtonProxy(rawButton, this); + } +}; + + +QObject* ToolbarScriptingInterface::getToolbar(const QString& toolbarId) { + auto offscreenUi = DependencyManager::get(); + auto desktop = offscreenUi->getDesktop(); + QVariant resultVar; + bool invokeResult = QMetaObject::invokeMethod(desktop, "getToolbar", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, toolbarId)); + if (!invokeResult) { + return nullptr; + } + + QObject* rawToolbar = qvariant_cast(resultVar); + if (!rawToolbar) { + return nullptr; + } + + return new ToolbarProxy(rawToolbar); +} + + +#include "ToolbarScriptingInterface.moc" \ No newline at end of file diff --git a/interface/src/scripting/ToolbarScriptingInterface.h b/interface/src/scripting/ToolbarScriptingInterface.h new file mode 100644 index 0000000000..9379284e55 --- /dev/null +++ b/interface/src/scripting/ToolbarScriptingInterface.h @@ -0,0 +1,26 @@ +// +// Created by Bradley Austin Davis on 2016-06-16 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ToolbarScriptingInterface_h +#define hifi_ToolbarScriptingInterface_h + +#include + +#include + +#include + +class ToolbarProxy; + +class ToolbarScriptingInterface : public QObject, public Dependency { + Q_OBJECT +public: + Q_INVOKABLE QObject* getToolbar(const QString& toolbarId); +}; + +#endif // hifi_ToolbarScriptingInterface_h diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 83d729779c..6a99641ce4 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -17,179 +17,134 @@ #include "OverlayConductor.h" OverlayConductor::OverlayConductor() { + } OverlayConductor::~OverlayConductor() { } -void OverlayConductor::update(float dt) { +bool OverlayConductor::headOutsideOverlay() const { + glm::mat4 hmdMat = qApp->getHMDSensorPose(); + glm::vec3 hmdPos = extractTranslation(hmdMat); + glm::vec3 hmdForward = transformVectorFast(hmdMat, glm::vec3(0.0f, 0.0f, -1.0f)); - updateMode(); + Transform uiTransform = qApp->getApplicationCompositor().getModelTransform(); + glm::vec3 uiPos = uiTransform.getTranslation(); + glm::vec3 uiForward = uiTransform.getRotation() * glm::vec3(0.0f, 0.0f, -1.0f); - switch (_mode) { - case SITTING: { - // when sitting, the overlay is at the origin, facing down the -z axis. - // the camera is taken directly from the HMD. - Transform identity; - qApp->getApplicationCompositor().setModelTransform(identity); - qApp->getApplicationCompositor().setCameraBaseTransform(identity); - break; - } - case STANDING: { - // when standing, the overlay is at a reference position, which is set when the overlay is - // enabled. The camera is taken directly from the HMD, but in world space. - // So the sensorToWorldMatrix must be applied. - MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - Transform t; - t.evalFromRawMatrix(myAvatar->getSensorToWorldMatrix()); - qApp->getApplicationCompositor().setCameraBaseTransform(t); - - // detect when head moves out side of sweet spot, or looks away. - mat4 headMat = myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose(); - vec3 headWorldPos = extractTranslation(headMat); - vec3 headForward = glm::quat_cast(headMat) * glm::vec3(0.0f, 0.0f, -1.0f); - Transform modelXform = qApp->getApplicationCompositor().getModelTransform(); - vec3 compositorWorldPos = modelXform.getTranslation(); - vec3 compositorForward = modelXform.getRotation() * glm::vec3(0.0f, 0.0f, -1.0f); - const float MAX_COMPOSITOR_DISTANCE = 0.6f; - const float MAX_COMPOSITOR_ANGLE = 110.0f; - if (_enabled && (glm::distance(headWorldPos, compositorWorldPos) > MAX_COMPOSITOR_DISTANCE || - glm::dot(headForward, compositorForward) < cosf(glm::radians(MAX_COMPOSITOR_ANGLE)))) { - // fade out the overlay - setEnabled(false); - } - break; - } - case FLAT: - // do nothing - break; + const float MAX_COMPOSITOR_DISTANCE = 0.99f; // If you're 1m from center of ui sphere, you're at the surface. + const float MAX_COMPOSITOR_ANGLE = 180.0f; // rotation check is effectively disabled + if (glm::distance(uiPos, hmdPos) > MAX_COMPOSITOR_DISTANCE || + glm::dot(uiForward, hmdForward) < cosf(glm::radians(MAX_COMPOSITOR_ANGLE))) { + return true; } + return false; } -void OverlayConductor::updateMode() { +bool OverlayConductor::updateAvatarIsAtRest() { + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - if (myAvatar->getClearOverlayWhenDriving()) { - float speed = glm::length(myAvatar->getVelocity()); - const float MIN_DRIVING = 0.2f; - const float MAX_NOT_DRIVING = 0.01f; - const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE = 200 * 1000; - const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE = 1000 * 1000; - bool nowDriving = _driving; // Assume current _driving mode unless... - if (speed > MIN_DRIVING) { // ... we're definitely moving... - nowDriving = true; - } else if (speed < MAX_NOT_DRIVING) { // ... or definitely not. - nowDriving = false; - } - // Check that we're in this new mode for long enough to really trigger a transition. - if (nowDriving == _driving) { // If there's no change in state, clear any attepted timer. - _timeInPotentialMode = 0; - } else if (_timeInPotentialMode == 0) { // We've just changed with no timer, so start timing now. - _timeInPotentialMode = usecTimestampNow(); - } else if ((usecTimestampNow() - _timeInPotentialMode) > (nowDriving ? REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE : REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE)) { - _timeInPotentialMode = 0; // a real transition - if (nowDriving) { - _wantsOverlays = Menu::getInstance()->isOptionChecked(MenuOption::Overlays); - } else { // reset when coming out of driving - _mode = FLAT; // Seems appropriate to let things reset, below, after the following. - // All reset of, e.g., room-scale location as though by apostrophe key, without all the other adjustments. - qApp->getActiveDisplayPlugin()->resetSensors(); - myAvatar->reset(true, false, false); - } - if (_wantsOverlays) { - setEnabled(!nowDriving, false); - } - _driving = nowDriving; - } // Else haven't accumulated enough time in new mode, but keep timing. + + const quint64 REST_ENABLE_TIME_USECS = 1000 * 1000; // 1 s + const quint64 REST_DISABLE_TIME_USECS = 200 * 1000; // 200 ms + + const float AT_REST_THRESHOLD = 0.01f; + bool desiredAtRest = glm::length(myAvatar->getVelocity()) < AT_REST_THRESHOLD; + if (desiredAtRest != _desiredAtRest) { + // start timer + _desiredAtRestTimer = usecTimestampNow() + (desiredAtRest ? REST_ENABLE_TIME_USECS : REST_DISABLE_TIME_USECS); } - Mode newMode; - if (qApp->isHMDMode()) { - newMode = SITTING; + _desiredAtRest = desiredAtRest; + + if (_desiredAtRestTimer != 0 && usecTimestampNow() > _desiredAtRestTimer) { + // timer expired + // change state! + _currentAtRest = _desiredAtRest; + // disable timer + _desiredAtRestTimer = 0; + } + + return _currentAtRest; +} + +bool OverlayConductor::updateAvatarHasDriveInput() { + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + + const quint64 DRIVE_ENABLE_TIME_USECS = 200 * 1000; // 200 ms + const quint64 DRIVE_DISABLE_TIME_USECS = 1000 * 1000; // 1 s + + bool desiredDriving = myAvatar->hasDriveInput(); + if (desiredDriving != _desiredDriving) { + // start timer + _desiredDrivingTimer = usecTimestampNow() + (desiredDriving ? DRIVE_ENABLE_TIME_USECS : DRIVE_DISABLE_TIME_USECS); + } + + _desiredDriving = desiredDriving; + + if (_desiredDrivingTimer != 0 && usecTimestampNow() > _desiredDrivingTimer) { + // timer expired + // change state! + _currentDriving = _desiredDriving; + // disable timer + _desiredDrivingTimer = 0; + } + + return _currentDriving; +} + +void OverlayConductor::centerUI() { + // place the overlay at the current hmd position in sensor space + auto camMat = cancelOutRollAndPitch(qApp->getHMDSensorPose()); + qApp->getApplicationCompositor().setModelTransform(Transform(camMat)); +} + +void OverlayConductor::update(float dt) { + auto offscreenUi = DependencyManager::get(); + bool currentVisible = !offscreenUi->getDesktop()->property("pinned").toBool(); + + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + // centerUI when hmd mode is first enabled and mounted + if (qApp->isHMDMode() && qApp->getActiveDisplayPlugin()->isDisplayVisible()) { + if (!_hmdMode) { + _hmdMode = true; + centerUI(); + } } else { - newMode = FLAT; + _hmdMode = false; } - if (newMode != _mode) { - switch (newMode) { - case SITTING: { - // enter the SITTING state - // place the overlay at origin - Transform identity; - qApp->getApplicationCompositor().setModelTransform(identity); - break; - } - case STANDING: { // STANDING mode is not currently used. - // enter the STANDING state - // place the overlay at the current hmd position in world space - auto camMat = cancelOutRollAndPitch(myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose()); - Transform t; - t.setTranslation(extractTranslation(camMat)); - t.setRotation(glm::quat_cast(camMat)); - qApp->getApplicationCompositor().setModelTransform(t); - break; - } + bool prevDriving = _currentDriving; + bool isDriving = updateAvatarHasDriveInput(); + bool drivingChanged = prevDriving != isDriving; + bool isAtRest = updateAvatarIsAtRest(); - case FLAT: - // do nothing - break; + if (_flags & SuppressedByDrive) { + if (!isDriving) { + _flags &= ~SuppressedByDrive; + } + } else { + if (myAvatar->getClearOverlayWhenMoving() && drivingChanged && isDriving) { + _flags |= SuppressedByDrive; } } - _mode = newMode; - -} - -void OverlayConductor::setEnabled(bool enabled, bool toggleQmlEvents) { - - if (enabled == _enabled) { - return; + if (_flags & SuppressedByHead) { + if (isAtRest) { + _flags &= ~SuppressedByHead; + } + } else { + if (_hmdMode && headOutsideOverlay()) { + _flags |= SuppressedByHead; + } } - if (toggleQmlEvents) { // Could recurse on us with the wrong toggleQmlEvents flag, and not need in the !toggleQmlEvent case anyway. - Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, enabled); - } - _enabled = enabled; // set the new value - - // if the new state is visible/enabled... - if (_enabled) { - // alpha fadeIn the overlay mesh. - qApp->getApplicationCompositor().fadeIn(); - - // enable mouse clicks from script - qApp->getOverlays().enable(); - - // enable QML events - if (toggleQmlEvents) { - auto offscreenUi = DependencyManager::get(); - offscreenUi->getRootItem()->setEnabled(true); - } - - if (_mode == STANDING) { - // place the overlay at the current hmd position in world space - MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - auto camMat = cancelOutRollAndPitch(myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose()); - Transform t; - t.setTranslation(extractTranslation(camMat)); - t.setRotation(glm::quat_cast(camMat)); - qApp->getApplicationCompositor().setModelTransform(t); - } - } else { // other wise, if the new state is hidden/not enabled - // alpha fadeOut the overlay mesh. - qApp->getApplicationCompositor().fadeOut(); - - // disable mouse clicks from script - qApp->getOverlays().disable(); - - // disable QML events - if (toggleQmlEvents) { // I'd really rather always do this, but it looses drive state. bugzid:501 - auto offscreenUi = DependencyManager::get(); - offscreenUi->getRootItem()->setEnabled(false); + bool targetVisible = Menu::getInstance()->isOptionChecked(MenuOption::Overlays) && (0 == (_flags & SuppressMask)); + if (targetVisible != currentVisible) { + offscreenUi->setPinned(!targetVisible); + if (targetVisible && _hmdMode) { + centerUI(); } } } - -bool OverlayConductor::getEnabled() const { - return _enabled; -} - diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index 99f4b56584..1bdfe2ed79 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -17,23 +17,31 @@ public: ~OverlayConductor(); void update(float dt); - void setEnabled(bool enable, bool toggleQmlEvents = true); - bool getEnabled() const; + void centerUI(); private: - void updateMode(); + bool headOutsideOverlay() const; + bool updateAvatarHasDriveInput(); + bool updateAvatarIsAtRest(); - enum Mode { - FLAT, - SITTING, - STANDING + enum SupressionFlags { + SuppressedByDrive = 0x01, + SuppressedByHead = 0x02, + SuppressMask = 0x03, }; - Mode _mode { FLAT }; - bool _enabled { false }; - bool _driving { false }; - quint64 _timeInPotentialMode { 0 }; - bool _wantsOverlays { true }; + uint8_t _flags { SuppressedByDrive }; + bool _hmdMode { false }; + + // used by updateAvatarHasDriveInput + quint64 _desiredDrivingTimer { 0 }; + bool _desiredDriving { false }; + bool _currentDriving { false }; + + // used by updateAvatarIsAtRest + quint64 _desiredAtRestTimer { 0 }; + bool _desiredAtRest { true }; + bool _currentAtRest { true }; }; #endif diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 6decef3240..c1705da206 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -62,9 +62,9 @@ void setupPreferences() { preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Snap turn when in HMD", getter, setter)); } { - auto getter = [=]()->bool {return myAvatar->getClearOverlayWhenDriving(); }; - auto setter = [=](bool value) { myAvatar->setClearOverlayWhenDriving(value); }; - preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when driving", getter, setter)); + auto getter = [=]()->bool {return myAvatar->getClearOverlayWhenMoving(); }; + auto setter = [=](bool value) { myAvatar->setClearOverlayWhenMoving(value); }; + preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when moving", getter, setter)); } { auto getter = []()->QString { return Snapshot::snapshotsLocation.get(); }; diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 9084fd837b..d153cfd977 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -44,6 +44,20 @@ bool AvatarHashMap::isAvatarInRange(const glm::vec3& position, const float range return false; } +int AvatarHashMap::numberOfAvatarsInRange(const glm::vec3& position, float rangeMeters) { + auto hashCopy = getHashCopy(); + auto rangeMeters2 = rangeMeters * rangeMeters; + int count = 0; + for (const AvatarSharedPointer& sharedAvatar : hashCopy) { + glm::vec3 avatarPosition = sharedAvatar->getPosition(); + auto distance2 = glm::distance2(avatarPosition, position); + if (distance2 < rangeMeters2) { + ++count; + } + } + return count; +} + AvatarSharedPointer AvatarHashMap::newSharedAvatar() { return std::make_shared(); } diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 5f58074427..9d3ebb60f5 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -39,6 +39,7 @@ public: Q_INVOKABLE AvatarData* getAvatar(QUuid avatarID); virtual AvatarSharedPointer getAvatarBySessionID(const QUuid& sessionID) { return findAvatar(sessionID); } + int numberOfAvatarsInRange(const glm::vec3& position, float rangeMeters); signals: void avatarAddedEvent(const QUuid& sessionUUID); diff --git a/libraries/controllers/src/controllers/Forward.h b/libraries/controllers/src/controllers/Forward.h index e1a62556d4..23dd162831 100644 --- a/libraries/controllers/src/controllers/Forward.h +++ b/libraries/controllers/src/controllers/Forward.h @@ -32,6 +32,7 @@ class Mapping; using MappingPointer = std::shared_ptr; using MappingList = std::list; +struct Pose; } #endif diff --git a/libraries/controllers/src/controllers/StandardControls.h b/libraries/controllers/src/controllers/StandardControls.h index d7eb3de2c2..501f97f04b 100644 --- a/libraries/controllers/src/controllers/StandardControls.h +++ b/libraries/controllers/src/controllers/StandardControls.h @@ -44,7 +44,8 @@ namespace controller { LS_TOUCH, LEFT_THUMB_UP, LS_CENTER, - LS_OUTER, + LS_X, + LS_Y, RIGHT_PRIMARY_THUMB, RIGHT_SECONDARY_THUMB, @@ -53,7 +54,8 @@ namespace controller { RS_TOUCH, RIGHT_THUMB_UP, RS_CENTER, - RS_OUTER, + RS_X, + RS_Y, LEFT_PRIMARY_INDEX, LEFT_SECONDARY_INDEX, diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index f9d527de8f..032350a07c 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -336,7 +336,9 @@ void CompositorHelper::computeHmdPickRay(const glm::vec2& cursorPos, glm::vec3& } glm::mat4 CompositorHelper::getUiTransform() const { - return _currentCamera * glm::inverse(_currentDisplayPlugin->getHeadPose()); + glm::mat4 modelMat; + _modelTransform.getMatrix(modelMat); + return _currentCamera * glm::inverse(_currentDisplayPlugin->getHeadPose()) * modelMat; } //Finds the collision point of a world space ray @@ -346,7 +348,7 @@ bool CompositorHelper::calculateRayUICollisionPoint(const glm::vec3& position, c auto relativePosition = vec3(relativePosition4) / relativePosition4.w; auto relativeDirection = glm::inverse(glm::quat_cast(UITransform)) * direction; - float uiRadius = _oculusUIRadius; // * myAvatar->getUniformScale(); // FIXME - how do we want to handle avatar scale + float uiRadius = _hmdUIRadius; // * myAvatar->getUniformScale(); // FIXME - how do we want to handle avatar scale float instersectionDistance; if (raySphereIntersect(relativeDirection, relativePosition, uiRadius, &instersectionDistance)){ @@ -407,84 +409,25 @@ void CompositorHelper::updateTooltips() { //} } -static const float FADE_DURATION = 500.0f; -static const float FADE_IN_ALPHA = 1.0f; -static const float FADE_OUT_ALPHA = 0.0f; - -void CompositorHelper::startFadeFailsafe(float endValue) { - _fadeStarted = usecTimestampNow(); - _fadeFailsafeEndValue = endValue; - - const int SLIGHT_DELAY = 10; - QTimer::singleShot(FADE_DURATION + SLIGHT_DELAY, [this]{ - checkFadeFailsafe(); - }); -} - -void CompositorHelper::checkFadeFailsafe() { - auto elapsedInFade = usecTimestampNow() - _fadeStarted; - if (elapsedInFade > FADE_DURATION) { - setAlpha(_fadeFailsafeEndValue); - } -} - -void CompositorHelper::fadeIn() { - _fadeInAlpha = true; - - _alphaPropertyAnimation->setDuration(FADE_DURATION); - _alphaPropertyAnimation->setStartValue(_alpha); - _alphaPropertyAnimation->setEndValue(FADE_IN_ALPHA); - _alphaPropertyAnimation->start(); - - // Sometimes, this "QPropertyAnimation" fails to complete the animation, and we end up with a partially faded - // state. So we will also have this fail-safe, where we record the timestamp of the fadeRequest, and the target - // value of the fade, and if after that time we still haven't faded all the way, we will kick it to the final - // fade value - startFadeFailsafe(FADE_IN_ALPHA); -} - -void CompositorHelper::fadeOut() { - _fadeInAlpha = false; - - _alphaPropertyAnimation->setDuration(FADE_DURATION); - _alphaPropertyAnimation->setStartValue(_alpha); - _alphaPropertyAnimation->setEndValue(FADE_OUT_ALPHA); - _alphaPropertyAnimation->start(); - startFadeFailsafe(FADE_OUT_ALPHA); -} - -void CompositorHelper::toggle() { - if (_fadeInAlpha) { - fadeOut(); - } else { - fadeIn(); - } -} - +// eyePose and headPosition are in sensor space. +// the resulting matrix should be in view space. glm::mat4 CompositorHelper::getReticleTransform(const glm::mat4& eyePose, const glm::vec3& headPosition) const { glm::mat4 result; if (isHMD()) { - vec3 reticleScale = vec3(Cursor::Manager::instance().getScale() * reticleSize); - auto reticlePosition = getReticlePosition(); - auto spherical = overlayToSpherical(reticlePosition); - // The pointer transform relative to the sensor - auto pointerTransform = glm::mat4_cast(quat(vec3(-spherical.y, spherical.x, 0.0f))) * glm::translate(mat4(), vec3(0, 0, -1)); - float reticleDepth = getReticleDepth(); - if (reticleDepth != 1.0f) { - // Cursor position in UI space - auto cursorPosition = vec3(pointerTransform[3]) / pointerTransform[3].w; - // Ray to the cursor, in UI space - auto cursorRay = glm::normalize(cursorPosition - headPosition) * reticleDepth; - // Move the ray to be relative to the head pose - pointerTransform[3] = vec4(cursorRay + headPosition, 1); - // Scale up the cursor because of distance - reticleScale *= reticleDepth; + vec2 spherical = overlayToSpherical(getReticlePosition()); + vec3 overlaySurfacePoint = getPoint(spherical.x, spherical.y); // overlay space + vec3 sensorSurfacePoint = _modelTransform.transform(overlaySurfacePoint); // sensor space + vec3 d = sensorSurfacePoint - headPosition; + vec3 reticlePosition; + if (glm::length(d) >= EPSILON) { + d = glm::normalize(d); + } else { + d = glm::normalize(overlaySurfacePoint); } - glm::mat4 overlayXfm; - _modelTransform.getMatrix(overlayXfm); - pointerTransform = overlayXfm * pointerTransform; - pointerTransform = glm::inverse(eyePose) * pointerTransform; - result = glm::scale(pointerTransform, reticleScale); + reticlePosition = headPosition + (d * getReticleDepth()); + quat reticleOrientation = glm::quat_cast(_currentDisplayPlugin->getHeadPose()); + vec3 reticleScale = vec3(Cursor::Manager::instance().getScale() * reticleSize * getReticleDepth()); + return glm::inverse(eyePose) * createMatFromScaleQuatAndPos(reticleScale, reticleOrientation, reticlePosition); } else { static const float CURSOR_PIXEL_SIZE = 32.0f; const auto canvasSize = vec2(toGlm(_renderingWidget->size()));; diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index c0b53b329e..2a3dd0c852 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -38,7 +38,7 @@ const float MAGNIFY_MULT = 2.0f; class CompositorHelper : public QObject, public Dependency { Q_OBJECT - Q_PROPERTY(float alpha READ getAlpha WRITE setAlpha) + Q_PROPERTY(float alpha READ getAlpha WRITE setAlpha NOTIFY alphaChanged) Q_PROPERTY(bool reticleOverDesktop READ getReticleOverDesktop WRITE setReticleOverDesktop) public: static const uvec2 VIRTUAL_SCREEN_SIZE; @@ -75,12 +75,8 @@ public: void setModelTransform(const Transform& transform) { _modelTransform = transform; } const Transform& getModelTransform() const { return _modelTransform; } - void fadeIn(); - void fadeOut(); - void toggle(); - float getAlpha() const { return _alpha; } - void setAlpha(float alpha) { _alpha = alpha; } + void setAlpha(float alpha) { if (alpha != _alpha) { emit alphaChanged(); _alpha = alpha; } } bool getReticleVisible() const { return _reticleVisible; } void setReticleVisible(bool visible) { _reticleVisible = visible; } @@ -113,10 +109,11 @@ public: void setReticleOverDesktop(bool value) { _isOverDesktop = value; } void setDisplayPlugin(const DisplayPluginPointer& displayPlugin) { _currentDisplayPlugin = displayPlugin; } - void setFrameInfo(uint32_t frame, const glm::mat4& camera) { _currentCamera = camera; _currentFrame = frame; } + void setFrameInfo(uint32_t frame, const glm::mat4& camera) { _currentCamera = camera; } signals: void allowMouseCaptureChanged(); + void alphaChanged(); protected slots: void sendFakeMouseEvent(); @@ -127,7 +124,6 @@ private: DisplayPluginPointer _currentDisplayPlugin; glm::mat4 _currentCamera; - uint32_t _currentFrame { 0 }; QWidget* _renderingWidget{ nullptr }; //// Support for hovering and tooltips @@ -144,16 +140,7 @@ private: float _textureAspectRatio { VIRTUAL_UI_ASPECT_RATIO }; float _alpha { 1.0f }; - float _prevAlpha { 1.0f }; - float _fadeInAlpha { true }; - float _oculusUIRadius { 1.0f }; - - quint64 _fadeStarted { 0 }; - float _fadeFailsafeEndValue { 1.0f }; - void checkFadeFailsafe(); - void startFadeFailsafe(float endValue); - - int _reticleQuad; + float _hmdUIRadius { 1.0f }; int _previousBorderWidth { -1 }; int _previousBorderHeight { -1 }; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 4bca48aeb0..b72f52351f 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -215,9 +215,10 @@ OpenGLDisplayPlugin::OpenGLDisplayPlugin() { } void OpenGLDisplayPlugin::cleanupForSceneTexture(const gpu::TexturePointer& sceneTexture) { - Lock lock(_mutex); - Q_ASSERT(_sceneTextureToFrameIndexMap.contains(sceneTexture)); - _sceneTextureToFrameIndexMap.remove(sceneTexture); + withRenderThreadLock([&] { + Q_ASSERT(_sceneTextureToFrameIndexMap.contains(sceneTexture)); + _sceneTextureToFrameIndexMap.remove(sceneTexture); + }); } @@ -275,6 +276,15 @@ bool OpenGLDisplayPlugin::activate() { _container->makeRenderingContextCurrent(); #endif + auto compositorHelper = DependencyManager::get(); + connect(compositorHelper.data(), &CompositorHelper::alphaChanged, [this] { + auto compositorHelper = DependencyManager::get(); + auto animation = new QPropertyAnimation(this, "overlayAlpha"); + animation->setDuration(200); + animation->setEndValue(compositorHelper->getAlpha()); + animation->start(); + }); + if (isHmd() && (getHmdScreen() >= 0)) { _container->showDisplayPluginsTools(); } @@ -284,6 +294,9 @@ bool OpenGLDisplayPlugin::activate() { void OpenGLDisplayPlugin::deactivate() { + auto compositorHelper = DependencyManager::get(); + disconnect(compositorHelper.data()); + #if THREADED_PRESENT auto presentThread = DependencyManager::get(); // Does not return until the GL transition has completeed @@ -410,10 +423,9 @@ void OpenGLDisplayPlugin::submitSceneTexture(uint32_t frameIndex, const gpu::Tex return; } - { - Lock lock(_mutex); + withRenderThreadLock([&] { _sceneTextureToFrameIndexMap[sceneTexture] = frameIndex; - } + }); // Submit it to the presentation thread via escrow _sceneTextureEscrow.submit(sceneTexture); @@ -447,11 +459,12 @@ void OpenGLDisplayPlugin::updateTextures() { } void OpenGLDisplayPlugin::updateFrameData() { - Lock lock(_mutex); - auto previousFrameIndex = _currentPresentFrameIndex; - _currentPresentFrameIndex = _sceneTextureToFrameIndexMap[_currentSceneTexture]; - auto skippedCount = (_currentPresentFrameIndex - previousFrameIndex) - 1; - _droppedFrameRate.increment(skippedCount); + withPresentThreadLock([&] { + auto previousFrameIndex = _currentPresentFrameIndex; + _currentPresentFrameIndex = _sceneTextureToFrameIndexMap[_currentSceneTexture]; + auto skippedCount = (_currentPresentFrameIndex - previousFrameIndex) - 1; + _droppedFrameRate.increment(skippedCount); + }); } void OpenGLDisplayPlugin::compositeOverlay() { @@ -460,25 +473,22 @@ void OpenGLDisplayPlugin::compositeOverlay() { auto compositorHelper = DependencyManager::get(); useProgram(_program); + // set the alpha + Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); // check the alpha - auto overlayAlpha = compositorHelper->getAlpha(); - if (overlayAlpha > 0.0f) { - // set the alpha - Uniform(*_program, _alphaUniform).Set(overlayAlpha); - - // Overlay draw - if (isStereo()) { - Uniform(*_program, _mvpUniform).Set(mat4()); - for_each_eye([&](Eye eye) { - eyeViewport(eye); - drawUnitQuad(); - }); - } else { - // Overlay draw - Uniform(*_program, _mvpUniform).Set(mat4()); + // Overlay draw + if (isStereo()) { + Uniform(*_program, _mvpUniform).Set(mat4()); + for_each_eye([&](Eye eye) { + eyeViewport(eye); drawUnitQuad(); - } + }); + } else { + // Overlay draw + Uniform(*_program, _mvpUniform).Set(mat4()); + drawUnitQuad(); } + // restore the alpha Uniform(*_program, _alphaUniform).Set(1.0); } @@ -487,23 +497,19 @@ void OpenGLDisplayPlugin::compositePointer() { auto compositorHelper = DependencyManager::get(); useProgram(_program); - // check the alpha - auto overlayAlpha = compositorHelper->getAlpha(); - if (overlayAlpha > 0.0f) { - // set the alpha - Uniform(*_program, _alphaUniform).Set(overlayAlpha); - - Uniform(*_program, _mvpUniform).Set(compositorHelper->getReticleTransform(glm::mat4())); - if (isStereo()) { - for_each_eye([&](Eye eye) { - eyeViewport(eye); - drawUnitQuad(); - }); - } else { + // set the alpha + Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); + Uniform(*_program, _mvpUniform).Set(compositorHelper->getReticleTransform(glm::mat4())); + if (isStereo()) { + for_each_eye([&](Eye eye) { + eyeViewport(eye); drawUnitQuad(); - } + }); + } else { + drawUnitQuad(); } Uniform(*_program, _mvpUniform).Set(mat4()); + // restore the alpha Uniform(*_program, _alphaUniform).Set(1.0); } @@ -523,14 +529,14 @@ void OpenGLDisplayPlugin::compositeLayers() { } _compositeFramebuffer->Bound(Framebuffer::Target::Draw, [&] { Context::Viewport(targetRenderSize.x, targetRenderSize.y); - Context::Clear().DepthBuffer(); - glBindTexture(GL_TEXTURE_2D, getSceneTextureId()); - compositeScene(); + auto sceneTextureId = getSceneTextureId(); auto overlayTextureId = getOverlayTextureId(); + glBindTexture(GL_TEXTURE_2D, sceneTextureId); + compositeScene(); if (overlayTextureId) { - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBindTexture(GL_TEXTURE_2D, overlayTextureId); + Context::Enable(Capability::Blend); + Context::BlendFunc(BlendFunction::SrcAlpha, BlendFunction::OneMinusSrcAlpha); compositeOverlay(); auto compositorHelper = DependencyManager::get(); @@ -538,11 +544,16 @@ void OpenGLDisplayPlugin::compositeLayers() { auto& cursorManager = Cursor::Manager::instance(); const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; glBindTexture(GL_TEXTURE_2D, cursorData.texture); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, overlayTextureId); compositePointer(); + glBindTexture(GL_TEXTURE_2D, 0); + glActiveTexture(GL_TEXTURE0); } glBindTexture(GL_TEXTURE_2D, 0); - glDisable(GL_BLEND); + Context::Disable(Capability::Blend); } + compositeExtra(); }); } @@ -580,7 +591,11 @@ float OpenGLDisplayPlugin::newFramePresentRate() const { } float OpenGLDisplayPlugin::droppedFrameRate() const { - return _droppedFrameRate.rate(); + float result; + withRenderThreadLock([&] { + result = _droppedFrameRate.rate(); + }); + return result; } float OpenGLDisplayPlugin::presentRate() const { @@ -695,3 +710,18 @@ void OpenGLDisplayPlugin::useProgram(const ProgramPtr& program) { _activeProgram = program; } } + +void OpenGLDisplayPlugin::assertIsRenderThread() const { + Q_ASSERT(QThread::currentThread() != _presentThread); +} + +void OpenGLDisplayPlugin::assertIsPresentThread() const { + Q_ASSERT(QThread::currentThread() == _presentThread); +} + +bool OpenGLDisplayPlugin::beginFrameRender(uint32_t frameIndex) { + withRenderThreadLock([&] { + _compositeOverlayAlpha = _overlayAlpha; + }); + return Parent::beginFrameRender(frameIndex); +} diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index aa3699584a..068b236289 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -24,7 +24,9 @@ #define THREADED_PRESENT 1 class OpenGLDisplayPlugin : public DisplayPlugin { - + Q_OBJECT + Q_PROPERTY(float overlayAlpha MEMBER _overlayAlpha) + using Parent = DisplayPlugin; protected: using Mutex = std::mutex; using Lock = std::unique_lock; @@ -61,6 +63,7 @@ public: float droppedFrameRate() const override; + bool beginFrameRender(uint32_t frameIndex) override; protected: #if THREADED_PRESENT friend class PresentThread; @@ -75,6 +78,7 @@ protected: virtual void compositeScene(); virtual void compositeOverlay(); virtual void compositePointer(); + virtual void compositeExtra() {}; virtual bool hasFocus() const override; @@ -110,12 +114,12 @@ protected: int32_t _alphaUniform { -1 }; ShapeWrapperPtr _plane; - mutable Mutex _mutex; RateCounter<> _droppedFrameRate; RateCounter<> _newFrameRate; RateCounter<> _presentRate; QMap _sceneTextureToFrameIndexMap; uint32_t _currentPresentFrameIndex { 0 }; + float _compositeOverlayAlpha{ 1.0f }; gpu::TexturePointer _currentSceneTexture; gpu::TexturePointer _currentOverlayTexture; @@ -136,10 +140,28 @@ protected: BasicFramebufferWrapperPtr _compositeFramebuffer; bool _lockCurrentTexture { false }; + void assertIsRenderThread() const; + void assertIsPresentThread() const; + + template + void withPresentThreadLock(F f) const { + assertIsPresentThread(); + Lock lock(_presentMutex); + f(); + } + + template + void withRenderThreadLock(F f) const { + assertIsRenderThread(); + Lock lock(_presentMutex); + f(); + } private: - using Parent = DisplayPlugin; + // Any resource shared by the main thread and the presentation thread must + // be serialized through this mutex + mutable Mutex _presentMutex; ProgramPtr _activeProgram; + float _overlayAlpha{ 1.0f }; }; - diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index b29348f646..f1aa1edc81 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -37,7 +38,6 @@ QRect HmdDisplayPlugin::getRecommendedOverlayRect() const { return CompositorHelper::VIRTUAL_SCREEN_RECOMMENDED_OVERLAY_RECT; } - bool HmdDisplayPlugin::internalActivate() { _monoPreview = _container->getBoolSetting("monoPreview", DEFAULT_MONO_VIEW); @@ -197,14 +197,43 @@ static ProgramPtr getReprojectionProgram() { #endif +static const char * LASER_VS = R"VS(#version 410 core +uniform mat4 mvp = mat4(1); + +in vec3 Position; + +out vec3 vPosition; + +void main() { + gl_Position = mvp * vec4(Position, 1); + vPosition = Position; +} + +)VS"; + +static const char * LASER_FS = R"FS(#version 410 core + +uniform vec4 color = vec4(1.0, 1.0, 1.0, 1.0); +in vec3 vPosition; + +out vec4 FragColor; + +void main() { + FragColor = color; +} + +)FS"; + void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); // Only enable mirroring if we know vsync is disabled enableVsync(false); _enablePreview = !isVsyncEnabled(); _sphereSection = loadSphereSection(_program, CompositorHelper::VIRTUAL_UI_TARGET_FOV.y, CompositorHelper::VIRTUAL_UI_ASPECT_RATIO); + compileProgram(_laserProgram, LASER_VS, LASER_FS); + _laserGeometry = loadLaser(_laserProgram); compileProgram(_reprojectionProgram, REPROJECTION_VS, REPROJECTION_FS); - + using namespace oglplus; REPROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "reprojection").Location(); INVERSE_PROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "inverseProjections").Location(); @@ -215,6 +244,8 @@ void HmdDisplayPlugin::uncustomizeContext() { _sphereSection.reset(); _compositeFramebuffer.reset(); _reprojectionProgram.reset(); + _laserProgram.reset(); + _laserGeometry.reset(); Parent::uncustomizeContext(); } @@ -253,23 +284,20 @@ void HmdDisplayPlugin::compositeScene() { void HmdDisplayPlugin::compositeOverlay() { using namespace oglplus; auto compositorHelper = DependencyManager::get(); + glm::mat4 modelMat = compositorHelper->getModelTransform().getMatrix(); - // check the alpha useProgram(_program); - auto overlayAlpha = compositorHelper->getAlpha(); - if (overlayAlpha > 0.0f) { - // set the alpha - Uniform(*_program, _alphaUniform).Set(overlayAlpha); - - _sphereSection->Use(); - for_each_eye([&](Eye eye) { - eyeViewport(eye); - auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)); - auto mvp = _eyeProjections[eye] * modelView; - Uniform(*_program, _mvpUniform).Set(mvp); - _sphereSection->Draw(); - }); - } + // set the alpha + Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); + _sphereSection->Use(); + for_each_eye([&](Eye eye) { + eyeViewport(eye); + auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)) * modelMat; + auto mvp = _eyeProjections[eye] * modelView; + Uniform(*_program, _mvpUniform).Set(mvp); + _sphereSection->Draw(); + }); + // restore the alpha Uniform(*_program, _alphaUniform).Set(1.0); } @@ -278,29 +306,27 @@ void HmdDisplayPlugin::compositePointer() { auto compositorHelper = DependencyManager::get(); - // check the alpha useProgram(_program); - auto overlayAlpha = compositorHelper->getAlpha(); - if (overlayAlpha > 0.0f) { - // set the alpha - Uniform(*_program, _alphaUniform).Set(overlayAlpha); + // set the alpha + Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); - // Mouse pointer - _plane->Use(); - // Reconstruct the headpose from the eye poses - auto headPosition = vec3(_currentPresentFrameInfo.presentPose[3]); - for_each_eye([&](Eye eye) { - eyeViewport(eye); - auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); - auto reticleTransform = compositorHelper->getReticleTransform(eyePose, headPosition); - auto mvp = _eyeProjections[eye] * reticleTransform; - Uniform(*_program, _mvpUniform).Set(mvp); - _plane->Draw(); - }); - } + // Mouse pointer + _plane->Use(); + // Reconstruct the headpose from the eye poses + auto headPosition = vec3(_currentPresentFrameInfo.presentPose[3]); + for_each_eye([&](Eye eye) { + eyeViewport(eye); + auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); + auto reticleTransform = compositorHelper->getReticleTransform(eyePose, headPosition); + auto mvp = _eyeProjections[eye] * reticleTransform; + Uniform(*_program, _mvpUniform).Set(mvp); + _plane->Draw(); + }); + // restore the alpha Uniform(*_program, _alphaUniform).Set(1.0); } + void HmdDisplayPlugin::internalPresent() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) @@ -357,22 +383,117 @@ void HmdDisplayPlugin::setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm: void HmdDisplayPlugin::updateFrameData() { // Check if we have old frame data to discard - { - Lock lock(_mutex); + withPresentThreadLock([&] { auto itr = _frameInfos.find(_currentPresentFrameIndex); if (itr != _frameInfos.end()) { _frameInfos.erase(itr); } - } + }); Parent::updateFrameData(); - { - Lock lock(_mutex); + withPresentThreadLock([&] { _currentPresentFrameInfo = _frameInfos[_currentPresentFrameIndex]; - } + }); } glm::mat4 HmdDisplayPlugin::getHeadPose() const { return _currentRenderFrameInfo.renderPose; } + +bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const vec4& color, const vec3& direction) { + HandLaserInfo info; + info.mode = mode; + info.color = color; + info.direction = direction; + withRenderThreadLock([&] { + if (hands & Hand::LeftHand) { + _handLasers[0] = info; + } + if (hands & Hand::RightHand) { + _handLasers[1] = info; + } + }); + // FIXME defer to a child class plugin to determine if hand lasers are actually + // available based on the presence or absence of hand controllers + return true; +} + +void HmdDisplayPlugin::compositeExtra() { + const int NUMBER_OF_HANDS = 2; + std::array handLasers; + std::array renderHandPoses; + Transform uiModelTransform; + withPresentThreadLock([&] { + handLasers = _handLasers; + renderHandPoses = _handPoses; + uiModelTransform = _uiModelTransform; + }); + + // If neither hand laser is activated, exit + if (!handLasers[0].valid() && !handLasers[1].valid()) { + return; + } + + static const glm::mat4 identity; + if (renderHandPoses[0] == identity && renderHandPoses[1] == identity) { + return; + } + + // Render hand lasers + using namespace oglplus; + useProgram(_laserProgram); + _laserGeometry->Use(); + std::array handLaserModelMatrices; + + for (int i = 0; i < NUMBER_OF_HANDS; ++i) { + if (renderHandPoses[i] == identity) { + continue; + } + const auto& handLaser = handLasers[i]; + if (!handLaser.valid()) { + continue; + } + + const auto& laserDirection = handLaser.direction; + auto model = renderHandPoses[i]; + auto castDirection = glm::quat_cast(model) * laserDirection; + if (glm::abs(glm::length2(castDirection) - 1.0f) > EPSILON) { + castDirection = glm::normalize(castDirection); + } + + // FIXME fetch the actual UI radius from... somewhere? + float uiRadius = 1.0f; + + // Find the intersection of the laser with he UI and use it to scale the model matrix + float distance; + if (!glm::intersectRaySphere(vec3(renderHandPoses[i][3]), castDirection, uiModelTransform.getTranslation(), uiRadius * uiRadius, distance)) { + continue; + } + + // Make sure we rotate to match the desired laser direction + if (laserDirection != Vectors::UNIT_NEG_Z) { + auto rotation = glm::rotation(Vectors::UNIT_NEG_Z, laserDirection); + model = model * glm::mat4_cast(rotation); + } + + model = glm::scale(model, vec3(distance)); + handLaserModelMatrices[i] = model; + } + + for_each_eye([&](Eye eye) { + eyeViewport(eye); + auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); + auto view = glm::inverse(eyePose); + const auto& projection = _eyeProjections[eye]; + for (int i = 0; i < NUMBER_OF_HANDS; ++i) { + if (handLaserModelMatrices[i] == identity) { + continue; + } + Uniform(*_laserProgram, "mvp").Set(projection * view * handLaserModelMatrices[i]); + Uniform(*_laserProgram, "color").Set(handLasers[i].color); + _laserGeometry->Draw(); + // TODO render some kind of visual indicator at the intersection point with the UI. + } + }); +} diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index e6ceb7e376..fada15d864 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -10,6 +10,7 @@ #include #include +#include #include "../OpenGLDisplayPlugin.h" @@ -30,7 +31,7 @@ public: virtual glm::mat4 getHeadPose() const override; - + bool setHandLaser(uint32_t hands, HandLaserMode mode, const vec4& color, const vec3& direction) override; protected: virtual void hmdPresent() = 0; @@ -46,7 +47,22 @@ protected: void customizeContext() override; void uncustomizeContext() override; void updateFrameData() override; + void compositeExtra() override; + struct HandLaserInfo { + HandLaserMode mode { HandLaserMode::None }; + vec4 color { 1.0f }; + vec3 direction { 0, 0, -1 }; + + // Is this hand laser info suitable for drawing? + bool valid() const { + return (mode != HandLaserMode::None && color.a > 0.0f && direction != vec3()); + } + }; + + Transform _uiModelTransform; + std::array _handLasers; + std::array _handPoses; std::array _eyeOffsets; std::array _eyeProjections; std::array _eyeInverseProjections; @@ -75,5 +91,7 @@ private: bool _enableReprojection { true }; ShapeWrapperPtr _sphereSection; ProgramPtr _reprojectionProgram; + ProgramPtr _laserProgram; + ShapeWrapperPtr _laserGeometry; }; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 7b3b3c3efe..366e365107 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -593,23 +593,25 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { // the model is still being downloaded. return false; + } else if (type == SHAPE_TYPE_STATIC_MESH) { + return (_model && _model->isLoaded()); } return true; } void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { ShapeType type = getShapeType(); + glm::vec3 dimensions = getDimensions(); if (type == SHAPE_TYPE_COMPOUND) { updateModelBounds(); // should never fall in here when collision model not fully loaded // hence we assert that all geometries exist and are loaded assert(_model->isLoaded() && _model->isCollisionLoaded()); - const FBXGeometry& renderGeometry = _model->getFBXGeometry(); const FBXGeometry& collisionGeometry = _model->getCollisionFBXGeometry(); - QVector>& points = info.getPoints(); - points.clear(); + ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); + pointCollection.clear(); uint32_t i = 0; // the way OBJ files get read, each section under a "g" line is its own meshPart. We only expect @@ -619,8 +621,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { foreach (const FBXMesh& mesh, collisionGeometry.meshes) { // each meshPart is a convex hull foreach (const FBXMeshPart &meshPart, mesh.parts) { - points.push_back(QVector()); - QVector& pointsInPart = points[i]; + pointCollection.push_back(QVector()); + ShapeInfo::PointList& pointsInPart = pointCollection[i]; // run through all the triangles and (uniquely) add each point to the hull uint32_t numIndices = (uint32_t)meshPart.triangleIndices.size(); @@ -664,7 +666,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { if (pointsInPart.size() == 0) { qCDebug(entitiesrenderer) << "Warning -- meshPart has no faces"; - points.pop_back(); + pointCollection.pop_back(); continue; } ++i; @@ -677,29 +679,136 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // to the visual model and apply them to the collision model (without regard for the // collision model's extents). - glm::vec3 scale = getDimensions() / renderGeometry.getUnscaledMeshExtents().size(); + glm::vec3 scaleToFit = dimensions / _model->getFBXGeometry().getUnscaledMeshExtents().size(); // multiply each point by scale before handing the point-set off to the physics engine. // also determine the extents of the collision model. - AABox box; - for (int i = 0; i < points.size(); i++) { - for (int j = 0; j < points[i].size(); j++) { + for (int i = 0; i < pointCollection.size(); i++) { + for (int j = 0; j < pointCollection[i].size(); j++) { // compensate for registration - points[i][j] += _model->getOffset(); + pointCollection[i][j] += _model->getOffset(); // scale so the collision points match the model points - points[i][j] *= scale; - // this next subtraction is done so we can give info the offset, which will cause - // the shape-key to change. - points[i][j] -= _model->getOffset(); - box += points[i][j]; + pointCollection[i][j] *= scaleToFit; + } + } + info.setParams(type, dimensions, _compoundShapeURL); + } else if (type == SHAPE_TYPE_STATIC_MESH) { + // compute meshPart local transforms + QVector localTransforms; + const FBXGeometry& geometry = _model->getFBXGeometry(); + int numberOfMeshes = geometry.meshes.size(); + for (int i = 0; i < numberOfMeshes; i++) { + const FBXMesh& mesh = geometry.meshes.at(i); + if (mesh.clusters.size() > 0) { + const FBXCluster& cluster = mesh.clusters.at(0); + auto jointMatrix = _model->getRig()->getJointTransform(cluster.jointIndex); + localTransforms.push_back(jointMatrix * cluster.inverseBindMatrix); + } else { + glm::mat4 identity; + localTransforms.push_back(identity); } } - glm::vec3 collisionModelDimensions = box.getDimensions(); - info.setParams(type, collisionModelDimensions, _compoundShapeURL); - info.setOffset(_model->getOffset()); + updateModelBounds(); + + // should never fall in here when collision model not fully loaded + assert(_model->isLoaded()); + + ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); + pointCollection.clear(); + + ShapeInfo::PointList points; + ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); + auto& meshes = _model->getGeometry()->getGeometry()->getMeshes(); + + Extents extents; + int meshCount = 0; + for (auto& mesh : meshes) { + const gpu::BufferView& vertices = mesh->getVertexBuffer(); + const gpu::BufferView& indices = mesh->getIndexBuffer(); + const gpu::BufferView& parts = mesh->getPartBuffer(); + + // copy points + const glm::mat4& localTransform = localTransforms[meshCount]; + uint32_t meshIndexOffset = (uint32_t)points.size(); + gpu::BufferView::Iterator vertexItr = vertices.cbegin(); + points.reserve((int32_t)((gpu::Size)points.size() + vertices.getNumElements())); + while (vertexItr != vertices.cend()) { + glm::vec3 point = extractTranslation(localTransform * glm::translate(*vertexItr)); + points.push_back(point); + extents.addPoint(point); + ++vertexItr; + } + + // copy triangleIndices + triangleIndices.reserve((int32_t)((gpu::Size)(triangleIndices.size()) + indices.getNumElements())); + gpu::BufferView::Iterator partItr = parts.cbegin(); + while (partItr != parts.cend()) { + + if (partItr->_topology == model::Mesh::TRIANGLES) { + assert(partItr->_numIndices % 3 == 0); + auto indexItr = indices.cbegin() + partItr->_startIndex; + auto indexEnd = indexItr + partItr->_numIndices; + while (indexItr != indexEnd) { + triangleIndices.push_back(*indexItr + meshIndexOffset); + ++indexItr; + } + } else if (partItr->_topology == model::Mesh::TRIANGLE_STRIP) { + assert(partItr->_numIndices > 2); + uint32_t approxNumIndices = 3 * partItr->_numIndices; + if (approxNumIndices > (uint32_t)(triangleIndices.capacity() - triangleIndices.size())) { + // we underestimated the final size of triangleIndices so we pre-emptively expand it + triangleIndices.reserve(triangleIndices.size() + approxNumIndices); + } + + auto indexItr = indices.cbegin() + partItr->_startIndex; + auto indexEnd = indexItr + (partItr->_numIndices - 2); + + // first triangle uses the first three indices + triangleIndices.push_back(*(indexItr++) + meshIndexOffset); + triangleIndices.push_back(*(indexItr++) + meshIndexOffset); + triangleIndices.push_back(*(indexItr++) + meshIndexOffset); + + // the rest use previous and next index + uint32_t triangleCount = 1; + while (indexItr != indexEnd) { + if ((*indexItr) != model::Mesh::PRIMITIVE_RESTART_INDEX) { + if (triangleCount % 2 == 0) { + // even triangles use first two indices in order + triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset); + triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset); + } else { + // odd triangles swap order of first two indices + triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset); + triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset); + } + triangleIndices.push_back(*indexItr + meshIndexOffset); + ++triangleCount; + } + ++indexItr; + } + } + ++partItr; + } + ++meshCount; + } + + // scale and shift + glm::vec3 extentsSize = extents.size(); + glm::vec3 scaleToFit = dimensions / extentsSize; + for (int i = 0; i < 3; ++i) { + if (extentsSize[i] < 1.0e-6f) { + scaleToFit[i] = 1.0f; + } + } + for (int i = 0; i < points.size(); ++i) { + points[i] = (points[i] * scaleToFit); + } + + pointCollection.push_back(points); + info.setParams(SHAPE_TYPE_STATIC_MESH, 0.5f * dimensions, _modelURL); } else { ModelEntityItem::computeShapeInfo(info); - info.setParams(type, 0.5f * getDimensions()); + info.setParams(type, 0.5f * dimensions); adjustShapeInfoByRegistration(info); } } diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index ad35a1a00c..eb6db2874f 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1198,7 +1198,7 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { QtConcurrent::run([entity, voxelSurfaceStyle, voxelVolumeSize, mesh] { auto polyVoxEntity = std::static_pointer_cast(entity); - QVector> points; + QVector> pointCollection; AABox box; glm::mat4 vtoM = std::static_pointer_cast(entity)->voxelToLocalMatrix(); @@ -1207,7 +1207,7 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { // pull each triangle in the mesh into a polyhedron which can be collided with unsigned int i = 0; - const gpu::BufferView vertexBufferView = mesh->getVertexBuffer(); + const gpu::BufferView& vertexBufferView = mesh->getVertexBuffer(); const gpu::BufferView& indexBufferView = mesh->getIndexBuffer(); gpu::BufferView::Iterator it = indexBufferView.cbegin(); @@ -1241,9 +1241,9 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { pointsInPart << p3Model; // add next convex hull QVector newMeshPoints; - points << newMeshPoints; + pointCollection << newMeshPoints; // add points to the new convex hull - points[i++] << pointsInPart; + pointCollection[i++] << pointsInPart; } } else { unsigned int i = 0; @@ -1299,19 +1299,19 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { // add next convex hull QVector newMeshPoints; - points << newMeshPoints; + pointCollection << newMeshPoints; // add points to the new convex hull - points[i++] << pointsInPart; + pointCollection[i++] << pointsInPart; } }); } - polyVoxEntity->setCollisionPoints(points, box); + polyVoxEntity->setCollisionPoints(pointCollection, box); }); } -void RenderablePolyVoxEntityItem::setCollisionPoints(const QVector> points, AABox box) { +void RenderablePolyVoxEntityItem::setCollisionPoints(ShapeInfo::PointCollection pointCollection, AABox box) { // this catches the payload from computeShapeInfoWorker - if (points.isEmpty()) { + if (pointCollection.isEmpty()) { EntityItem::computeShapeInfo(_shapeInfo); return; } @@ -1325,7 +1325,7 @@ void RenderablePolyVoxEntityItem::setCollisionPoints(const QVector thunk); void setMesh(model::MeshPointer mesh); - void setCollisionPoints(const QVector> points, AABox box); + void setCollisionPoints(ShapeInfo::PointCollection points, AABox box); PolyVox::SimpleVolume* getVolData() { return _volData; } uint8_t getVoxelInternal(int x, int y, int z); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 64b6a2c655..f0a4d40860 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1602,14 +1602,20 @@ void EntityItem::updateMass(float mass) { void EntityItem::updateVelocity(const glm::vec3& value) { glm::vec3 velocity = getLocalVelocity(); if (velocity != value) { - const float MIN_LINEAR_SPEED = 0.001f; - if (glm::length(value) < MIN_LINEAR_SPEED) { - velocity = ENTITY_ITEM_ZERO_VEC3; + if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { + if (velocity != Vectors::ZERO) { + setLocalVelocity(Vectors::ZERO); + } } else { - velocity = value; + const float MIN_LINEAR_SPEED = 0.001f; + if (glm::length(value) < MIN_LINEAR_SPEED) { + velocity = ENTITY_ITEM_ZERO_VEC3; + } else { + velocity = value; + } + setLocalVelocity(velocity); + _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; } - setLocalVelocity(velocity); - _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; } } @@ -1630,22 +1636,30 @@ void EntityItem::updateDamping(float value) { void EntityItem::updateGravity(const glm::vec3& value) { if (_gravity != value) { - _gravity = value; - _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; + if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { + _gravity = Vectors::ZERO; + } else { + _gravity = value; + _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; + } } } void EntityItem::updateAngularVelocity(const glm::vec3& value) { glm::vec3 angularVelocity = getLocalAngularVelocity(); if (angularVelocity != value) { - const float MIN_ANGULAR_SPEED = 0.0002f; - if (glm::length(value) < MIN_ANGULAR_SPEED) { - angularVelocity = ENTITY_ITEM_ZERO_VEC3; + if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { + setLocalAngularVelocity(Vectors::ZERO); } else { - angularVelocity = value; + const float MIN_ANGULAR_SPEED = 0.0002f; + if (glm::length(value) < MIN_ANGULAR_SPEED) { + angularVelocity = ENTITY_ITEM_ZERO_VEC3; + } else { + angularVelocity = value; + } + setLocalAngularVelocity(angularVelocity); + _dirtyFlags |= Simulation::DIRTY_ANGULAR_VELOCITY; } - setLocalAngularVelocity(angularVelocity); - _dirtyFlags |= Simulation::DIRTY_ANGULAR_VELOCITY; } } @@ -1679,9 +1693,17 @@ void EntityItem::updateCollisionMask(uint8_t value) { } void EntityItem::updateDynamic(bool value) { - if (_dynamic != value) { - _dynamic = value; - _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + if (getDynamic() != value) { + // dynamic and STATIC_MESH are incompatible so we check for that case + if (value && getShapeType() == SHAPE_TYPE_STATIC_MESH) { + if (_dynamic) { + _dynamic = false; + _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + } + } else { + _dynamic = value; + _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + } } } @@ -1731,7 +1753,7 @@ void EntityItem::computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask group = BULLET_COLLISION_GROUP_COLLISIONLESS; mask = 0; } else { - if (_dynamic) { + if (getDynamic()) { group = BULLET_COLLISION_GROUP_DYNAMIC; } else if (isMovingRelativeToParent() || hasActions()) { group = BULLET_COLLISION_GROUP_KINEMATIC; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 4a691462ab..9fa13690f1 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -283,7 +283,7 @@ public: void computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask) const; - bool getDynamic() const { return _dynamic; } + bool getDynamic() const { return SHAPE_TYPE_STATIC_MESH == getShapeType() ? false : _dynamic; } void setDynamic(bool value) { _dynamic = value; } virtual bool shouldBePhysical() const { return false; } @@ -348,7 +348,7 @@ public: void updateDynamic(bool value); void updateLifetime(float value); void updateCreated(uint64_t value); - virtual void updateShapeType(ShapeType type) { /* do nothing */ } + virtual void setShapeType(ShapeType type) { /* do nothing */ } uint32_t getDirtyFlags() const { return _dirtyFlags; } void clearDirtyFlags(uint32_t mask = 0xffffffff) { _dirtyFlags &= ~mask; } diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 89bf9f1a21..a62f4b182a 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -88,8 +88,21 @@ void EntityItemProperties::setLastEdited(quint64 usecTime) { _lastEdited = usecTime > _created ? usecTime : _created; } -const char* shapeTypeNames[] = {"none", "box", "sphere", "plane", "compound", "capsule-x", - "capsule-y", "capsule-z", "cylinder-x", "cylinder-y", "cylinder-z"}; +const char* shapeTypeNames[] = { + "none", + "box", + "sphere", + "capsule-x", + "capsule-y", + "capsule-z", + "cylinder-x", + "cylinder-y", + "cylinder-z", + "hull", + "plane", + "compound", + "static-mesh" +}; QHash stringToShapeTypeLookup; @@ -101,14 +114,16 @@ void buildStringToShapeTypeLookup() { addShapeType(SHAPE_TYPE_NONE); addShapeType(SHAPE_TYPE_BOX); addShapeType(SHAPE_TYPE_SPHERE); - addShapeType(SHAPE_TYPE_PLANE); - addShapeType(SHAPE_TYPE_COMPOUND); addShapeType(SHAPE_TYPE_CAPSULE_X); addShapeType(SHAPE_TYPE_CAPSULE_Y); addShapeType(SHAPE_TYPE_CAPSULE_Z); addShapeType(SHAPE_TYPE_CYLINDER_X); addShapeType(SHAPE_TYPE_CYLINDER_Y); addShapeType(SHAPE_TYPE_CYLINDER_Z); + addShapeType(SHAPE_TYPE_HULL); + addShapeType(SHAPE_TYPE_PLANE); + addShapeType(SHAPE_TYPE_COMPOUND); + addShapeType(SHAPE_TYPE_STATIC_MESH); } QString getCollisionGroupAsString(uint8_t group) { diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 40faf2c3c3..8e925b2f79 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -77,7 +77,7 @@ bool ModelEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(modelURL, setModelURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(textures, setTextures); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, updateShapeType); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointRotationsSet, setJointRotationsSet); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointRotations, setJointRotations); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointTranslationsSet, setJointTranslationsSet); @@ -145,7 +145,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, dataAt += bytesFromAnimation; } - READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, updateShapeType); + READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, setShapeType); if (animationPropertiesChanged) { _dirtyFlags |= Simulation::DIRTY_UPDATEABLE; @@ -257,37 +257,54 @@ void ModelEntityItem::debugDump() const { qCDebug(entities) << " compound shape URL:" << getCompoundShapeURL(); } -void ModelEntityItem::updateShapeType(ShapeType type) { - // BEGIN_TEMPORARY_WORKAROUND - // we have allowed inconsistent ShapeType's to be stored in SVO files in the past (this was a bug) - // but we are now enforcing the entity properties to be consistent. To make the possible we're - // introducing a temporary workaround: we will ignore ShapeType updates that conflict with the - // _compoundShapeURL. - if (hasCompoundShapeURL()) { - type = SHAPE_TYPE_COMPOUND; - } - // END_TEMPORARY_WORKAROUND - +void ModelEntityItem::setShapeType(ShapeType type) { if (type != _shapeType) { + if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { + // dynamic and STATIC_MESH are incompatible + // since the shape is being set here we clear the dynamic bit + _dynamic = false; + _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + } _shapeType = type; _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; } } -// virtual ShapeType ModelEntityItem::getShapeType() const { - if (_shapeType == SHAPE_TYPE_COMPOUND) { - return hasCompoundShapeURL() ? SHAPE_TYPE_COMPOUND : SHAPE_TYPE_NONE; - } else { - return _shapeType; + return computeTrueShapeType(); +} + +ShapeType ModelEntityItem::computeTrueShapeType() const { + ShapeType type = _shapeType; + if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { + // dynamic is incompatible with STATIC_MESH + // shouldn't fall in here but just in case --> fall back to COMPOUND + type = SHAPE_TYPE_COMPOUND; + } + if (type == SHAPE_TYPE_COMPOUND && !hasCompoundShapeURL()) { + // no compoundURL set --> fall back to NONE + type = SHAPE_TYPE_NONE; + } + return type; +} + +void ModelEntityItem::setModelURL(const QString& url) { + if (_modelURL != url) { + _modelURL = url; + _parsedModelURL = QUrl(url); + if (_shapeType == SHAPE_TYPE_STATIC_MESH) { + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + } } } void ModelEntityItem::setCompoundShapeURL(const QString& url) { if (_compoundShapeURL != url) { + ShapeType oldType = computeTrueShapeType(); _compoundShapeURL = url; - _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; - _shapeType = _compoundShapeURL.isEmpty() ? SHAPE_TYPE_NONE : SHAPE_TYPE_COMPOUND; + if (oldType != computeTrueShapeType()) { + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + } } } diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 29730bf4df..7b7edaf945 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -50,9 +50,10 @@ public: virtual bool needsToCallUpdate() const; virtual void debugDump() const; - void updateShapeType(ShapeType type); + void setShapeType(ShapeType type); virtual ShapeType getShapeType() const; + // TODO: Move these to subclasses, or other appropriate abstraction // getters/setters applicable to models and particles @@ -76,7 +77,7 @@ public: } // model related properties - virtual void setModelURL(const QString& url) { _modelURL = url; _parsedModelURL = QUrl(url); } + virtual void setModelURL(const QString& url); virtual void setCompoundShapeURL(const QString& url); // Animation related items... @@ -130,6 +131,7 @@ public: private: void setAnimationSettings(const QString& value); // only called for old bitstream format + ShapeType computeTrueShapeType() const; protected: // these are used: diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index a7bd0038e6..c501737146 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -342,7 +342,7 @@ bool ParticleEffectEntityItem::setProperties(const EntityItemProperties& propert SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); SET_ENTITY_PROPERTY_FROM_PROPERTIES(alpha, setAlpha); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, updateShapeType); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(maxParticles, setMaxParticles); SET_ENTITY_PROPERTY_FROM_PROPERTIES(lifespan, setLifespan); SET_ENTITY_PROPERTY_FROM_PROPERTIES(isEmitting, setIsEmitting); @@ -406,7 +406,7 @@ int ParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned ch READ_ENTITY_PROPERTY(PROP_EMITTING_PARTICLES, bool, setIsEmitting); } - READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, updateShapeType); + READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, setShapeType); READ_ENTITY_PROPERTY(PROP_MAX_PARTICLES, quint32, setMaxParticles); READ_ENTITY_PROPERTY(PROP_LIFESPAN, float, setLifespan); READ_ENTITY_PROPERTY(PROP_EMIT_RATE, float, setEmitRate); @@ -584,7 +584,7 @@ void ParticleEffectEntityItem::debugDump() const { qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); } -void ParticleEffectEntityItem::updateShapeType(ShapeType type) { +void ParticleEffectEntityItem::setShapeType(ShapeType type) { if (type != _shapeType) { _shapeType = type; _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index 4538a1bb43..9ddda62c8b 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -95,7 +95,7 @@ public: void setAlphaSpread(float alphaSpread); float getAlphaSpread() const { return _alphaSpread; } - void updateShapeType(ShapeType type); + void setShapeType(ShapeType type); virtual ShapeType getShapeType() const { return _shapeType; } virtual void debugDump() const; diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index a28b8210c2..0b99d0377f 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -73,7 +73,7 @@ bool ZoneEntityItem::setProperties(const EntityItemProperties& properties) { bool somethingChangedInStage = _stageProperties.setProperties(properties); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, updateShapeType); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(backgroundMode, setBackgroundMode); @@ -117,7 +117,7 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, bytesRead += bytesFromStage; dataAt += bytesFromStage; - READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, updateShapeType); + READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, setShapeType); READ_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); READ_ENTITY_PROPERTY(PROP_BACKGROUND_MODE, BackgroundMode, setBackgroundMode); diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 56968aa9c9..f0f2a91d63 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -55,7 +55,7 @@ public: static void setDrawZoneBoundaries(bool value) { _drawZoneBoundaries = value; } virtual bool isReadyToComputeShape() { return false; } - void updateShapeType(ShapeType type) { _shapeType = type; } + void setShapeType(ShapeType type) { _shapeType = type; } virtual ShapeType getShapeType() const; virtual bool hasCompoundShapeURL() const { return !_compoundShapeURL.isEmpty(); } diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index 302e0b8515..79b39a2331 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -5,6 +5,7 @@ #include #include #include +#include const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { static QSurfaceFormat format; @@ -39,6 +40,13 @@ const QGLFormat& getDefaultGLFormat() { return glFormat; } +int glVersionToInteger(QString glVersion) { + QStringList versionParts = glVersion.split(QRegularExpression("[\\.\\s]")); + int majorNumber = versionParts[0].toInt(); + int minorNumber = versionParts[1].toInt(); + return majorNumber * 100 + minorNumber * 10; +} + QJsonObject getGLContextData() { if (!QOpenGLContext::currentContext()) { return QJsonObject(); diff --git a/libraries/gl/src/gl/GLHelpers.h b/libraries/gl/src/gl/GLHelpers.h index ddb254f1c5..477bf7abc8 100644 --- a/libraries/gl/src/gl/GLHelpers.h +++ b/libraries/gl/src/gl/GLHelpers.h @@ -27,5 +27,6 @@ void setGLFormatVersion(F& format, int major = 4, int minor = 5) { format.setVer const QSurfaceFormat& getDefaultOpenGLSurfaceFormat(); const QGLFormat& getDefaultGLFormat(); QJsonObject getGLContextData(); +int glVersionToInteger(QString glVersion); #endif diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 388ca26482..14518ac37a 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -396,6 +396,8 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { _renderer->_renderControl->_renderWindow = _proxyWindow; + connect(_renderer->_quickWindow, &QQuickWindow::focusObjectChanged, this, &OffscreenQmlSurface::onFocusObjectChanged); + // Create a QML engine. _qmlEngine = new QQmlEngine; if (!_qmlEngine->incubationController()) { @@ -742,3 +744,21 @@ QVariant OffscreenQmlSurface::returnFromUiThread(std::function funct return function(); } + +void OffscreenQmlSurface::onFocusObjectChanged(QObject* object) { + if (!object) { + setFocusText(false); + return; + } + + QInputMethodQueryEvent query(Qt::ImEnabled); + qApp->sendEvent(object, &query); + setFocusText(query.value(Qt::ImEnabled).toBool()); +} + +void OffscreenQmlSurface::setFocusText(bool newFocusText) { + if (newFocusText != _focusText) { + _focusText = newFocusText; + emit focusTextChanged(_focusText); + } +} diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index 22a1b99fe6..1ce7276877 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -30,7 +30,7 @@ class OffscreenQmlRenderThread; class OffscreenQmlSurface : public QObject { Q_OBJECT - + Q_PROPERTY(bool focusText READ isFocusText NOTIFY focusTextChanged) public: OffscreenQmlSurface(); virtual ~OffscreenQmlSurface(); @@ -55,6 +55,7 @@ public: _mouseTranslator = mouseTranslator; } + bool isFocusText() const { return _focusText; } void pause(); void resume(); bool isPaused() const; @@ -70,6 +71,8 @@ public: signals: void textureUpdated(unsigned int texture); + void focusObjectChanged(QObject* newFocus); + void focusTextChanged(bool focusText); public slots: void requestUpdate(); @@ -78,6 +81,7 @@ public slots: protected: bool filterEnabled(QObject* originalDestination, QEvent* event) const; + void setFocusText(bool newFocusText); private: QObject* finishQmlLoad(std::function f); @@ -85,6 +89,7 @@ private: private slots: void updateQuick(); + void onFocusObjectChanged(QObject* newFocus); private: friend class OffscreenQmlRenderThread; @@ -97,6 +102,7 @@ private: bool _render{ false }; bool _polish{ true }; bool _paused{ true }; + bool _focusText { false }; uint8_t _maxFps{ 60 }; MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p.toPoint(); } }; QWindow* _proxyWindow { nullptr }; diff --git a/libraries/gl/src/gl/OglplusHelpers.cpp b/libraries/gl/src/gl/OglplusHelpers.cpp index 5bf0298593..7a535a806d 100644 --- a/libraries/gl/src/gl/OglplusHelpers.cpp +++ b/libraries/gl/src/gl/OglplusHelpers.cpp @@ -45,9 +45,11 @@ in vec2 vTexCoord; out vec4 FragColor; void main() { - FragColor = texture(sampler, vTexCoord); FragColor.a *= alpha; + if (FragColor.a <= 0.0) { + discard; + } } )FS"; @@ -359,6 +361,94 @@ ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov, float aspect, i ); } +namespace oglplus { + namespace shapes { + + class Laser : public DrawingInstructionWriter, public DrawMode { + public: + using IndexArray = std::vector; + using PosArray = std::vector; + /// The type of the index container returned by Indices() + // vertex positions + PosArray _pos_data; + IndexArray _idx_data; + unsigned int _prim_count { 0 }; + + public: + Laser() { + int vertices = 2; + _pos_data.resize(vertices * 3); + _pos_data[0] = 0; + _pos_data[1] = 0; + _pos_data[2] = 0; + + _pos_data[3] = 0; + _pos_data[4] = 0; + _pos_data[5] = -1; + + _idx_data.push_back(0); + _idx_data.push_back(1); + _prim_count = 1; + } + + /// Returns the winding direction of faces + FaceOrientation FaceWinding(void) const { + return FaceOrientation::CCW; + } + + /// Queries the bounding sphere coordinates and dimensions + template + void BoundingSphere(Sphere& bounding_sphere) const { + bounding_sphere = Sphere(0, 0, -0.5, 0.5); + } + + typedef GLuint(Laser::*VertexAttribFunc)(std::vector&) const; + + /// Makes the vertex positions and returns the number of values per vertex + template + GLuint Positions(std::vector& dest) const { + dest.clear(); + dest.insert(dest.begin(), _pos_data.begin(), _pos_data.end()); + return 3; + } + + typedef VertexAttribsInfo< + Laser, + std::tuple + > VertexAttribs; + + + /// Returns element indices that are used with the drawing instructions + const IndexArray & Indices(Default = Default()) const { + return _idx_data; + } + + /// Returns the instructions for rendering of faces + DrawingInstructions Instructions(PrimitiveType primitive) const { + DrawingInstructions instr = MakeInstructions(); + DrawOperation operation; + operation.method = DrawOperation::Method::DrawElements; + operation.mode = primitive; + operation.first = 0; + operation.count = _prim_count * 3; + operation.restart_index = DrawOperation::NoRestartIndex(); + operation.phase = 0; + AddInstruction(instr, operation); + return instr; + } + + /// Returns the instructions for rendering of faces + DrawingInstructions Instructions(Default = Default()) const { + return Instructions(PrimitiveType::Lines); + } + }; + } +} + +ShapeWrapperPtr loadLaser(const ProgramPtr& program) { + return std::make_shared(shapes::ShapeWrapper("Position", shapes::Laser(), *program)); +} + void TextureRecycler::setSize(const uvec2& size) { if (size == _size) { return; diff --git a/libraries/gl/src/gl/OglplusHelpers.h b/libraries/gl/src/gl/OglplusHelpers.h index afb06069b8..8940205b21 100644 --- a/libraries/gl/src/gl/OglplusHelpers.h +++ b/libraries/gl/src/gl/OglplusHelpers.h @@ -64,8 +64,9 @@ ProgramPtr loadCubemapShader(); void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& fs); ShapeWrapperPtr loadSkybox(ProgramPtr program); ShapeWrapperPtr loadPlane(ProgramPtr program, float aspect = 1.0f); -ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov = PI / 3.0f * 2.0f, float aspect = 16.0f / 9.0f, int slices = 32, int stacks = 32); - +ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov = PI / 3.0f * 2.0f, float aspect = 16.0f / 9.0f, int slices = 128, int stacks = 128); +ShapeWrapperPtr loadLaser(const ProgramPtr& program); + // A basic wrapper for constructing a framebuffer with a renderbuffer // for the depth attachment and an undefined type for the color attachement diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 26b3801ec1..b6c5e691a6 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -44,6 +44,7 @@ Q_DECLARE_METATYPE(QNetworkAccessManager::Operation) Q_DECLARE_METATYPE(JSONCallbackParameters) const QString ACCOUNTS_GROUP = "accounts"; +static const auto METAVERSE_SESSION_ID_HEADER = QString("HFM-SessionID").toLocal8Bit(); JSONCallbackParameters::JSONCallbackParameters(QObject* jsonCallbackReceiver, const QString& jsonCallbackMethod, QObject* errorCallbackReceiver, const QString& errorCallbackMethod, @@ -222,8 +223,7 @@ void AccountManager::sendRequest(const QString& path, // if we're allowed to send usage data, include whatever the current session ID is with this request auto& activityLogger = UserActivityLogger::getInstance(); if (activityLogger.isEnabled()) { - static const QString METAVERSE_SESSION_ID_HEADER = "HFM-SessionID"; - networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER.toLocal8Bit(), + networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER, uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit()); } @@ -322,6 +322,9 @@ void AccountManager::processReply() { QNetworkReply* requestReply = reinterpret_cast(sender()); if (requestReply->error() == QNetworkReply::NoError) { + if (requestReply->hasRawHeader(METAVERSE_SESSION_ID_HEADER)) { + _sessionID = requestReply->rawHeader(METAVERSE_SESSION_ID_HEADER); + } passSuccessToCallback(requestReply); } else { passErrorToCallback(requestReply); diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 4803d2625f..d30a05fb2c 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -26,9 +26,9 @@ class JSONCallbackParameters { public: - JSONCallbackParameters(QObject* jsonCallbackReceiver = NULL, const QString& jsonCallbackMethod = QString(), - QObject* errorCallbackReceiver = NULL, const QString& errorCallbackMethod = QString(), - QObject* updateReceiver = NULL, const QString& updateSlot = QString()); + JSONCallbackParameters(QObject* jsonCallbackReceiver = nullptr, const QString& jsonCallbackMethod = QString(), + QObject* errorCallbackReceiver = nullptr, const QString& errorCallbackMethod = QString(), + QObject* updateReceiver = nullptr, const QString& updateSlot = QString()); bool isEmpty() const { return !jsonCallbackReceiver && !errorCallbackReceiver; } @@ -86,6 +86,7 @@ public: static QJsonObject dataObjectFromResponse(QNetworkReply& requestReply); + QUuid getSessionID() const { return _sessionID; } void setSessionID(const QUuid& sessionID) { _sessionID = sessionID; } public slots: @@ -139,7 +140,7 @@ private: bool _isWaitingForKeypairResponse { false }; QByteArray _pendingPrivateKey; - QUuid _sessionID; + QUuid _sessionID { QUuid::createUuid() }; }; #endif // hifi_AccountManager_h diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 80989acd2c..df9b4094b0 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -23,6 +23,7 @@ #include "AddressManager.h" #include "NodeList.h" #include "NetworkLogging.h" +#include "UserActivityLogger.h" const QString ADDRESS_MANAGER_SETTINGS_GROUP = "AddressManager"; @@ -130,6 +131,10 @@ const JSONCallbackParameters& AddressManager::apiCallbackParameters() { } bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { + static QString URL_TYPE_USER = "user"; + static QString URL_TYPE_DOMAIN_ID = "domain_id"; + static QString URL_TYPE_PLACE = "place"; + static QString URL_TYPE_NETWORK_ADDRESS = "network_address"; if (lookupUrl.scheme() == HIFI_URL_SCHEME) { qCDebug(networking) << "Trying to go to URL" << lookupUrl.toString(); @@ -147,6 +152,8 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { if (handleUsername(lookupUrl.authority())) { // handled a username for lookup + UserActivityLogger::getInstance().wentTo(trigger, URL_TYPE_USER, lookupUrl.toString()); + // in case we're failing to connect to where we thought this user was // store their username as previous lookup so we can refresh their location via API _previousLookup = lookupUrl; @@ -157,6 +164,8 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { if (handleNetworkAddress(lookupUrl.host() + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { + UserActivityLogger::getInstance().wentTo(trigger, URL_TYPE_NETWORK_ADDRESS, lookupUrl.toString()); + // a network address lookup clears the previous lookup since we don't expect to re-attempt it _previousLookup.clear(); @@ -174,6 +183,8 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // we may have a path that defines a relative viewpoint - if so we should jump to that now handlePath(path, trigger); } else if (handleDomainID(lookupUrl.host())){ + UserActivityLogger::getInstance().wentTo(trigger, URL_TYPE_DOMAIN_ID, lookupUrl.toString()); + // store this domain ID as the previous lookup in case we're failing to connect and want to refresh API info _previousLookup = lookupUrl; @@ -181,6 +192,8 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // try to look up the domain ID on the metaverse API attemptDomainIDLookup(lookupUrl.host(), lookupUrl.path(), trigger); } else { + UserActivityLogger::getInstance().wentTo(trigger, URL_TYPE_PLACE, lookupUrl.toString()); + // store this place name as the previous lookup in case we fail to connect and want to refresh API info _previousLookup = lookupUrl; diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp index 83c6eb304e..eba4d31167 100644 --- a/libraries/networking/src/UserActivityLogger.cpp +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -18,6 +18,7 @@ #include "UserActivityLogger.h" #include +#include "AddressManager.h" static const QString USER_ACTIVITY_URL = "/api/v1/user_activities"; @@ -125,6 +126,19 @@ void UserActivityLogger::changedDomain(QString domainURL) { } void UserActivityLogger::connectedDevice(QString typeOfDevice, QString deviceName) { + static QStringList DEVICE_BLACKLIST = { + "Desktop", + "NullDisplayPlugin", + "3D TV - Side by Side Stereo", + "3D TV - Interleaved", + + "Keyboard/Mouse" + }; + + if (DEVICE_BLACKLIST.contains(deviceName)) { + return; + } + const QString ACTION_NAME = "connected_device"; QJsonObject actionDetails; const QString TYPE_OF_DEVICE = "type_of_device"; @@ -148,12 +162,34 @@ void UserActivityLogger::loadedScript(QString scriptName) { } -void UserActivityLogger::wentTo(QString destinationType, QString destinationName) { +void UserActivityLogger::wentTo(AddressManager::LookupTrigger lookupTrigger, QString destinationType, QString destinationName) { + // Only accept these types of triggers. Other triggers are usually used internally in AddressManager. + QString trigger; + switch (lookupTrigger) { + case AddressManager::UserInput: + trigger = "UserInput"; + break; + case AddressManager::Back: + trigger = "Back"; + break; + case AddressManager::Forward: + trigger = "Forward"; + break; + case AddressManager::StartupFromSettings: + trigger = "StartupFromSettings"; + break; + default: + return; + } + + const QString ACTION_NAME = "went_to"; QJsonObject actionDetails; + const QString TRIGGER_TYPE_KEY = "trigger"; const QString DESTINATION_TYPE_KEY = "destination_type"; const QString DESTINATION_NAME_KEY = "detination_name"; + actionDetails.insert(TRIGGER_TYPE_KEY, trigger); actionDetails.insert(DESTINATION_TYPE_KEY, destinationType); actionDetails.insert(DESTINATION_NAME_KEY, destinationName); diff --git a/libraries/networking/src/UserActivityLogger.h b/libraries/networking/src/UserActivityLogger.h index c2ab93db2f..b41960a8ad 100644 --- a/libraries/networking/src/UserActivityLogger.h +++ b/libraries/networking/src/UserActivityLogger.h @@ -20,6 +20,7 @@ #include #include +#include "AddressManager.h" class UserActivityLogger : public QObject { Q_OBJECT @@ -42,7 +43,7 @@ public slots: void changedDomain(QString domainURL); void connectedDevice(QString typeOfDevice, QString deviceName); void loadedScript(QString scriptName); - void wentTo(QString destinationType, QString destinationName); + void wentTo(AddressManager::LookupTrigger trigger, QString destinationType, QString destinationName); private slots: void requestError(QNetworkReply& errorReply); diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp new file mode 100644 index 0000000000..8b22b8ff58 --- /dev/null +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -0,0 +1,31 @@ +// +// UserActivityLoggerScriptingInterface.h +// libraries/networking/src +// +// Created by Ryan Huffman on 6/06/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "UserActivityLoggerScriptingInterface.h" +#include "UserActivityLogger.h" + +void UserActivityLoggerScriptingInterface::enabledEdit() { + logAction("enabled_edit"); +} + +void UserActivityLoggerScriptingInterface::openedMarketplace() { + logAction("opened_marketplace"); +} + +void UserActivityLoggerScriptingInterface::toggledAway(bool isAway) { + logAction("toggled_away", { { "is_away", isAway } }); +} + +void UserActivityLoggerScriptingInterface::logAction(QString action, QJsonObject details) { + QMetaObject::invokeMethod(&UserActivityLogger::getInstance(), "logAction", + Q_ARG(QString, action), + Q_ARG(QJsonObject, details)); +} diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h new file mode 100644 index 0000000000..9d60d666e2 --- /dev/null +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -0,0 +1,31 @@ +// +// UserActivityLoggerScriptingInterface.h +// libraries/networking/src +// +// Created by Ryan Huffman on 6/06/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_UserActivityLoggerScriptingInterface_h +#define hifi_UserActivityLoggerScriptingInterface_h + +#include +#include + +#include + +class UserActivityLoggerScriptingInterface : public QObject, public Dependency { + Q_OBJECT +public: + Q_INVOKABLE void enabledEdit(); + Q_INVOKABLE void openedMarketplace(); + Q_INVOKABLE void toggledAway(bool isAway); + +private: + void logAction(QString action, QJsonObject details = {}); +}; + +#endif // hifi_UserActivityLoggerScriptingInterface_h diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 6ca50420f3..c74b10820d 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS; + return VERSION_MODEL_ENTITIES_SUPPORT_STATIC_MESH; case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index ae54450fee..e484a06502 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -180,6 +180,7 @@ const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; const PacketVersion VERSION_ENTITIES_NO_FLY_ZONES = 58; const PacketVersion VERSION_ENTITIES_MORE_SHAPES = 59; const PacketVersion VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS = 60; +const PacketVersion VERSION_MODEL_ENTITIES_SUPPORT_STATIC_MESH = 61; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index dc57a82fd3..08d207fa72 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -159,6 +159,11 @@ PhysicsMotionType EntityMotionState::computePhysicsMotionType() const { } assert(entityTreeIsLocked()); + if (_entity->getShapeType() == SHAPE_TYPE_STATIC_MESH + || (_body && _body->getCollisionShape()->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE)) { + return MOTION_TYPE_STATIC; + } + if (_entity->getDynamic()) { if (!_entity->getParentID().isNull()) { // if something would have been dynamic but is a child of something else, force it to be kinematic, instead. diff --git a/libraries/physics/src/ObjectMotionState.cpp b/libraries/physics/src/ObjectMotionState.cpp index de435e80da..f915121718 100644 --- a/libraries/physics/src/ObjectMotionState.cpp +++ b/libraries/physics/src/ObjectMotionState.cpp @@ -203,35 +203,37 @@ void ObjectMotionState::handleEasyChanges(uint32_t& flags) { } } - if (flags & Simulation::DIRTY_LINEAR_VELOCITY) { - btVector3 newLinearVelocity = glmToBullet(getObjectLinearVelocity()); - if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { - float delta = (newLinearVelocity - _body->getLinearVelocity()).length(); - if (delta > ACTIVATION_LINEAR_VELOCITY_DELTA) { - flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + if (_body->getCollisionShape()->getShapeType() != TRIANGLE_MESH_SHAPE_PROXYTYPE) { + if (flags & Simulation::DIRTY_LINEAR_VELOCITY) { + btVector3 newLinearVelocity = glmToBullet(getObjectLinearVelocity()); + if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { + float delta = (newLinearVelocity - _body->getLinearVelocity()).length(); + if (delta > ACTIVATION_LINEAR_VELOCITY_DELTA) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } } - } - _body->setLinearVelocity(newLinearVelocity); + _body->setLinearVelocity(newLinearVelocity); - btVector3 newGravity = glmToBullet(getObjectGravity()); - if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { - float delta = (newGravity - _body->getGravity()).length(); - if (delta > ACTIVATION_GRAVITY_DELTA || - (delta > 0.0f && _body->getGravity().length2() == 0.0f)) { - flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + btVector3 newGravity = glmToBullet(getObjectGravity()); + if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { + float delta = (newGravity - _body->getGravity()).length(); + if (delta > ACTIVATION_GRAVITY_DELTA || + (delta > 0.0f && _body->getGravity().length2() == 0.0f)) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } } + _body->setGravity(newGravity); } - _body->setGravity(newGravity); - } - if (flags & Simulation::DIRTY_ANGULAR_VELOCITY) { - btVector3 newAngularVelocity = glmToBullet(getObjectAngularVelocity()); - if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { - float delta = (newAngularVelocity - _body->getAngularVelocity()).length(); - if (delta > ACTIVATION_ANGULAR_VELOCITY_DELTA) { - flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + if (flags & Simulation::DIRTY_ANGULAR_VELOCITY) { + btVector3 newAngularVelocity = glmToBullet(getObjectAngularVelocity()); + if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { + float delta = (newAngularVelocity - _body->getAngularVelocity()).length(); + if (delta > ACTIVATION_ANGULAR_VELOCITY_DELTA) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } } + _body->setAngularVelocity(newAngularVelocity); } - _body->setAngularVelocity(newAngularVelocity); } if (flags & Simulation::DIRTY_MATERIAL) { diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 6806b3a398..cdf33a6edb 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -217,7 +217,7 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re } else if (entity->isReadyToComputeShape()) { ShapeInfo shapeInfo; entity->computeShapeInfo(shapeInfo); - int numPoints = shapeInfo.getMaxNumPoints(); + int numPoints = shapeInfo.getLargestSubshapePointCount(); if (numPoints > MAX_HULL_POINTS) { qWarning() << "convex hull with" << numPoints << "points for entity" << entity->getName() @@ -231,7 +231,7 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re result.push_back(motionState); entityItr = _entitiesToAddToPhysics.erase(entityItr); } else { - //qDebug() << "Warning! Failed to generate new shape for entity." << entity->getName(); + //qWarning() << "Failed to generate new shape for entity." << entity->getName(); ++entityItr; } } else { diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index d667d1075d..3afc170a8c 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -67,7 +67,8 @@ static const btVector3 _unitSphereDirections[NUM_UNIT_SPHERE_DIRECTIONS] = { }; -btConvexHullShape* ShapeFactory::createConvexHull(const QVector& points) { +// util method +btConvexHullShape* createConvexHull(const ShapeInfo::PointList& points) { assert(points.size() > 0); btConvexHullShape* hull = new btConvexHullShape(); @@ -158,6 +159,84 @@ btConvexHullShape* ShapeFactory::createConvexHull(const QVector& poin return hull; } +// util method +btTriangleIndexVertexArray* createStaticMeshArray(const ShapeInfo& info) { + assert(info.getType() == SHAPE_TYPE_STATIC_MESH); // should only get here for mesh shapes + + const ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); + assert(pointCollection.size() == 1); // should only have one mesh + + const ShapeInfo::PointList& pointList = pointCollection[0]; + assert(pointList.size() > 2); // should have at least one triangle's worth of points + + const ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); + assert(triangleIndices.size() > 2); // should have at least one triangle's worth of indices + + // allocate mesh buffers + btIndexedMesh mesh; + int32_t numIndices = triangleIndices.size(); + const int32_t VERTICES_PER_TRIANGLE = 3; + mesh.m_numTriangles = numIndices / VERTICES_PER_TRIANGLE; + if (numIndices < INT16_MAX) { + // small number of points so we can use 16-bit indices + mesh.m_triangleIndexBase = new unsigned char[sizeof(int16_t) * (size_t)numIndices]; + mesh.m_indexType = PHY_SHORT; + mesh.m_triangleIndexStride = VERTICES_PER_TRIANGLE * sizeof(int16_t); + } else { + mesh.m_triangleIndexBase = new unsigned char[sizeof(int32_t) * (size_t)numIndices]; + mesh.m_indexType = PHY_INTEGER; + mesh.m_triangleIndexStride = VERTICES_PER_TRIANGLE * sizeof(int32_t); + } + mesh.m_numVertices = pointList.size(); + mesh.m_vertexBase = new unsigned char[VERTICES_PER_TRIANGLE * sizeof(btScalar) * (size_t)mesh.m_numVertices]; + mesh.m_vertexStride = VERTICES_PER_TRIANGLE * sizeof(btScalar); + mesh.m_vertexType = PHY_FLOAT; + + // copy data into buffers + btScalar* vertexData = static_cast((void*)(mesh.m_vertexBase)); + for (int32_t i = 0; i < mesh.m_numVertices; ++i) { + int32_t j = i * VERTICES_PER_TRIANGLE; + const glm::vec3& point = pointList[i]; + vertexData[j] = point.x; + vertexData[j + 1] = point.y; + vertexData[j + 2] = point.z; + } + if (numIndices < INT16_MAX) { + int16_t* indices = static_cast((void*)(mesh.m_triangleIndexBase)); + for (int32_t i = 0; i < numIndices; ++i) { + indices[i] = triangleIndices[i]; + } + } else { + int32_t* indices = static_cast((void*)(mesh.m_triangleIndexBase)); + for (int32_t i = 0; i < numIndices; ++i) { + indices[i] = triangleIndices[i]; + } + } + + // store buffers in a new dataArray and return the pointer + // (external StaticMeshShape will own all of the data that was allocated here) + btTriangleIndexVertexArray* dataArray = new btTriangleIndexVertexArray; + dataArray->addIndexedMesh(mesh, mesh.m_indexType); + return dataArray; +} + +// util method +void deleteStaticMeshArray(btTriangleIndexVertexArray* dataArray) { + assert(dataArray); + IndexedMeshArray& meshes = dataArray->getIndexedMeshArray(); + for (int32_t i = 0; i < meshes.size(); ++i) { + btIndexedMesh mesh = meshes[i]; + mesh.m_numTriangles = 0; + delete [] mesh.m_triangleIndexBase; + mesh.m_triangleIndexBase = nullptr; + mesh.m_numVertices = 0; + delete [] mesh.m_vertexBase; + mesh.m_vertexBase = nullptr; + } + meshes.clear(); + delete dataArray; +} + btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { btCollisionShape* shape = NULL; int type = info.getType(); @@ -179,15 +258,15 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { } break; case SHAPE_TYPE_COMPOUND: { - const QVector>& points = info.getPoints(); + const ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); uint32_t numSubShapes = info.getNumSubShapes(); if (numSubShapes == 1) { - shape = createConvexHull(info.getPoints()[0]); + shape = createConvexHull(pointCollection[0]); } else { auto compound = new btCompoundShape(); btTransform trans; trans.setIdentity(); - foreach (QVector hullPoints, points) { + foreach (const ShapeInfo::PointList& hullPoints, pointCollection) { btConvexHullShape* hull = createConvexHull(hullPoints); compound->addChildShape (trans, hull); } @@ -195,6 +274,11 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { } } break; + case SHAPE_TYPE_STATIC_MESH: { + btTriangleIndexVertexArray* dataArray = createStaticMeshArray(info); + shape = new StaticMeshShape(dataArray); + } + break; } if (shape) { if (glm::length2(info.getOffset()) > MIN_SHAPE_OFFSET * MIN_SHAPE_OFFSET) { @@ -228,3 +312,14 @@ void ShapeFactory::deleteShape(btCollisionShape* shape) { } delete shape; } + +// the dataArray must be created before we create the StaticMeshShape +ShapeFactory::StaticMeshShape::StaticMeshShape(btTriangleIndexVertexArray* dataArray) +: btBvhTriangleMeshShape(dataArray, true), _dataArray(dataArray) { + assert(dataArray); +} + +ShapeFactory::StaticMeshShape::~StaticMeshShape() { + deleteStaticMeshArray(_dataArray); + _dataArray = nullptr; +} diff --git a/libraries/physics/src/ShapeFactory.h b/libraries/physics/src/ShapeFactory.h index 1ba2bdb619..6202612eb9 100644 --- a/libraries/physics/src/ShapeFactory.h +++ b/libraries/physics/src/ShapeFactory.h @@ -20,9 +20,22 @@ // translates between ShapeInfo and btShape namespace ShapeFactory { - btConvexHullShape* createConvexHull(const QVector& points); btCollisionShape* createShapeFromInfo(const ShapeInfo& info); void deleteShape(btCollisionShape* shape); + + //btTriangleIndexVertexArray* createStaticMeshArray(const ShapeInfo& info); + //void deleteStaticMeshArray(btTriangleIndexVertexArray* dataArray); + + class StaticMeshShape : public btBvhTriangleMeshShape { + public: + StaticMeshShape() = delete; + StaticMeshShape(btTriangleIndexVertexArray* dataArray); + ~StaticMeshShape(); + + private: + // the StaticMeshShape owns its vertex/index data + btTriangleIndexVertexArray* _dataArray; + }; }; #endif // hifi_ShapeFactory_h diff --git a/libraries/physics/src/ShapeManager.cpp b/libraries/physics/src/ShapeManager.cpp index 4231d1eb60..4fa660239c 100644 --- a/libraries/physics/src/ShapeManager.cpp +++ b/libraries/physics/src/ShapeManager.cpp @@ -32,15 +32,13 @@ btCollisionShape* ShapeManager::getShape(const ShapeInfo& info) { if (info.getType() == SHAPE_TYPE_NONE) { return NULL; } - if (info.getType() != SHAPE_TYPE_COMPOUND) { - // Very small or large non-compound objects are not supported. - float diagonal = 4.0f * glm::length2(info.getHalfExtents()); - const float MIN_SHAPE_DIAGONAL_SQUARED = 3.0e-4f; // 1 cm cube - if (diagonal < MIN_SHAPE_DIAGONAL_SQUARED) { - // qCDebug(physics) << "ShapeManager::getShape -- not making shape due to size" << diagonal; - return NULL; - } + const float MIN_SHAPE_DIAGONAL_SQUARED = 3.0e-4f; // 1 cm cube + if (4.0f * glm::length2(info.getHalfExtents()) < MIN_SHAPE_DIAGONAL_SQUARED) { + // tiny shapes are not supported + // qCDebug(physics) << "ShapeManager::getShape -- not making shape due to size" << diagonal; + return NULL; } + DoubleHashKey key = info.getHash(); ShapeReference* shapeRef = _shapeMap.find(key); if (shapeRef) { @@ -66,8 +64,8 @@ bool ShapeManager::releaseShapeByKey(const DoubleHashKey& key) { shapeRef->refCount--; if (shapeRef->refCount == 0) { _pendingGarbage.push_back(key); - const int MAX_GARBAGE_CAPACITY = 255; - if (_pendingGarbage.size() > MAX_GARBAGE_CAPACITY) { + const int MAX_SHAPE_GARBAGE_CAPACITY = 255; + if (_pendingGarbage.size() > MAX_SHAPE_GARBAGE_CAPACITY) { collectGarbage(); } } diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 837694372a..f0ba762ecb 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -16,16 +16,18 @@ #include #include #include -class QImage; #include #include +#include #include "Plugin.h" +class QImage; + enum Eye { - Left, - Right + Left = (int)bilateral::Side::Left, + Right = (int)bilateral::Side::Right }; /* @@ -56,7 +58,73 @@ namespace gpu { using TexturePointer = std::shared_ptr; } -class DisplayPlugin : public Plugin { +// Stereo display functionality +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// displayPlugin->isStereo returns true +class StereoDisplay { +public: + // Stereo specific methods + virtual glm::mat4 getEyeProjection(Eye eye, const glm::mat4& baseProjection) const { + return baseProjection; + } + + virtual glm::mat4 getCullingProjection(const glm::mat4& baseProjection) const { + return baseProjection; + } + + virtual float getIPD() const { return AVERAGE_HUMAN_IPD; } +}; + +// HMD display functionality +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// displayPlugin->isHmd returns true +class HmdDisplay : public StereoDisplay { +public: + // HMD specific methods + // TODO move these into another class? + virtual glm::mat4 getEyeToHeadTransform(Eye eye) const { + static const glm::mat4 transform; return transform; + } + + // returns a copy of the most recent head pose, computed via updateHeadPose + virtual glm::mat4 getHeadPose() const { + return glm::mat4(); + } + + // Needed for timewarp style features + virtual void setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm::mat4& pose) { + // NOOP + } + + virtual void abandonCalibration() {} + + virtual void resetSensors() {} + + enum Hand { + LeftHand = 0x01, + RightHand = 0x02, + }; + + enum class HandLaserMode { + None, // Render no hand lasers + Overlay, // Render hand lasers only if they intersect with the UI layer, and stop at the UI layer + }; + + virtual bool setHandLaser( + uint32_t hands, // Bits from the Hand enum + HandLaserMode mode, // Mode in which to render + const vec4& color = vec4(1), // The color of the rendered laser + const vec3& direction = vec3(0, 0, -1) // The direction in which to render the hand lasers + ) { + return false; + } + + virtual bool suppressKeyboard() { return false; } + virtual void unsuppressKeyboard() {}; + virtual bool isKeyboardVisible() { return false; } +}; + +class DisplayPlugin : public Plugin, public HmdDisplay { Q_OBJECT using Parent = Plugin; public: @@ -115,42 +183,12 @@ public: return QRect(0, 0, recommendedSize.x, recommendedSize.y); } - // Stereo specific methods - virtual glm::mat4 getEyeProjection(Eye eye, const glm::mat4& baseProjection) const { - return baseProjection; - } - - virtual glm::mat4 getCullingProjection(const glm::mat4& baseProjection) const { - return baseProjection; - } - - // Fetch the most recently displayed image as a QImage virtual QImage getScreenshot() const = 0; - // HMD specific methods - // TODO move these into another class? - virtual glm::mat4 getEyeToHeadTransform(Eye eye) const { - static const glm::mat4 transform; return transform; - } - // will query the underlying hmd api to compute the most recent head pose virtual bool beginFrameRender(uint32_t frameIndex) { return true; } - // returns a copy of the most recent head pose, computed via updateHeadPose - virtual glm::mat4 getHeadPose() const { - return glm::mat4(); - } - - // Needed for timewarp style features - virtual void setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm::mat4& pose) { - // NOOP - } - - virtual float getIPD() const { return AVERAGE_HUMAN_IPD; } - - virtual void abandonCalibration() {} - virtual void resetSensors() {} virtual float devicePixelRatio() { return 1.0f; } // Rate at which we present to the display device virtual float presentRate() const { return -1.0f; } @@ -158,6 +196,7 @@ public: virtual float newFramePresentRate() const { return -1.0f; } // Rate at which rendered frames are being skipped virtual float droppedFrameRate() const { return -1.0f; } + uint32_t presentCount() const { return _presentedFrameIndex; } // Time since last call to incrementPresentCount (only valid if DEBUG_PAINT_DELAY is defined) int64_t getPaintDelayUsecs() const; @@ -166,6 +205,7 @@ public: static const QString& MENU_PATH(); + signals: void recommendedFramebufferSizeChanged(const QSize & size); diff --git a/libraries/plugins/src/plugins/Plugin.h b/libraries/plugins/src/plugins/Plugin.h index fb5bf0ba55..0452c7fbfe 100644 --- a/libraries/plugins/src/plugins/Plugin.h +++ b/libraries/plugins/src/plugins/Plugin.h @@ -15,6 +15,7 @@ #include "Forward.h" class Plugin : public QObject { + Q_OBJECT public: /// \return human-readable name virtual const QString& getName() const = 0; @@ -63,6 +64,13 @@ public: virtual void saveSettings() const {} virtual void loadSettings() {} +signals: + // These signals should be emitted when a device is first known to be available. In some cases this will + // be in `init()`, in other cases, like Neuron, this isn't known until activation. + // SDL2 isn't a device itself, but can have 0+ subdevices. subdeviceConnected is used in this case. + void deviceConnected(QString pluginName) const; + void subdeviceConnected(QString pluginName, QString subdeviceName) const; + protected: bool _active { false }; PluginContainer* _container { nullptr }; diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index d5c860200a..29658eeb6b 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -14,6 +14,9 @@ #include #include +#include +#include + #include "RuntimePlugin.h" #include "DisplayPlugin.h" #include "InputPlugin.h" @@ -119,6 +122,15 @@ static DisplayPluginList displayPlugins; const DisplayPluginList& PluginManager::getDisplayPlugins() { static std::once_flag once; + static auto deviceAddedCallback = [](QString deviceName) { + qDebug() << "Added device: " << deviceName; + UserActivityLogger::getInstance().connectedDevice("display", deviceName); + }; + static auto subdeviceAddedCallback = [](QString pluginName, QString deviceName) { + qDebug() << "Added subdevice: " << deviceName; + UserActivityLogger::getInstance().connectedDevice("display", pluginName + " | " + deviceName); + }; + std::call_once(once, [&] { // Grab the built in plugins displayPlugins = ::getDisplayPlugins(); @@ -133,6 +145,8 @@ const DisplayPluginList& PluginManager::getDisplayPlugins() { } } for (auto plugin : displayPlugins) { + connect(plugin.get(), &Plugin::deviceConnected, this, deviceAddedCallback, Qt::QueuedConnection); + connect(plugin.get(), &Plugin::subdeviceConnected, this, subdeviceAddedCallback, Qt::QueuedConnection); plugin->setContainer(_container); plugin->init(); } @@ -154,6 +168,15 @@ void PluginManager::disableDisplayPlugin(const QString& name) { const InputPluginList& PluginManager::getInputPlugins() { static InputPluginList inputPlugins; static std::once_flag once; + static auto deviceAddedCallback = [](QString deviceName) { + qDebug() << "Added device: " << deviceName; + UserActivityLogger::getInstance().connectedDevice("input", deviceName); + }; + static auto subdeviceAddedCallback = [](QString pluginName, QString deviceName) { + qDebug() << "Added subdevice: " << deviceName; + UserActivityLogger::getInstance().connectedDevice("input", pluginName + " | " + deviceName); + }; + std::call_once(once, [&] { inputPlugins = ::getInputPlugins(); @@ -170,6 +193,8 @@ const InputPluginList& PluginManager::getInputPlugins() { } for (auto plugin : inputPlugins) { + connect(plugin.get(), &Plugin::deviceConnected, this, deviceAddedCallback, Qt::QueuedConnection); + connect(plugin.get(), &Plugin::subdeviceConnected, this, subdeviceAddedCallback, Qt::QueuedConnection); plugin->setContainer(_container); plugin->init(); } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index ded1184c24..0470a238fc 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1002,7 +1002,7 @@ void Model::scaleToFit() { Extents modelMeshExtents = getUnscaledMeshExtents(); // size is our "target size in world space" - // we need to set our model scale so that the extents of the mesh, fit in a cube that size... + // we need to set our model scale so that the extents of the mesh, fit in a box that size... glm::vec3 meshDimensions = modelMeshExtents.maximum - modelMeshExtents.minimum; glm::vec3 rescaleDimensions = _scaleToFitDimensions / meshDimensions; setScaleInternal(rescaleDimensions); diff --git a/libraries/shared/src/GPUIdent.cpp b/libraries/shared/src/GPUIdent.cpp index 19838964a4..02f92d87e7 100644 --- a/libraries/shared/src/GPUIdent.cpp +++ b/libraries/shared/src/GPUIdent.cpp @@ -122,7 +122,7 @@ GPUIdent* GPUIdent::ensureQuery(const QString& vendor, const QString& renderer) } if (count > bestCount) { bestCount = count; - _name = sString; + _name = QString(sString).trimmed(); hr = spInstance->Get(CComBSTR(_T("DriverVersion")), 0, &var, 0, 0); if (hr == S_OK) { diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index 9c1e5c3816..e0f4cc18b2 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -16,19 +16,23 @@ #include "NumericalConstants.h" // for MILLIMETERS_PER_METER void ShapeInfo::clear() { - _type = SHAPE_TYPE_NONE; - _halfExtents = _offset = glm::vec3(0.0f); + _url.clear(); + _pointCollection.clear(); + _triangleIndices.clear(); + _halfExtents = glm::vec3(0.0f); + _offset = glm::vec3(0.0f); _doubleHashKey.clear(); + _type = SHAPE_TYPE_NONE; } void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString url) { _type = type; + _halfExtents = halfExtents; switch(type) { case SHAPE_TYPE_NONE: _halfExtents = glm::vec3(0.0f); break; case SHAPE_TYPE_BOX: - _halfExtents = halfExtents; break; case SHAPE_TYPE_SPHERE: { // sphere radius is max of halfExtents @@ -37,11 +41,10 @@ void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString break; } case SHAPE_TYPE_COMPOUND: + case SHAPE_TYPE_STATIC_MESH: _url = QUrl(url); - _halfExtents = halfExtents; break; default: - _halfExtents = halfExtents; break; } _doubleHashKey.clear(); @@ -61,9 +64,9 @@ void ShapeInfo::setSphere(float radius) { _doubleHashKey.clear(); } -void ShapeInfo::setConvexHulls(const QVector>& points) { - _points = points; - _type = (_points.size() > 0) ? SHAPE_TYPE_COMPOUND : SHAPE_TYPE_NONE; +void ShapeInfo::setPointCollection(const ShapeInfo::PointCollection& pointCollection) { + _pointCollection = pointCollection; + _type = (_pointCollection.size() > 0) ? SHAPE_TYPE_COMPOUND : SHAPE_TYPE_NONE; _doubleHashKey.clear(); } @@ -83,15 +86,15 @@ uint32_t ShapeInfo::getNumSubShapes() const { if (_type == SHAPE_TYPE_NONE) { return 0; } else if (_type == SHAPE_TYPE_COMPOUND) { - return _points.size(); + return _pointCollection.size(); } return 1; } -int ShapeInfo::getMaxNumPoints() const { +int ShapeInfo::getLargestSubshapePointCount() const { int numPoints = 0; - for (int i = 0; i < _points.size(); ++i) { - int n = _points[i].size(); + for (int i = 0; i < _pointCollection.size(); ++i) { + int n = _pointCollection[i].size(); if (n > numPoints) { numPoints = n; } @@ -178,34 +181,31 @@ const DoubleHashKey& ShapeInfo::getHash() const { // NOTE: we cache the key so we only ever need to compute it once for any valid ShapeInfo instance. if (_doubleHashKey.isNull() && _type != SHAPE_TYPE_NONE) { bool useOffset = glm::length2(_offset) > MIN_SHAPE_OFFSET * MIN_SHAPE_OFFSET; - // The key is not yet cached therefore we must compute it! To this end we bypass the const-ness - // of this method by grabbing a non-const pointer to "this" and a non-const reference to _doubleHashKey. - ShapeInfo* thisPtr = const_cast(this); - DoubleHashKey& key = thisPtr->_doubleHashKey; + // The key is not yet cached therefore we must compute it. // compute hash1 // TODO?: provide lookup table for hash/hash2 of _type rather than recompute? uint32_t primeIndex = 0; - key.computeHash((uint32_t)_type, primeIndex++); - - // compute hash1 - uint32_t hash = key.getHash(); + _doubleHashKey.computeHash((uint32_t)_type, primeIndex++); + + // compute hash1 + uint32_t hash = _doubleHashKey.getHash(); for (int j = 0; j < 3; ++j) { // NOTE: 0.49f is used to bump the float up almost half a millimeter // so the cast to int produces a round() effect rather than a floor() hash ^= DoubleHashKey::hashFunction( - (uint32_t)(_halfExtents[j] * MILLIMETERS_PER_METER + copysignf(1.0f, _halfExtents[j]) * 0.49f), + (uint32_t)(_halfExtents[j] * MILLIMETERS_PER_METER + copysignf(1.0f, _halfExtents[j]) * 0.49f), primeIndex++); if (useOffset) { hash ^= DoubleHashKey::hashFunction( - (uint32_t)(_offset[j] * MILLIMETERS_PER_METER + copysignf(1.0f, _offset[j]) * 0.49f), + (uint32_t)(_offset[j] * MILLIMETERS_PER_METER + copysignf(1.0f, _offset[j]) * 0.49f), primeIndex++); } } - key.setHash(hash); - + _doubleHashKey.setHash(hash); + // compute hash2 - hash = key.getHash2(); + hash = _doubleHashKey.getHash2(); for (int j = 0; j < 3; ++j) { // NOTE: 0.49f is used to bump the float up almost half a millimeter // so the cast to int produces a round() effect rather than a floor() @@ -222,16 +222,18 @@ const DoubleHashKey& ShapeInfo::getHash() const { hash += ~(floatHash << 10); hash = (hash << 16) | (hash >> 16); } - key.setHash2(hash); + _doubleHashKey.setHash2(hash); - QString url = _url.toString(); - if (!url.isEmpty()) { - // fold the urlHash into both parts - QByteArray baUrl = url.toLocal8Bit(); - const char *cUrl = baUrl.data(); - uint32_t urlHash = qChecksum(cUrl, baUrl.count()); - key.setHash(key.getHash() ^ urlHash); - key.setHash2(key.getHash2() ^ urlHash); + if (_type == SHAPE_TYPE_COMPOUND || _type == SHAPE_TYPE_STATIC_MESH) { + QString url = _url.toString(); + if (!url.isEmpty()) { + // fold the urlHash into both parts + QByteArray baUrl = url.toLocal8Bit(); + const char *cUrl = baUrl.data(); + uint32_t urlHash = qChecksum(cUrl, baUrl.count()); + _doubleHashKey.setHash(_doubleHashKey.getHash() ^ urlHash); + _doubleHashKey.setHash2(_doubleHashKey.getHash2() ^ urlHash); + } } } return _doubleHashKey; diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index c853666d90..96132a4b23 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -30,26 +30,32 @@ enum ShapeType { SHAPE_TYPE_NONE, SHAPE_TYPE_BOX, SHAPE_TYPE_SPHERE, - SHAPE_TYPE_PLANE, - SHAPE_TYPE_COMPOUND, SHAPE_TYPE_CAPSULE_X, SHAPE_TYPE_CAPSULE_Y, SHAPE_TYPE_CAPSULE_Z, SHAPE_TYPE_CYLINDER_X, SHAPE_TYPE_CYLINDER_Y, SHAPE_TYPE_CYLINDER_Z, + SHAPE_TYPE_HULL, + SHAPE_TYPE_PLANE, + SHAPE_TYPE_COMPOUND, SHAPE_TYPE_STATIC_MESH }; class ShapeInfo { public: + + using PointList = QVector; + using PointCollection = QVector; + using TriangleIndices = QVector; + void clear(); void setParams(ShapeType type, const glm::vec3& halfExtents, QString url=""); void setBox(const glm::vec3& halfExtents); void setSphere(float radius); - void setConvexHulls(const QVector>& points); + void setPointCollection(const PointCollection& pointCollection); void setCapsuleY(float radius, float halfHeight); void setOffset(const glm::vec3& offset); @@ -57,13 +63,15 @@ public: const glm::vec3& getHalfExtents() const { return _halfExtents; } const glm::vec3& getOffset() const { return _offset; } - - QVector>& getPoints() { return _points; } - const QVector>& getPoints() const { return _points; } uint32_t getNumSubShapes() const; - void appendToPoints (const QVector& newPoints) { _points << newPoints; } - int getMaxNumPoints() const; + PointCollection& getPointCollection() { return _pointCollection; } + const PointCollection& getPointCollection() const { return _pointCollection; } + + TriangleIndices& getTriangleIndices() { return _triangleIndices; } + const TriangleIndices& getTriangleIndices() const { return _triangleIndices; } + + int getLargestSubshapePointCount() const; float computeVolume() const; @@ -74,12 +82,13 @@ public: const DoubleHashKey& getHash() const; protected: - ShapeType _type = SHAPE_TYPE_NONE; + QUrl _url; // url for model of convex collision hulls + PointCollection _pointCollection; + TriangleIndices _triangleIndices; glm::vec3 _halfExtents = glm::vec3(0.0f); glm::vec3 _offset = glm::vec3(0.0f); - DoubleHashKey _doubleHashKey; - QVector> _points; // points for convex collision hulls - QUrl _url; // url for model of convex collision hulls + mutable DoubleHashKey _doubleHashKey; + ShapeType _type = SHAPE_TYPE_NONE; }; #endif // hifi_ShapeInfo_h diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index b80fac637c..edb6fe437d 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -28,6 +28,7 @@ #ifdef Q_OS_WIN #include "CPUIdent.h" +#include #endif @@ -843,3 +844,29 @@ void printSystemInformation() { (envVariables.contains(env) ? " = " + envVariables.value(env) : " NOT FOUND"); } } + +bool getMemoryInfo(MemoryInfo& info) { +#ifdef Q_OS_WIN + MEMORYSTATUSEX ms; + ms.dwLength = sizeof(ms); + if (!GlobalMemoryStatusEx(&ms)) { + return false; + } + + info.totalMemoryBytes = ms.ullTotalPhys; + info.availMemoryBytes = ms.ullAvailPhys; + info.usedMemoryBytes = ms.ullTotalPhys - ms.ullAvailPhys; + + + PROCESS_MEMORY_COUNTERS_EX pmc; + if (!GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&pmc), sizeof(pmc))) { + return false; + } + info.processUsedMemoryBytes = pmc.PrivateUsage; + info.processPeakUsedMemoryBytes = pmc.PeakPagefileUsage; + + return true; +#endif + + return false; +} \ No newline at end of file diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index 042396f474..f3e5625484 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -204,4 +204,14 @@ void disableQtBearerPoll(); void printSystemInformation(); +struct MemoryInfo { + uint64_t totalMemoryBytes; + uint64_t availMemoryBytes; + uint64_t usedMemoryBytes; + uint64_t processUsedMemoryBytes; + uint64_t processPeakUsedMemoryBytes; +}; + +bool getMemoryInfo(MemoryInfo& info); + #endif // hifi_SharedUtil_h diff --git a/libraries/shared/src/shared/Bilateral.h b/libraries/shared/src/shared/Bilateral.h new file mode 100644 index 0000000000..c4daf60177 --- /dev/null +++ b/libraries/shared/src/shared/Bilateral.h @@ -0,0 +1,49 @@ +// +// Created by Bradley Austin Davis 2015/10/09 +// 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 +// + +#pragma once + +namespace bilateral { + enum class Side { + Left = 0, + Right = 1 + }; + + using Indices = Side; + + enum class Bits { + Left = 0x01, + Right = 0x02 + }; + + inline uint8_t bit(Side side) { + switch (side) { + case Side::Left: + return 0x01; + case Side::Right: + return 0x02; + } + return UINT8_MAX; + } + + inline uint8_t index(Side side) { + switch (side) { + case Side::Left: + return 0; + case Side::Right: + return 1; + } + return UINT8_MAX; + } + + template + void for_each_side(F f) { + f(Side::Left); + f(Side::Right); + } +} diff --git a/libraries/ui/src/ErrorDialog.cpp b/libraries/ui/src/ErrorDialog.cpp index ab36ef8d36..fcd73b4cc0 100644 --- a/libraries/ui/src/ErrorDialog.cpp +++ b/libraries/ui/src/ErrorDialog.cpp @@ -22,10 +22,6 @@ QString ErrorDialog::text() const { return _text; } -void ErrorDialog::setVisible(bool v) { - OffscreenQmlDialog::setVisible(v); -} - void ErrorDialog::setText(const QString& arg) { if (arg != _text) { _text = arg; diff --git a/libraries/ui/src/ErrorDialog.h b/libraries/ui/src/ErrorDialog.h index 665090da1a..38954714a7 100644 --- a/libraries/ui/src/ErrorDialog.h +++ b/libraries/ui/src/ErrorDialog.h @@ -30,7 +30,6 @@ public: QString text() const; public slots: - virtual void setVisible(bool v); void setText(const QString& arg); signals: diff --git a/libraries/ui/src/OffscreenQmlDialog.cpp b/libraries/ui/src/OffscreenQmlDialog.cpp index 43514c4761..2d1ca20876 100644 --- a/libraries/ui/src/OffscreenQmlDialog.cpp +++ b/libraries/ui/src/OffscreenQmlDialog.cpp @@ -17,7 +17,7 @@ OffscreenQmlDialog::~OffscreenQmlDialog() { } void OffscreenQmlDialog::hide() { - static_cast(parent())->setVisible(false); + parent()->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, false); } QString OffscreenQmlDialog::title() const { diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index dfd9056703..fa1a31d196 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -121,32 +121,28 @@ void OffscreenUi::show(const QUrl& url, const QString& name, std::functionfindChild(name); } + if (item) { - item->setVisible(true); + QQmlProperty(item, OFFSCREEN_VISIBILITY_PROPERTY).write(true); } } void OffscreenUi::toggle(const QUrl& url, const QString& name, std::function f) { QQuickItem* item = getRootItem()->findChild(name); - // Already loaded? - if (item) { - emit showDesktop(); - item->setVisible(!item->isVisible()); + if (!item) { + show(url, name, f); return; } - load(url, f); - item = getRootItem()->findChild(name); - if (item && !item->isVisible()) { - emit showDesktop(); - item->setVisible(true); - } + // Already loaded, so just flip the bit + QQmlProperty shownProperty(item, OFFSCREEN_VISIBILITY_PROPERTY); + shownProperty.write(!shownProperty.read().toBool()); } void OffscreenUi::hide(const QString& name) { QQuickItem* item = getRootItem()->findChild(name); if (item) { - item->setVisible(false); + QQmlProperty(item, OFFSCREEN_VISIBILITY_PROPERTY).write(false); } } @@ -345,6 +341,20 @@ QVariant OffscreenUi::inputDialog(const Icon icon, const QString& title, const Q return waitForInputDialogResult(createInputDialog(icon, title, label, current)); } +void OffscreenUi::togglePinned() { + bool invokeResult = QMetaObject::invokeMethod(_desktop, "togglePinned"); + if (!invokeResult) { + qWarning() << "Failed to toggle window visibility"; + } +} + +void OffscreenUi::setPinned(bool pinned) { + bool invokeResult = QMetaObject::invokeMethod(_desktop, "setPinned", Q_ARG(QVariant, pinned)); + if (!invokeResult) { + qWarning() << "Failed to set window visibility"; + } +} + void OffscreenUi::addMenuInitializer(std::function f) { if (!_vrMenu) { _queuedMenuInitializers.push_back(f); diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 5a16b49491..e1d552c978 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -28,6 +28,8 @@ class VrMenu; +#define OFFSCREEN_VISIBILITY_PROPERTY "shown" + class OffscreenUi : public OffscreenQmlSurface, public Dependency { Q_OBJECT @@ -44,6 +46,13 @@ public: void setNavigationFocused(bool focused); void unfocusWindows(); void toggleMenu(const QPoint& screenCoordinates); + + + // Setting pinned to true will hide all overlay elements on the desktop that don't have a pinned flag + void setPinned(bool pinned = true); + + void togglePinned(); + bool eventFilter(QObject* originalDestination, QEvent* event) override; void addMenuInitializer(std::function f); diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index 396d716cda..b8834f0549 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -163,8 +163,7 @@ void QmlWindowClass::setVisible(bool visible) { QMetaObject::invokeMethod(targetWindow, "showTabForUrl", Qt::QueuedConnection, Q_ARG(QVariant, _source), Q_ARG(QVariant, visible)); } else { DependencyManager::get()->executeOnUiThread([=] { - targetWindow->setVisible(visible); - //emit visibilityChanged(visible); + targetWindow->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, visible); }); } } diff --git a/libraries/ui/src/Tooltip.cpp b/libraries/ui/src/Tooltip.cpp index 3c0902b378..94e04f34b6 100644 --- a/libraries/ui/src/Tooltip.cpp +++ b/libraries/ui/src/Tooltip.cpp @@ -47,10 +47,6 @@ void Tooltip::setImageURL(const QString& imageURL) { } } -void Tooltip::setVisible(bool visible) { - QQuickItem::setVisible(visible); -} - QString Tooltip::showTip(const QString& title, const QString& description) { const QString newTipId = QUuid().createUuid().toString(); diff --git a/libraries/ui/src/Tooltip.h b/libraries/ui/src/Tooltip.h index d1c7330a74..5e884a7aea 100644 --- a/libraries/ui/src/Tooltip.h +++ b/libraries/ui/src/Tooltip.h @@ -39,8 +39,6 @@ public: static void closeTip(const QString& tipId); public slots: - virtual void setVisible(bool v); - void setTitle(const QString& title); void setDescription(const QString& description); void setImageURL(const QString& imageURL); diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index 0a4bc7f8d2..e41472a8c5 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -387,6 +387,8 @@ bool NeuronPlugin::activate() { } else { qCDebug(inputplugins) << "NeuronPlugin: success connecting to " << _serverAddress.c_str() << ":" << _serverPort; + emit deviceConnected(getName()); + BRRegisterAutoSyncParmeter(_socketRef, Cmd_CombinationMode); return true; } diff --git a/plugins/hifiSdl2/src/SDL2Manager.cpp b/plugins/hifiSdl2/src/SDL2Manager.cpp index 09e783864c..b9a19658e2 100644 --- a/plugins/hifiSdl2/src/SDL2Manager.cpp +++ b/plugins/hifiSdl2/src/SDL2Manager.cpp @@ -66,6 +66,7 @@ void SDL2Manager::init() { auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(joystick); emit joystickAdded(joystick.get()); + emit subdeviceConnected(getName(), SDL_GameControllerName(controller)); } } } @@ -157,6 +158,7 @@ void SDL2Manager::pluginUpdate(float deltaTime, const controller::InputCalibrati _openJoysticks[id] = joystick; userInputMapper->registerDevice(joystick); emit joystickAdded(joystick.get()); + emit subdeviceConnected(getName(), SDL_GameControllerName(controller)); } } else if (event.type == SDL_CONTROLLERDEVICEREMOVED) { if (_openJoysticks.contains(event.cdevice.which)) { diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index 566f879f69..03028249a3 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -137,6 +137,12 @@ void SixenseManager::setSixenseFilter(bool filter) { void SixenseManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { BAIL_IF_NOT_LOADED + static bool sixenseHasBeenConnected { false }; + if (!sixenseHasBeenConnected && sixenseIsBaseConnected(0)) { + sixenseHasBeenConnected = true; + emit deviceConnected(getName()); + } + auto userInputMapper = DependencyManager::get(); userInputMapper->withLock([&, this]() { _inputDevice->update(deltaTime, inputCalibrationData); diff --git a/plugins/hifiSpacemouse/src/SpacemouseManager.cpp b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp index 0c29ced6f9..4641799b79 100644 --- a/plugins/hifiSpacemouse/src/SpacemouseManager.cpp +++ b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp @@ -58,7 +58,6 @@ bool SpacemouseManager::activate() { if (instance->getDeviceID() == controller::Input::INVALID_DEVICE) { auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(instance); - UserActivityLogger::getInstance().connectedDevice("controller", NAME); } return true; } @@ -329,7 +328,6 @@ bool SpacemouseManager::RawInputEventFilter(void* msg, long* result) { auto userInputMapper = DependencyManager::get(); if (Is3dmouseAttached() && instance->getDeviceID() == controller::Input::INVALID_DEVICE) { userInputMapper->registerDevice(instance); - UserActivityLogger::getInstance().connectedDevice("controller", "Spacemouse"); } else if (!Is3dmouseAttached() && instance->getDeviceID() != controller::Input::INVALID_DEVICE) { userInputMapper->removeDevice(instance->getDeviceID()); @@ -856,7 +854,7 @@ void SpacemouseManager::init() { if (Is3dmouseAttached() && instance->getDeviceID() == controller::Input::INVALID_DEVICE) { auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(instance); - UserActivityLogger::getInstance().connectedDevice("controller", "Spacemouse"); + emit deviceConnected(getName()); } //let one axis be dominant //ConnexionClientControl(fConnexionClientID, kConnexionCtlSetSwitches, kConnexionSwitchDominant | kConnexionSwitchEnableAll, NULL); diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index e9f8545cff..e26a48b89c 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -8,6 +8,8 @@ #include "OculusBaseDisplayPlugin.h" #include +#include +#include #include "OculusHelpers.h" @@ -24,9 +26,26 @@ bool OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { auto trackingState = ovr_GetTrackingState(_session, _currentRenderFrameInfo.predictedDisplayTime, ovrTrue); _currentRenderFrameInfo.renderPose = toGlm(trackingState.HeadPose.ThePose); _currentRenderFrameInfo.presentPose = _currentRenderFrameInfo.renderPose; - Lock lock(_mutex); - _frameInfos[frameIndex] = _currentRenderFrameInfo; - return true; + + std::array handPoses; + // Make controller poses available to the presentation thread + ovr_for_each_hand([&](ovrHandType hand) { + static const auto REQUIRED_HAND_STATUS = ovrStatus_OrientationTracked & ovrStatus_PositionTracked; + if (REQUIRED_HAND_STATUS != (trackingState.HandStatusFlags[hand] & REQUIRED_HAND_STATUS)) { + return; + } + + auto correctedPose = ovrControllerPoseToHandPose(hand, trackingState.HandPoses[hand]); + static const glm::quat HAND_TO_LASER_ROTATION = glm::rotation(Vectors::UNIT_Z, Vectors::UNIT_NEG_Y); + handPoses[hand] = glm::translate(glm::mat4(), correctedPose.translation) * glm::mat4_cast(correctedPose.rotation * HAND_TO_LASER_ROTATION); + }); + + withRenderThreadLock([&] { + _uiModelTransform = DependencyManager::get()->getModelTransform(); + _handPoses = handPoses; + _frameInfos[frameIndex] = _currentRenderFrameInfo; + }); + return Parent::beginFrameRender(frameIndex); } bool OculusBaseDisplayPlugin::isSupported() const { diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 128b980558..b3b1b20b2b 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -243,91 +243,13 @@ void OculusControllerManager::TouchDevice::focusOutEvent() { void OculusControllerManager::TouchDevice::handlePose(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, ovrHandType hand, const ovrPoseStatef& handPose) { - // When the sensor-to-world rotation is identity the coordinate axes look like this: - // - // user - // forward - // -z - // | - // y| user - // y o----x right - // o-----x user - // | up - // | - // z - // - // Rift - - // From ABOVE the hand canonical axes looks like this: - // - // | | | | y | | | | - // | | | | | | | | | - // | | | | | - // |left | / x---- + \ |right| - // | _/ z \_ | - // | | | | - // | | | | - // - - // So when the user is in Rift space facing the -zAxis with hands outstretched and palms down - // the rotation to align the Touch axes with those of the hands is: - // - // touchToHand = halfTurnAboutY * quaterTurnAboutX - - // Due to how the Touch controllers fit into the palm there is an offset that is different for each hand. - // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that - // the combination (measurement * offset) is identity at this orientation. - // - // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) - // - // An approximate offset for the Touch can be obtained by inspection: - // - // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) * glm::angleAxis(PI/4.0f, xAxis)) - // - // So the full equation is: - // - // Q = combinedMeasurement * touchToHand - // - // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) - // - // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) - auto poseId = hand == ovrHand_Left ? controller::LEFT_HAND : controller::RIGHT_HAND; auto& pose = _poseStateMap[poseId]; - - static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); - static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); - static const glm::quat touchToHand = yFlip * quarterX; - - static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); - static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); - static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); - - static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ * eighthX) * touchToHand; - static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ * eighthX) * touchToHand; - - static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches - static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET * 2.0f); - static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; - static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; - - auto translationOffset = (hand == ovrHand_Left ? leftTranslationOffset : rightTranslationOffset); - auto rotationOffset = (hand == ovrHand_Left ? leftRotationOffset : rightRotationOffset); - - glm::quat rotation = toGlm(handPose.ThePose.Orientation); - - pose.translation = toGlm(handPose.ThePose.Position); - pose.translation += rotation * translationOffset; - pose.rotation = rotation * rotationOffset; - pose.angularVelocity = toGlm(handPose.AngularVelocity); - pose.velocity = toGlm(handPose.LinearVelocity); - pose.valid = true; - + pose = ovrControllerPoseToHandPose(hand, handPose); // transform into avatar frame glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; pose = pose.transform(controllerToAvatar); + } bool OculusControllerManager::TouchDevice::triggerHapticPulse(float strength, float duration, controller::Hand hand) { diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index 1006d69f06..2b2ec5bdb0 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -28,6 +28,12 @@ bool OculusDisplayPlugin::internalActivate() { return result; } +void OculusDisplayPlugin::init() { + Plugin::init(); + + emit deviceConnected(getName()); +} + void OculusDisplayPlugin::cycleDebugOutput() { if (_session) { currentDebugMode = static_cast((currentDebugMode + 1) % ovrPerfHud_Count); diff --git a/plugins/oculus/src/OculusDisplayPlugin.h b/plugins/oculus/src/OculusDisplayPlugin.h index d6cd6f6f3d..ed6e0d13ea 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.h +++ b/plugins/oculus/src/OculusDisplayPlugin.h @@ -17,6 +17,8 @@ class OculusDisplayPlugin : public OculusBaseDisplayPlugin { public: const QString& getName() const override { return NAME; } + void init() override; + QString getPreferredAudioInDevice() const override; QString getPreferredAudioOutDevice() const override; diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index 6ddace684b..49c14c8d66 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -14,6 +14,10 @@ #include #include #include +#include + +#include +#include using Mutex = std::mutex; using Lock = std::unique_lock; @@ -50,6 +54,13 @@ bool oculusAvailable() { static std::once_flag once; static bool result { false }; std::call_once(once, [&] { + + static const QString DEBUG_FLAG("HIFI_DEBUG_OPENVR"); + static bool enableDebugOpenVR = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); + if (enableDebugOpenVR) { + return; + } + ovrDetectResult detect = ovr_Detect(0); if (!detect.IsOculusServiceRunning || !detect.IsOculusHMDConnected) { return; @@ -191,3 +202,88 @@ void SwapFramebufferWrapper::onBind(oglplus::Framebuffer::Target target) { void SwapFramebufferWrapper::onUnbind(oglplus::Framebuffer::Target target) { glFramebufferTexture2D(toEnum(target), GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); } + + +controller::Pose ovrControllerPoseToHandPose( + ovrHandType hand, + const ovrPoseStatef& handPose) { + // When the sensor-to-world rotation is identity the coordinate axes look like this: + // + // user + // forward + // -z + // | + // y| user + // y o----x right + // o-----x user + // | up + // | + // z + // + // Rift + + // From ABOVE the hand canonical axes looks like this: + // + // | | | | y | | | | + // | | | | | | | | | + // | | | | | + // |left | / x---- + \ |right| + // | _/ z \_ | + // | | | | + // | | | | + // + + // So when the user is in Rift space facing the -zAxis with hands outstretched and palms down + // the rotation to align the Touch axes with those of the hands is: + // + // touchToHand = halfTurnAboutY * quaterTurnAboutX + + // Due to how the Touch controllers fit into the palm there is an offset that is different for each hand. + // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that + // the combination (measurement * offset) is identity at this orientation. + // + // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) + // + // An approximate offset for the Touch can be obtained by inspection: + // + // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) * glm::angleAxis(PI/4.0f, xAxis)) + // + // So the full equation is: + // + // Q = combinedMeasurement * touchToHand + // + // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) + // + // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) + static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); + static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); + static const glm::quat touchToHand = yFlip * quarterX; + + static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); + + static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ * eighthX) * touchToHand; + static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ * eighthX) * touchToHand; + + static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches + static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET * 2.0f); + static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; + static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; + + auto translationOffset = (hand == ovrHand_Left ? leftTranslationOffset : rightTranslationOffset); + auto rotationOffset = (hand == ovrHand_Left ? leftRotationOffset : rightRotationOffset); + + glm::quat rotation = toGlm(handPose.ThePose.Orientation); + + controller::Pose pose; + pose.translation = toGlm(handPose.ThePose.Position); + pose.translation += rotation * translationOffset; + pose.rotation = rotation * rotationOffset; + pose.angularVelocity = toGlm(handPose.AngularVelocity); + pose.velocity = toGlm(handPose.LinearVelocity); + pose.valid = true; + return pose; +} \ No newline at end of file diff --git a/plugins/oculus/src/OculusHelpers.h b/plugins/oculus/src/OculusHelpers.h index 2f13c45466..66cdccf15a 100644 --- a/plugins/oculus/src/OculusHelpers.h +++ b/plugins/oculus/src/OculusHelpers.h @@ -13,6 +13,7 @@ #include #include +#include void logWarning(const char* what); void logFatal(const char* what); @@ -128,3 +129,7 @@ protected: private: ovrSession _session; }; + +controller::Pose ovrControllerPoseToHandPose( + ovrHandType hand, + const ovrPoseStatef& handPose); diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 4aadb890d5..2c032f7005 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -36,18 +36,26 @@ const QString OculusLegacyDisplayPlugin::NAME("Oculus Rift"); OculusLegacyDisplayPlugin::OculusLegacyDisplayPlugin() { } +void OculusLegacyDisplayPlugin::init() { + Plugin::init(); + + emit deviceConnected(getName()); +} + void OculusLegacyDisplayPlugin::resetSensors() { ovrHmd_RecenterPose(_hmd); } bool OculusLegacyDisplayPlugin::beginFrameRender(uint32_t frameIndex) { + _currentRenderFrameInfo = FrameInfo(); _currentRenderFrameInfo.predictedDisplayTime = _currentRenderFrameInfo.sensorSampleTime = ovr_GetTimeInSeconds(); _trackingState = ovrHmd_GetTrackingState(_hmd, _currentRenderFrameInfo.predictedDisplayTime); _currentRenderFrameInfo.rawRenderPose = _currentRenderFrameInfo.renderPose = toGlm(_trackingState.HeadPose.ThePose); - Lock lock(_mutex); - _frameInfos[frameIndex] = _currentRenderFrameInfo; - return true; + withRenderThreadLock([&]{ + _frameInfos[frameIndex] = _currentRenderFrameInfo; + }); + return Parent::beginFrameRender(frameIndex); } bool OculusLegacyDisplayPlugin::isSupported() const { diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h index 453a6f9168..6ffc1a7f44 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h @@ -23,6 +23,8 @@ public: bool isSupported() const override; const QString& getName() const override { return NAME; } + void init() override; + int getHmdScreen() const override; // Stereo specific methods diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index c5d3be25b2..5233ad644a 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -18,9 +18,11 @@ #include #include +#include #include #include #include +#include #include #include "OpenVrHelpers.h" @@ -29,19 +31,29 @@ Q_DECLARE_LOGGING_CATEGORY(displayplugins) const QString OpenVrDisplayPlugin::NAME("OpenVR (Vive)"); const QString StandingHMDSensorMode = "Standing HMD Sensor Mode"; // this probably shouldn't be hardcoded here -static vr::IVRCompositor* _compositor{ nullptr }; +static vr::IVRCompositor* _compositor { nullptr }; vr::TrackedDevicePose_t _trackedDevicePose[vr::k_unMaxTrackedDeviceCount]; + mat4 _trackedDevicePoseMat4[vr::k_unMaxTrackedDeviceCount]; vec3 _trackedDeviceLinearVelocities[vr::k_unMaxTrackedDeviceCount]; vec3 _trackedDeviceAngularVelocities[vr::k_unMaxTrackedDeviceCount]; + static mat4 _sensorResetMat; static std::array VR_EYES { { vr::Eye_Left, vr::Eye_Right } }; +bool _openVrDisplayActive { false }; bool OpenVrDisplayPlugin::isSupported() const { return openVrSupported(); } +void OpenVrDisplayPlugin::init() { + Plugin::init(); + + emit deviceConnected(getName()); +} + bool OpenVrDisplayPlugin::internalActivate() { + _openVrDisplayActive = true; _container->setIsOptionChecked(StandingHMDSensorMode, true); if (!_system) { @@ -57,16 +69,14 @@ bool OpenVrDisplayPlugin::internalActivate() { // left + right eyes _renderTargetSize.x *= 2; - { - Lock lock(_poseMutex); + withRenderThreadLock([&] { openvr_for_each_eye([&](vr::Hmd_Eye eye) { _eyeOffsets[eye] = toGlm(_system->GetEyeToHeadTransform(eye)); _eyeProjections[eye] = toGlm(_system->GetProjectionMatrix(eye, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP, vr::API_OpenGL)); }); // FIXME Calculate the proper combined projection by using GetProjectionRaw values from both eyes _cullingProjection = _eyeProjections[0]; - - } + }); _compositor = vr::VRCompositor(); Q_ASSERT(_compositor); @@ -94,6 +104,7 @@ bool OpenVrDisplayPlugin::internalActivate() { void OpenVrDisplayPlugin::internalDeactivate() { Parent::internalDeactivate(); + _openVrDisplayActive = false; _container->setIsOptionChecked(StandingHMDSensorMode, false); if (_system) { // Invalidate poses. It's fine if someone else sets these shared values, but we're about to stop updating them, and @@ -110,7 +121,7 @@ void OpenVrDisplayPlugin::internalDeactivate() { void OpenVrDisplayPlugin::customizeContext() { // Display plugins in DLLs must initialize glew locally static std::once_flag once; - std::call_once(once, []{ + std::call_once(once, [] { glewExperimental = true; GLenum err = glewInit(); glGetError(); // clear the potential error from glewExperimental @@ -120,9 +131,10 @@ void OpenVrDisplayPlugin::customizeContext() { } void OpenVrDisplayPlugin::resetSensors() { - Lock lock(_poseMutex); - glm::mat4 m = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); - _sensorResetMat = glm::inverse(cancelOutRollAndPitch(m)); + withRenderThreadLock([&] { + glm::mat4 m = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); + _sensorResetMat = glm::inverse(cancelOutRollAndPitch(m)); + }); } @@ -144,6 +156,24 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, _currentRenderFrameInfo.predictedDisplayTime, _trackedDevicePose, vr::k_unMaxTrackedDeviceCount); + + vr::TrackedDeviceIndex_t handIndices[2] { vr::k_unTrackedDeviceIndexInvalid, vr::k_unTrackedDeviceIndexInvalid }; + { + vr::TrackedDeviceIndex_t controllerIndices[2] ; + auto trackedCount = _system->GetSortedTrackedDeviceIndicesOfClass(vr::TrackedDeviceClass_Controller, controllerIndices, 2); + // Find the left and right hand controllers, if they exist + for (uint32_t i = 0; i < std::min(trackedCount, 2); ++i) { + if (_trackedDevicePose[i].bPoseIsValid) { + auto role = _system->GetControllerRoleForTrackedDeviceIndex(controllerIndices[i]); + if (vr::TrackedControllerRole_LeftHand == role) { + handIndices[0] = controllerIndices[i]; + } else if (vr::TrackedControllerRole_RightHand == role) { + handIndices[1] = controllerIndices[i]; + } + } + } + } + // copy and process predictedTrackedDevicePoses for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { _trackedDevicePoseMat4[i] = _sensorResetMat * toGlm(_trackedDevicePose[i].mDeviceToAbsoluteTracking); @@ -153,18 +183,39 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _currentRenderFrameInfo.rawRenderPose = toGlm(_trackedDevicePose[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking); _currentRenderFrameInfo.renderPose = _trackedDevicePoseMat4[vr::k_unTrackedDeviceIndex_Hmd]; - Lock lock(_mutex); - _frameInfos[frameIndex] = _currentRenderFrameInfo; - return true; + bool keyboardVisible = isOpenVrKeyboardShown(); + + std::array handPoses; + if (!keyboardVisible) { + for (int i = 0; i < 2; ++i) { + if (handIndices[i] == vr::k_unTrackedDeviceIndexInvalid) { + continue; + } + auto deviceIndex = handIndices[i]; + const mat4& mat = _trackedDevicePoseMat4[deviceIndex]; + const vec3& linearVelocity = _trackedDeviceLinearVelocities[deviceIndex]; + const vec3& angularVelocity = _trackedDeviceAngularVelocities[deviceIndex]; + auto correctedPose = openVrControllerPoseToHandPose(i == 0, mat, linearVelocity, angularVelocity); + static const glm::quat HAND_TO_LASER_ROTATION = glm::rotation(Vectors::UNIT_Z, Vectors::UNIT_NEG_Y); + handPoses[i] = glm::translate(glm::mat4(), correctedPose.translation) * glm::mat4_cast(correctedPose.rotation * HAND_TO_LASER_ROTATION); + } + } + + withRenderThreadLock([&] { + _uiModelTransform = DependencyManager::get()->getModelTransform(); + // Make controller poses available to the presentation thread + _handPoses = handPoses; + _frameInfos[frameIndex] = _currentRenderFrameInfo; + }); + return Parent::beginFrameRender(frameIndex); } void OpenVrDisplayPlugin::hmdPresent() { - PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentPresentFrameIndex) // Flip y-axis since GL UV coords are backwards. - static vr::VRTextureBounds_t leftBounds{ 0, 0, 0.5f, 1 }; - static vr::VRTextureBounds_t rightBounds{ 0.5f, 0, 1, 1 }; + static vr::VRTextureBounds_t leftBounds { 0, 0, 0.5f, 1 }; + static vr::VRTextureBounds_t rightBounds { 0.5f, 0, 1, 1 }; vr::Texture_t texture { (void*)oglplus::GetName(_compositeFramebuffer->color), vr::API_OpenGL, vr::ColorSpace_Auto }; @@ -185,6 +236,10 @@ bool OpenVrDisplayPlugin::isHmdMounted() const { } void OpenVrDisplayPlugin::updatePresentPose() { + mat4 sensorResetMat; + withPresentThreadLock([&] { + sensorResetMat = _sensorResetMat; + }); { float fSecondsSinceLastVsync; _system->GetTimeSinceLastVsync(&fSecondsSinceLastVsync, nullptr); @@ -196,9 +251,32 @@ void OpenVrDisplayPlugin::updatePresentPose() { _system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, fPredictedSecondsFromNow, &pose, 1); _currentPresentFrameInfo.rawPresentPose = toGlm(pose.mDeviceToAbsoluteTracking); } - _currentPresentFrameInfo.presentPose = _sensorResetMat * _currentPresentFrameInfo.rawPresentPose; + _currentPresentFrameInfo.presentPose = sensorResetMat * _currentPresentFrameInfo.rawPresentPose; mat3 renderRotation(_currentPresentFrameInfo.rawRenderPose); mat3 presentRotation(_currentPresentFrameInfo.rawPresentPose); _currentPresentFrameInfo.presentReprojection = glm::mat3(glm::inverse(renderRotation) * presentRotation); } +bool OpenVrDisplayPlugin::suppressKeyboard() { + if (isOpenVrKeyboardShown()) { + return false; + } + if (!_keyboardSupressionCount.fetch_add(1)) { + disableOpenVrKeyboard(); + } + return true; +} + +void OpenVrDisplayPlugin::unsuppressKeyboard() { + if (_keyboardSupressionCount == 0) { + qWarning() << "Attempted to unsuppress a keyboard that was not suppressed"; + return; + } + if (1 == _keyboardSupressionCount.fetch_sub(1)) { + enableOpenVrKeyboard(_container); + } +} + +bool OpenVrDisplayPlugin::isKeyboardVisible() { + return isOpenVrKeyboardShown(); +} diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index fda5e37c2a..fca4dab9e9 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -21,6 +21,8 @@ public: bool isSupported() const override; const QString& getName() const override { return NAME; } + void init() override; + float getTargetFrameRate() const override { return TARGET_RATE_OpenVr; } void customizeContext() override; @@ -30,6 +32,10 @@ public: bool beginFrameRender(uint32_t frameIndex) override; void cycleDebugOutput() override { _lockCurrentTexture = !_lockCurrentTexture; } + bool suppressKeyboard() override; + void unsuppressKeyboard() override; + bool isKeyboardVisible() override; + protected: bool internalActivate() override; void internalDeactivate() override; @@ -39,9 +45,10 @@ protected: bool isHmdMounted() const override; void postPreview() override; + private: vr::IVRSystem* _system { nullptr }; std::atomic _hmdActivityLevel { vr::k_EDeviceActivityLevel_Unknown }; + std::atomic _keyboardSupressionCount{ 0 }; static const QString NAME; - mutable Mutex _poseMutex; }; diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 3c765c3fa8..e71c8942d6 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -14,8 +14,16 @@ #include #include #include +#include +#include #include +#include +#include +#include +#include +#include +#include "../../interface/src/Menu.h" Q_DECLARE_LOGGING_CATEGORY(displayplugins) Q_LOGGING_CATEGORY(displayplugins, "hifi.plugins.display") @@ -84,6 +92,146 @@ void releaseOpenVrSystem() { } } +static char textArray[8192]; + +static QMetaObject::Connection _focusConnection, _focusTextConnection, _overlayMenuConnection; +extern bool _openVrDisplayActive; +static vr::IVROverlay* _overlay { nullptr }; +static QObject* _keyboardFocusObject { nullptr }; +static QString _existingText; +static Qt::InputMethodHints _currentHints; +extern vr::TrackedDevicePose_t _trackedDevicePose[vr::k_unMaxTrackedDeviceCount]; +static bool _keyboardShown { false }; +static bool _overlayRevealed { false }; +static const uint32_t SHOW_KEYBOARD_DELAY_MS = 400; + +void showOpenVrKeyboard(bool show = true) { + if (!_overlay) { + return; + } + + if (show) { + // To avoid flickering the keyboard when a text element is only briefly selected, + // show the keyboard asynchrnously after a very short delay, but only after we check + // that the current focus object is still one that is text enabled + QTimer::singleShot(SHOW_KEYBOARD_DELAY_MS, [] { + auto offscreenUi = DependencyManager::get(); + auto currentFocus = offscreenUi->getWindow()->focusObject(); + QInputMethodQueryEvent query(Qt::ImEnabled | Qt::ImQueryInput | Qt::ImHints); + qApp->sendEvent(currentFocus, &query); + // Current focus isn't text enabled, bail early. + if (!query.value(Qt::ImEnabled).toBool()) { + return; + } + // We're going to show the keyboard now... + _keyboardFocusObject = currentFocus; + _currentHints = Qt::InputMethodHints(query.value(Qt::ImHints).toUInt()); + vr::EGamepadTextInputMode inputMode = vr::k_EGamepadTextInputModeNormal; + if (_currentHints & Qt::ImhHiddenText) { + inputMode = vr::k_EGamepadTextInputModePassword; + } + vr::EGamepadTextInputLineMode lineMode = vr::k_EGamepadTextInputLineModeSingleLine; + if (_currentHints & Qt::ImhMultiLine) { + lineMode = vr::k_EGamepadTextInputLineModeMultipleLines; + } + _existingText = query.value(Qt::ImSurroundingText).toString(); + + auto showKeyboardResult = _overlay->ShowKeyboard(inputMode, lineMode, "Keyboard", 1024, + _existingText.toLocal8Bit().toStdString().c_str(), false, 0); + + if (vr::VROverlayError_None == showKeyboardResult) { + _keyboardShown = true; + // Try to position the keyboard slightly below where the user is looking. + mat4 headPose = cancelOutRollAndPitch(toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking)); + mat4 keyboardTransform = glm::translate(headPose, vec3(0, -0.5, -1)); + keyboardTransform = keyboardTransform * glm::rotate(mat4(), 3.14159f / 4.0f, vec3(-1, 0, 0)); + auto keyboardTransformVr = toOpenVr(keyboardTransform); + _overlay->SetKeyboardTransformAbsolute(vr::ETrackingUniverseOrigin::TrackingUniverseStanding, &keyboardTransformVr); + } + }); + } else { + _keyboardFocusObject = nullptr; + if (_keyboardShown) { + _overlay->HideKeyboard(); + _keyboardShown = false; + } + } +} + +void finishOpenVrKeyboardInput() { + auto offscreenUi = DependencyManager::get(); + auto chars = _overlay->GetKeyboardText(textArray, 8192); + auto newText = QString(QByteArray(textArray, chars)); + _keyboardFocusObject->setProperty("text", newText); + //// TODO modify the new text to match the possible input hints: + //// ImhDigitsOnly ImhFormattedNumbersOnly ImhUppercaseOnly ImhLowercaseOnly + //// ImhDialableCharactersOnly ImhEmailCharactersOnly ImhUrlCharactersOnly ImhLatinOnly + //QInputMethodEvent event(_existingText, QList()); + //event.setCommitString(newText, 0, _existingText.size()); + //qApp->sendEvent(_keyboardFocusObject, &event); + // Simulate an enter press on the top level window to trigger the action + if (0 == (_currentHints & Qt::ImhMultiLine)) { + qApp->sendEvent(offscreenUi->getWindow(), &QKeyEvent(QEvent::KeyPress, Qt::Key_Return, Qt::KeyboardModifiers(), QString("\n"))); + qApp->sendEvent(offscreenUi->getWindow(), &QKeyEvent(QEvent::KeyRelease, Qt::Key_Return, Qt::KeyboardModifiers())); + } +} + +static const QString DEBUG_FLAG("HIFI_DISABLE_STEAM_VR_KEYBOARD"); +bool disableSteamVrKeyboard = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); + +void enableOpenVrKeyboard(PluginContainer* container) { + if (disableSteamVrKeyboard) { + return; + } + auto offscreenUi = DependencyManager::get(); + _overlay = vr::VROverlay(); + + + auto menu = container->getPrimaryMenu(); + auto action = menu->getActionForOption(MenuOption::Overlays); + + // When the overlays are revealed, suppress the keyboard from appearing on text focus for a tenth of a second. + _overlayMenuConnection = QObject::connect(action, &QAction::triggered, [action] { + if (action->isChecked()) { + _overlayRevealed = true; + const int KEYBOARD_DELAY_MS = 100; + QTimer::singleShot(KEYBOARD_DELAY_MS, [&] { _overlayRevealed = false; }); + } + }); + + _focusConnection = QObject::connect(offscreenUi->getWindow(), &QQuickWindow::focusObjectChanged, [](QObject* object) { + if (object != _keyboardFocusObject) { + showOpenVrKeyboard(false); + } + }); + + _focusTextConnection = QObject::connect(offscreenUi.data(), &OffscreenUi::focusTextChanged, [](bool focusText) { + if (_openVrDisplayActive) { + if (_overlayRevealed) { + // suppress at most one text focus event + _overlayRevealed = false; + return; + } + showOpenVrKeyboard(focusText); + } + }); +} + + +void disableOpenVrKeyboard() { + if (disableSteamVrKeyboard) { + return; + } + QObject::disconnect(_overlayMenuConnection); + QObject::disconnect(_focusTextConnection); + QObject::disconnect(_focusConnection); +} + +bool isOpenVrKeyboardShown() { + return _keyboardShown; +} + + void handleOpenVrEvents() { if (!activeHmd) { return; @@ -101,6 +249,15 @@ void handleOpenVrEvents() { QMetaObject::invokeMethod(qApp, "quit"); break; + case vr::VREvent_KeyboardDone: + finishOpenVrKeyboardInput(); + + // FALL THROUGH + case vr::VREvent_KeyboardClosed: + _keyboardFocusObject = nullptr; + _keyboardShown = false; + break; + default: break; } @@ -108,3 +265,87 @@ void handleOpenVrEvents() { } } + +controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity) { + // When the sensor-to-world rotation is identity the coordinate axes look like this: + // + // user + // forward + // -z + // | + // y| user + // y o----x right + // o-----x user + // | up + // | + // z + // + // Rift + + // From ABOVE the hand canonical axes looks like this: + // + // | | | | y | | | | + // | | | | | | | | | + // | | | | | + // |left | / x---- + \ |right| + // | _/ z \_ | + // | | | | + // | | | | + // + + // So when the user is in Rift space facing the -zAxis with hands outstretched and palms down + // the rotation to align the Touch axes with those of the hands is: + // + // touchToHand = halfTurnAboutY * quaterTurnAboutX + + // Due to how the Touch controllers fit into the palm there is an offset that is different for each hand. + // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that + // the combination (measurement * offset) is identity at this orientation. + // + // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) + // + // An approximate offset for the Touch can be obtained by inspection: + // + // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) * glm::angleAxis(PI/4.0f, xAxis)) + // + // So the full equation is: + // + // Q = combinedMeasurement * touchToHand + // + // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) + // + // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) + static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); + static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); + static const glm::quat touchToHand = yFlip * quarterX; + + static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); + + static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ * eighthX) * touchToHand; + static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ * eighthX) * touchToHand; + + static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches + static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET * 2.0f); + static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; + static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; + + auto translationOffset = (isLeftHand ? leftTranslationOffset : rightTranslationOffset); + auto rotationOffset = (isLeftHand ? leftRotationOffset : rightRotationOffset); + + glm::vec3 position = extractTranslation(mat); + glm::quat rotation = glm::normalize(glm::quat_cast(mat)); + + position += rotation * translationOffset; + rotation = rotation * rotationOffset; + + // transform into avatar frame + auto result = controller::Pose(position, rotation); + // handle change in velocity due to translationOffset + result.velocity = linearVelocity + glm::cross(angularVelocity, position - extractTranslation(mat)); + result.angularVelocity = angularVelocity; + return result; +} diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index 1e5914844c..19c9cbfff5 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -12,12 +12,19 @@ #include #include +#include +#include + bool openVrSupported(); vr::IVRSystem* acquireOpenVrSystem(); void releaseOpenVrSystem(); void handleOpenVrEvents(); bool openVrQuitRequested(); +void enableOpenVrKeyboard(PluginContainer* container); +void disableOpenVrKeyboard(); +bool isOpenVrKeyboardShown(); + template void openvr_for_each_eye(F f) { @@ -41,3 +48,15 @@ inline mat4 toGlm(const vr::HmdMatrix34_t& m) { m.m[0][3], m.m[1][3], m.m[2][3], 1.0f); return result; } + +inline vr::HmdMatrix34_t toOpenVr(const mat4& m) { + vr::HmdMatrix34_t result; + for (uint8_t i = 0; i < 3; ++i) { + for (uint8_t j = 0; j < 4; ++j) { + result.m[i][j] = m[j][i]; + } + } + return result; +} + +controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity); diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 12813dae93..4a515978c3 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -11,8 +11,6 @@ #include "ViveControllerManager.h" -#include - #include #include #include @@ -22,6 +20,7 @@ #include #include #include +#include #include @@ -38,10 +37,6 @@ vr::IVRSystem* acquireOpenVrSystem(); void releaseOpenVrSystem(); -static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches -static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET * 2.0f); static const char* CONTROLLER_MODEL_STRING = "vr_controller_05_wireless_b"; static const QString MENU_PARENT = "Avatar"; @@ -68,6 +63,8 @@ bool ViveControllerManager::activate() { } Q_ASSERT(_system); + enableOpenVrKeyboard(_container); + // OpenVR provides 3d mesh representations of the controllers // Disabled controller rendering code /* @@ -131,6 +128,8 @@ bool ViveControllerManager::activate() { void ViveControllerManager::deactivate() { InputPlugin::deactivate(); + disableOpenVrKeyboard(); + _container->removeMenuItem(MENU_NAME, RENDER_CONTROLLERS); _container->removeMenu(MENU_PATH); @@ -229,7 +228,6 @@ void ViveControllerManager::pluginUpdate(float deltaTime, const controller::Inpu if (!_registeredWithInputMapper && _inputDevice->_trackedControllers > 0) { userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; - UserActivityLogger::getInstance().connectedDevice("spatial_controller", "steamVR"); } } @@ -237,6 +235,12 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle _poseStateMap.clear(); _buttonPressedMap.clear(); + // While the keyboard is open, we defer strictly to the keyboard values + if (isOpenVrKeyboardShown()) { + _axisStateMap.clear(); + return; + } + PerformanceTimer perfTimer("ViveControllerManager::update"); auto leftHandDeviceIndex = _system->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_LeftHand); @@ -295,20 +299,24 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u } // pseudo buttons the depend on both of the above for-loops - partitionTouchpad(controller::LS, controller::LX, controller::LY, controller::LS_CENTER, controller::LS_OUTER); - partitionTouchpad(controller::RS, controller::RX, controller::RY, controller::RS_CENTER, controller::RS_OUTER); + partitionTouchpad(controller::LS, controller::LX, controller::LY, controller::LS_CENTER, controller::LS_X, controller::LS_Y); + partitionTouchpad(controller::RS, controller::RX, controller::RY, controller::RS_CENTER, controller::RS_X, controller::RS_Y); } } } -void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPseudoButton, int outerPseudoButton) { +void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPseudoButton, int xPseudoButton, int yPseudoButton) { // Populate the L/RS_CENTER/OUTER pseudo buttons, corresponding to a partition of the L/RS space based on the X/Y values. const float CENTER_DEADBAND = 0.6f; + const float DIAGONAL_DIVIDE_IN_RADIANS = PI / 4.0f; if (_buttonPressedMap.find(sButton) != _buttonPressedMap.end()) { float absX = abs(_axisStateMap[xAxis]); float absY = abs(_axisStateMap[yAxis]); - bool isCenter = (absX < CENTER_DEADBAND) && (absY < CENTER_DEADBAND); // square deadband - _buttonPressedMap.insert(isCenter ? centerPseudoButton : outerPseudoButton); + glm::vec2 cartesianQuadrantI(absX, absY); + float angle = glm::atan(cartesianQuadrantI.y / cartesianQuadrantI.x); + float radius = glm::length(cartesianQuadrantI); + bool isCenter = radius < CENTER_DEADBAND; + _buttonPressedMap.insert(isCenter ? centerPseudoButton : ((angle < DIAGONAL_DIVIDE_IN_RADIANS) ? xPseudoButton :yPseudoButton)); } } @@ -375,86 +383,11 @@ void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand) { - // When the sensor-to-world rotation is identity the coordinate axes look like this: - // - // user - // forward - // -z - // | - // y| user - // y o----x right - // o-----x user - // | up - // | - // z - // - // Vive - // - - // From ABOVE the hand canonical axes looks like this: - // - // | | | | y | | | | - // | | | | | | | | | - // | | | | | - // |left | / x---- + \ |right| - // | _/ z \_ | - // | | | | - // | | | | - // - - // So when the user is standing in Vive space facing the -zAxis with hands outstretched and palms down - // the rotation to align the Vive axes with those of the hands is: - // - // QviveToHand = halfTurnAboutY * quaterTurnAboutX - - // Due to how the Vive controllers fit into the palm there is an offset that is different for each hand. - // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that - // the combination (measurement * offset) is identity at this orientation. - // - // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) - // - // An approximate offset for the Vive can be obtained by inspection: - // - // Qoffset = glm::inverse(glm::angleAxis(sign * PI/4.0f, zAxis) * glm::angleAxis(PI/2.0f, xAxis)) - // - // So the full equation is: - // - // Q = combinedMeasurement * viveToHand - // - // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) - // - // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) - - static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); - static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); - static const glm::quat viveToHand = yFlip * quarterX; - - static const glm::quat leftQuaterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); - static const glm::quat rightQuaterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); - static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); - - static const glm::quat leftRotationOffset = glm::inverse(leftQuaterZ * eighthX) * viveToHand; - static const glm::quat rightRotationOffset = glm::inverse(rightQuaterZ * eighthX) * viveToHand; - - static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; - static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; - - auto translationOffset = (isLeftHand ? leftTranslationOffset : rightTranslationOffset); - auto rotationOffset = (isLeftHand ? leftRotationOffset : rightRotationOffset); - - glm::vec3 position = extractTranslation(mat); - glm::quat rotation = glm::normalize(glm::quat_cast(mat)); - - position += rotation * translationOffset; - rotation = rotation * rotationOffset; + auto pose = openVrControllerPoseToHandPose(isLeftHand, mat, linearVelocity, angularVelocity); // transform into avatar frame glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; - auto avatarPose = controller::Pose(position, rotation); - // handle change in velocity due to translationOffset - avatarPose.velocity = linearVelocity + glm::cross(angularVelocity, position - extractTranslation(mat)); - avatarPose.angularVelocity = angularVelocity; - _poseStateMap[isLeftHand ? controller::LEFT_HAND : controller::RIGHT_HAND] = avatarPose.transform(controllerToAvatar); + _poseStateMap[isLeftHand ? controller::LEFT_HAND : controller::RIGHT_HAND] = pose.transform(controllerToAvatar); } bool ViveControllerManager::InputDevice::triggerHapticPulse(float strength, float duration, controller::Hand hand) { @@ -524,9 +457,11 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI makePair(RS, "RS"), // Differentiate where we are in the touch pad click makePair(LS_CENTER, "LSCenter"), - makePair(LS_OUTER, "LSOuter"), + makePair(LS_X, "LSX"), + makePair(LS_Y, "LSY"), makePair(RS_CENTER, "RSCenter"), - makePair(RS_OUTER, "RSOuter"), + makePair(RS_X, "RSX"), + makePair(RS_Y, "RSY"), // triggers makePair(LT, "LT"), diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 3a2ef1573f..95ff2f881a 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -64,7 +64,7 @@ private: void handleAxisEvent(float deltaTime, uint32_t axis, float x, float y, bool isLeftHand); void handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand); - void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int outerPseudoButton); + void partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int xPseudoButton, int yPseudoButton); class FilteredStick { public: diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 2a050d183e..bbbe049f39 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -12,6 +12,9 @@ Script.load("system/progress.js"); Script.load("system/away.js"); Script.load("system/users.js"); +Script.load("system/mute.js"); +Script.load("system/goto.js"); +Script.load("system/hmd.js"); Script.load("system/examples.js"); Script.load("system/edit.js"); Script.load("system/selectAudioDevice.js"); diff --git a/scripts/developer/tests/toolbarTest.js b/scripts/developer/tests/toolbarTest.js new file mode 100644 index 0000000000..e21fbd8e19 --- /dev/null +++ b/scripts/developer/tests/toolbarTest.js @@ -0,0 +1,118 @@ +var isActive = false; + +var toolBar = (function() { + var that = {}, + toolBar, + activeButton, + newModelButton, + newCubeButton, + newSphereButton, + newLightButton, + newTextButton, + newWebButton, + newZoneButton, + newParticleButton + + var toolIconUrl = Script.resolvePath("../../system/assets/images/tools/"); + + function initialize() { + print("Toolbars: " + Toolbars); + toolBar = Toolbars.getToolbar("highfidelity.edit.toolbar"); + print("Toolbar: " + toolBar); + activeButton = toolBar.addButton({ + objectName: "activeButton", + imageURL: toolIconUrl + "edit-01.svg", + visible: true, + alpha: 0.9, + }); + + print("Button " + activeButton); + print("Button signal " + activeButton.clicked); + activeButton.clicked.connect(function(){ + print("Clicked on button " + isActive); + that.setActive(!isActive); + }); + + newModelButton = toolBar.addButton({ + objectName: "newModelButton", + imageURL: toolIconUrl + "model-01.svg", + alpha: 0.9, + visible: false + }); + + newCubeButton = toolBar.addButton({ + objectName: "newCubeButton", + imageURL: toolIconUrl + "cube-01.svg", + alpha: 0.9, + visible: false + }); + + newSphereButton = toolBar.addButton({ + objectName: "newSphereButton", + imageURL: toolIconUrl + "sphere-01.svg", + alpha: 0.9, + visible: false + }); + + newLightButton = toolBar.addButton({ + objectName: "newLightButton", + imageURL: toolIconUrl + "light-01.svg", + alpha: 0.9, + visible: false + }); + + newTextButton = toolBar.addButton({ + objectName: "newTextButton", + imageURL: toolIconUrl + "text-01.svg", + alpha: 0.9, + visible: false + }); + + newWebButton = toolBar.addButton({ + objectName: "newWebButton", + imageURL: toolIconUrl + "web-01.svg", + alpha: 0.9, + visible: false + }); + + newZoneButton = toolBar.addButton({ + objectName: "newZoneButton", + imageURL: toolIconUrl + "zone-01.svg", + alpha: 0.9, + visible: false + }); + + newParticleButton = toolBar.addButton({ + objectName: "newParticleButton", + imageURL: toolIconUrl + "particle-01.svg", + alpha: 0.9, + visible: false + }); + + that.setActive(false); + newModelButton.clicked(); + } + + that.setActive = function(active) { + if (active != isActive) { + isActive = active; + that.showTools(isActive); + } + }; + + // Sets visibility of tool buttons, excluding the power button + that.showTools = function(doShow) { + newModelButton.writeProperty('visible', doShow); + newCubeButton.writeProperty('visible', doShow); + newSphereButton.writeProperty('visible', doShow); + newLightButton.writeProperty('visible', doShow); + newTextButton.writeProperty('visible', doShow); + newWebButton.writeProperty('visible', doShow); + newZoneButton.writeProperty('visible', doShow); + newModelButton.writeProperty('visible', doShow); + newParticleButton.writeProperty('visible', doShow); + }; + + initialize(); + return that; +}()); diff --git a/scripts/system/assets/images/tools/hmd-switch-01.svg b/scripts/system/assets/images/tools/hmd-switch-01.svg new file mode 100644 index 0000000000..31389d355c --- /dev/null +++ b/scripts/system/assets/images/tools/hmd-switch-01.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/tools/microphone.svg b/scripts/system/assets/images/tools/microphone.svg new file mode 100644 index 0000000000..bd5e8afac7 --- /dev/null +++ b/scripts/system/assets/images/tools/microphone.svg @@ -0,0 +1,13 @@ + + + image/svg+xml + + + Layer 1 + + + + Mute + + + \ No newline at end of file diff --git a/scripts/system/away.js b/scripts/system/away.js index 38b0f13c00..04263d4223 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -158,6 +158,8 @@ function goAway() { return; } + UserActivityLogger.toggledAway(true); + isAway = true; print('going "away"'); wasMuted = AudioDevice.getMuted(); @@ -176,9 +178,11 @@ function goAway() { // tell the Reticle, we want to stop capturing the mouse until we come back Reticle.allowMouseCapture = false; - if (HMD.active) { - Reticle.visible = false; - } + // Allow users to find their way to other applications, our menus, etc. + // For desktop, that means we want the reticle visible. + // For HMD, the hmd preview will show the system mouse because of allowMouseCapture, + // but we want to turn off our Reticle so that we don't get two in preview and a stuck one in headset. + Reticle.visible = !HMD.active; wasHmdMounted = safeGetHMDMounted(); // always remember the correct state avatarPosition = MyAvatar.position; @@ -189,6 +193,9 @@ function goActive() { if (!isAway) { return; } + + UserActivityLogger.toggledAway(false); + isAway = false; print('going "active"'); if (!wasMuted) { diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 986a4c0722..7706132c58 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -10,11 +10,10 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, randFloat, randInt, pointInExtents, vec3equal, setEntityCustomData, getEntityCustomData */ +/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr */ Script.include("/~/system/libraries/utils.js"); - // // add lines where the hand ray picking is happening // @@ -38,6 +37,7 @@ var THUMB_ON_VALUE = 0.5; var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only use head for search/move. var PICK_WITH_HAND_RAY = true; +var DROP_WITHOUT_SHAKE = false; // // distant manipulation @@ -47,9 +47,7 @@ var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified -var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did var MOVE_WITH_HEAD = true; // experimental head-control of distantly held objects -var FAR_TO_NEAR_GRAB_PADDING_FACTOR = 1.2; var NO_INTERSECT_COLOR = { red: 10, @@ -81,17 +79,11 @@ var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-g var SHOW_GRAB_SPHERE = false; // draw a green sphere to show the grab search position and size var CHECK_TOO_FAR_UNEQUIP_TIME = 1.0; // seconds -// -// equip -// - -var EQUIP_SPRING_SHUTOFF_DISTANCE = 0.05; -var EQUIP_SPRING_TIMEFRAME = 0.4; // how quickly objects move to their new position - // // other constants // +var HOTSPOT_DRAW_DISTANCE = 10; var RIGHT_HAND = 1; var LEFT_HAND = 0; @@ -133,8 +125,6 @@ var DEFAULT_GRABBABLE_DATA = { disableReleaseVelocity: false }; - - // sometimes we want to exclude objects from being picked var USE_BLACKLIST = true; var blacklist = []; @@ -147,29 +137,20 @@ var USE_ENTITY_LINES_FOR_MOVING = false; var USE_OVERLAY_LINES_FOR_MOVING = false; var USE_PARTICLE_BEAM_FOR_MOVING = true; - var USE_SPOTLIGHT = false; var USE_POINTLIGHT = false; +var FORBIDDEN_GRAB_NAMES = ["Grab Debug Entity", "grab pointer"]; +var FORBIDDEN_GRAB_TYPES = ['Unknown', 'Light', 'PolyLine', 'Zone']; + // states for the state machine var STATE_OFF = 0; var STATE_SEARCHING = 1; -var STATE_HOLD_SEARCHING = 2; -var STATE_DISTANCE_HOLDING = 3; -var STATE_CONTINUE_DISTANCE_HOLDING = 4; -var STATE_NEAR_GRABBING = 5; -var STATE_CONTINUE_NEAR_GRABBING = 6; -var STATE_NEAR_TRIGGER = 7; -var STATE_CONTINUE_NEAR_TRIGGER = 8; -var STATE_FAR_TRIGGER = 9; -var STATE_CONTINUE_FAR_TRIGGER = 10; -var STATE_RELEASE = 11; -var STATE_EQUIP = 12; -var STATE_HOLD = 13; -var STATE_CONTINUE_HOLD = 14; -var STATE_CONTINUE_EQUIP = 15; -var STATE_WAITING_FOR_RELEASE_THUMB_RELEASE = 16; -var STATE_WAITING_FOR_EQUIP_THUMB_RELEASE = 17; +var STATE_DISTANCE_HOLDING = 2; +var STATE_NEAR_GRABBING = 3; +var STATE_NEAR_TRIGGER = 4; +var STATE_FAR_TRIGGER = 5; +var STATE_HOLD = 6; // "collidesWith" is specified by comma-separated list of group names // the possible group names are: static, dynamic, kinematic, myAvatar, otherAvatar @@ -179,47 +160,47 @@ var COLLIDES_WITH_WHILE_MULTI_GRABBED = "dynamic"; var HEART_BEAT_INTERVAL = 5 * MSECS_PER_SEC; var HEART_BEAT_TIMEOUT = 15 * MSECS_PER_SEC; -function stateToName(state) { - switch (state) { - case STATE_OFF: - return "off"; - case STATE_SEARCHING: - return "searching"; - case STATE_HOLD_SEARCHING: - return "hold_searching"; - case STATE_DISTANCE_HOLDING: - return "distance_holding"; - case STATE_CONTINUE_DISTANCE_HOLDING: - return "continue_distance_holding"; - case STATE_NEAR_GRABBING: - return "near_grabbing"; - case STATE_CONTINUE_NEAR_GRABBING: - return "continue_near_grabbing"; - case STATE_NEAR_TRIGGER: - return "near_trigger"; - case STATE_CONTINUE_NEAR_TRIGGER: - return "continue_near_trigger"; - case STATE_FAR_TRIGGER: - return "far_trigger"; - case STATE_CONTINUE_FAR_TRIGGER: - return "continue_far_trigger"; - case STATE_RELEASE: - return "release"; - case STATE_EQUIP: - return "equip"; - case STATE_HOLD: - return "hold"; - case STATE_CONTINUE_HOLD: - return "continue_hold"; - case STATE_CONTINUE_EQUIP: - return "continue_equip"; - case STATE_WAITING_FOR_EQUIP_THUMB_RELEASE: - return "waiting_for_equip_thumb_release"; - case STATE_WAITING_FOR_RELEASE_THUMB_RELEASE: - return "waiting_for_release_thumb_release"; - } +var CONTROLLER_STATE_MACHINE = {}; - return "unknown"; +CONTROLLER_STATE_MACHINE[STATE_OFF] = { + name: "off", + enterMethod: "offEnter", + updateMethod: "off" +}; +CONTROLLER_STATE_MACHINE[STATE_SEARCHING] = { + name: "searching", + updateMethod: "search", + enterMethod: "searchEnter", + exitMethod: "searchExit" +}; +CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { + name: "distance_holding", + enterMethod: "distanceHoldingEnter", + updateMethod: "distanceHolding" +}; +CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { + name: "near_grabbing", + enterMethod: "nearGrabbingEnter", + updateMethod: "nearGrabbing" +}; +CONTROLLER_STATE_MACHINE[STATE_HOLD] = { + name: "hold", + enterMethod: "nearGrabbingEnter", + updateMethod: "nearGrabbing" +}; +CONTROLLER_STATE_MACHINE[STATE_NEAR_TRIGGER] = { + name: "trigger", + enterMethod: "nearTriggerEnter", + updateMethod: "nearTrigger" +}; +CONTROLLER_STATE_MACHINE[STATE_FAR_TRIGGER] = { + name: "far_trigger", + enterMethod: "farTriggerEnter", + updateMethod: "farTrigger" +}; + +function stateToName(state) { + return CONTROLLER_STATE_MACHINE[state] ? CONTROLLER_STATE_MACHINE[state].name : "???"; } function getTag() { @@ -249,6 +230,70 @@ function entityIsGrabbedByOther(entityID) { return false; } +function propsArePhysical(props) { + if (!props.dynamic) { + return false; + } + var isPhysical = (props.shapeType && props.shapeType != 'none'); + return isPhysical; +} + +// If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here, +// and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. +var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; +function isIn2DMode() { + // In this version, we make our own determination of whether we're aimed a HUD element, + // because other scripts (such as handControllerPointer) might be using some other visualization + // instead of setting Reticle.visible. + return EXTERNALLY_MANAGED_2D_MINOR_MODE && (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position)); +} +function restore2DMode() { + if (!EXTERNALLY_MANAGED_2D_MINOR_MODE) { + Reticle.setVisible(true); + } +} + +// constructor +function EntityPropertiesCache() { + this.cache = {}; +} +EntityPropertiesCache.prototype.clear = function() { + this.cache = {}; +}; +EntityPropertiesCache.prototype.findEntities = function(position, radius) { + var entities = Entities.findEntities(position, radius); + var _this = this; + entities.forEach(function (x) { + _this.addEntity(x); + }); +}; +EntityPropertiesCache.prototype.addEntity = function(entityID) { + var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES); + var grabbableProps = getEntityCustomData(GRABBABLE_DATA_KEY, entityID, DEFAULT_GRABBABLE_DATA); + var grabProps = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); + var wearableProps = getEntityCustomData("wearable", entityID, {}); + this.cache[entityID] = { props: props, grabbableProps: grabbableProps, grabProps: grabProps, wearableProps: wearableProps }; +}; +EntityPropertiesCache.prototype.getEntities = function() { + return Object.keys(this.cache); +} +EntityPropertiesCache.prototype.getProps = function(entityID) { + var obj = this.cache[entityID] + return obj ? obj.props : undefined; +}; +EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) { + var obj = this.cache[entityID] + return obj ? obj.grabbableProps : undefined; +}; +EntityPropertiesCache.prototype.getGrabProps = function(entityID) { + var obj = this.cache[entityID] + return obj ? obj.grabProps : undefined; +}; +EntityPropertiesCache.prototype.getWearableProps = function(entityID) { + var obj = this.cache[entityID] + return obj ? obj.wearableProps : undefined; +}; + function MyController(hand) { this.hand = hand; if (this.hand === RIGHT_HAND) { @@ -285,6 +330,8 @@ function MyController(hand) { this.overlayLine = null; this.searchSphere = null; + this.waitForTriggerRelease = false; + // how far from camera to search intersection? var DEFAULT_SEARCH_SPHERE_DISTANCE = 1000; this.intersectionDistance = 0.0; @@ -297,57 +344,36 @@ function MyController(hand) { this.lastPickTime = 0; this.lastUnequipCheckTime = 0; + this.entityPropertyCache = new EntityPropertiesCache(); + var _this = this; - this.update = function() { + var suppressedIn2D = [STATE_OFF, STATE_SEARCHING]; + this.ignoreInput = function () { + // We've made the decision to use 'this' for new code, even though it is fragile, + // in order to keep/ the code uniform without making any no-op line changes. + return (-1 !== suppressedIn2D.indexOf(this.state)) && isIn2DMode(); + }; + + this.update = function(deltaTime) { this.updateSmoothedTrigger(); - switch (this.state) { - case STATE_OFF: - this.off(); - break; - case STATE_SEARCHING: - case STATE_HOLD_SEARCHING: - this.search(); - break; - case STATE_DISTANCE_HOLDING: - this.distanceHolding(); - break; - case STATE_CONTINUE_DISTANCE_HOLDING: - this.continueDistanceHolding(); - break; - case STATE_NEAR_GRABBING: - case STATE_EQUIP: - case STATE_HOLD: - this.nearGrabbing(); - break; - case STATE_WAITING_FOR_EQUIP_THUMB_RELEASE: - this.waitingForEquipThumbRelease(); - break; - case STATE_WAITING_FOR_RELEASE_THUMB_RELEASE: - this.waitingForReleaseThumbRelease(); - break; - case STATE_CONTINUE_NEAR_GRABBING: - case STATE_CONTINUE_HOLD: - case STATE_CONTINUE_EQUIP: - this.continueNearGrabbing(); - break; - case STATE_NEAR_TRIGGER: - this.nearTrigger(); - break; - case STATE_CONTINUE_NEAR_TRIGGER: - this.continueNearTrigger(); - break; - case STATE_FAR_TRIGGER: - this.farTrigger(); - break; - case STATE_CONTINUE_FAR_TRIGGER: - this.continueFarTrigger(); - break; - case STATE_RELEASE: - this.release(); - break; + if (this.ignoreInput()) { + this.turnOffVisualizations(); + return; + } + + if (CONTROLLER_STATE_MACHINE[this.state]) { + var updateMethodName = CONTROLLER_STATE_MACHINE[this.state].updateMethod; + var updateMethod = this[updateMethodName]; + if (updateMethod) { + updateMethod.call(this, deltaTime); + } else { + print("WARNING: could not find updateMethod for state " + stateToName(this.state)); + } + } else { + print("WARNING: could not find state " + this.state + " in state machine"); } }; @@ -356,13 +382,37 @@ function MyController(hand) { Entities.callEntityMethod(this.grabbedEntity, entityMethodName, args); } - this.setState = function(newState) { + this.setState = function(newState, reason) { this.grabSphereOff(); if (WANT_DEBUG || WANT_DEBUG_STATE) { - print("STATE (" + this.hand + "): " + stateToName(this.state) + " --> " + - stateToName(newState) + ", hand: " + this.hand); + var oldStateName = stateToName(this.state); + var newStateName = stateToName(newState); + print("STATE (" + this.hand + "): " + newStateName + " <-- " + oldStateName + ", reason = " + reason); } + + // exit the old state + if (CONTROLLER_STATE_MACHINE[this.state]) { + var exitMethodName = CONTROLLER_STATE_MACHINE[this.state].exitMethod; + var exitMethod = this[exitMethodName]; + if (exitMethod) { + exitMethod.call(this); + } + } else { + print("WARNING: could not find state " + this.state + " in state machine"); + } + this.state = newState; + + // enter the new state + if (CONTROLLER_STATE_MACHINE[newState]) { + var enterMethodName = CONTROLLER_STATE_MACHINE[newState].enterMethod; + var enterMethod = this[enterMethodName]; + if (enterMethod) { + enterMethod.call(this); + } + } else { + print("WARNING: could not find newState " + newState + " in state machine"); + } }; this.debugLine = function(closePoint, farPoint, color) { @@ -425,6 +475,8 @@ function MyController(hand) { color: color, alpha: SEARCH_SPHERE_ALPHA, solid: true, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. visible: true } this.searchSphere = Overlays.addOverlay("sphere", sphereProperties); @@ -447,6 +499,8 @@ function MyController(hand) { color: color, alpha: 0.1, solid: true, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. visible: true } this.grabSphere = Overlays.addOverlay("sphere", sphereProperties); @@ -477,19 +531,21 @@ function MyController(hand) { end: farPoint, color: color, ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. visible: true, alpha: 1 }; this.overlayLine = Overlays.addOverlay("line3d", lineProperties); } else { - var success = Overlays.editOverlay(this.overlayLine, { + Overlays.editOverlay(this.overlayLine, { lineWidth: 5, start: closePoint, end: farPoint, color: color, visible: true, ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. alpha: 1 }); } @@ -598,13 +654,6 @@ function MyController(hand) { }) }; - this.renewParticleBeamLifetime = function() { - var props = Entities.getEntityProperties(this.particleBeamObject, "age"); - Entities.editEntity(this.particleBeamObject, { - lifetime: TEMPORARY_PARTICLE_BEAM_LIFETIME + props.age // renew lifetime - }) - } - this.evalLightWorldTransform = function(modelPos, modelRot) { var MODEL_LIGHT_POSITION = { @@ -625,7 +674,7 @@ function MyController(hand) { }; }; - this.handleSpotlight = function(parentID, position) { + this.handleSpotlight = function(parentID) { var LIFETIME = 100; var modelProperties = Entities.getEntityProperties(parentID, ['position', 'rotation']); @@ -649,7 +698,7 @@ function MyController(hand) { exponent: 0.3, cutoff: 20, lifetime: LIFETIME, - position: lightTransform.p, + position: lightTransform.p }; if (this.spotlight === null) { @@ -657,12 +706,12 @@ function MyController(hand) { } else { Entities.editEntity(this.spotlight, { //without this, this light would maintain rotation with its parent - rotation: Quat.fromPitchYawRollDegrees(-90, 0, 0), + rotation: Quat.fromPitchYawRollDegrees(-90, 0, 0) }) } }; - this.handlePointLight = function(parentID, position) { + this.handlePointLight = function(parentID) { var LIFETIME = 100; var modelProperties = Entities.getEntityProperties(parentID, ['position', 'rotation']); @@ -686,13 +735,11 @@ function MyController(hand) { exponent: 0.3, cutoff: 20, lifetime: LIFETIME, - position: lightTransform.p, + position: lightTransform.p }; if (this.pointlight === null) { this.pointlight = Entities.addEntity(lightProperties); - } else { - } }; @@ -738,14 +785,6 @@ function MyController(hand) { } }; - this.propsArePhysical = function(props) { - if (!props.dynamic) { - return false; - } - var isPhysical = (props.shapeType && props.shapeType != 'none'); - return isPhysical; - } - this.turnOffVisualizations = function() { if (USE_ENTITY_LINES_FOR_SEARCHING === true || USE_ENTITY_LINES_FOR_MOVING === true) { this.lineOff(); @@ -759,8 +798,7 @@ function MyController(hand) { this.particleBeamOff(); } this.searchSphereOff(); - - Reticle.setVisible(true); + restore2DMode(); }; @@ -820,275 +858,400 @@ function MyController(hand) { }; this.off = function() { - if (this.triggerSmoothedSqueezed() || this.secondarySqueezed()) { + + if (this.triggerSmoothedReleased()) { + this.waitForTriggerRelease = false; + } + if (!this.waitForTriggerRelease && this.triggerSmoothedSqueezed()) { this.lastPickTime = 0; var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; this.startingHandRotation = Controller.getPoseValue(controllerHandInput).rotation; if (this.triggerSmoothedSqueezed()) { - this.setState(STATE_SEARCHING); - } else if (this.secondarySqueezed()) { - this.setState(STATE_HOLD_SEARCHING); + this.setState(STATE_SEARCHING, "trigger squeeze detected"); } } }; + this.searchEnter = function() { + this.equipHotspotOverlays = []; + + // find entities near the avatar that might be equipable. + var entities = Entities.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); + var i, length = entities.length; + for (i = 0; i < length; i++) { + var grabProps = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES); + // does this entity have an attach point? + var wearableData = getEntityCustomData("wearable", entities[i], undefined); + if (wearableData && wearableData.joints) { + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (wearableData.joints[handJointName]) { + // draw the hotspot + this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", { + position: grabProps.position, + size: 0.2, + color: { red: 90, green: 255, blue: 90 }, + alpha: 0.7, + solid: true, + visible: true, + ignoreRayIntersection: false, + drawInFront: false + })); + } + } + } + }; + + this.searchExit = function() { + + // delete all equip hotspots + var i, l = this.equipHotspotOverlays.length; + for (i = 0; i < l; i++) { + Overlays.deleteOverlay(this.equipHotspotOverlays[i]); + } + this.equipHotspotOverlays = []; + }; + + /// + // Performs ray pick test from the hand controller into the world + // @param {number} which hand to use, RIGHT_HAND or LEFT_HAND + // @returns {object} returns object with two keys entityID and distance + // + this.calcRayPickInfo = function(hand) { + + var pose = Controller.getPoseValue((hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); + var worldHandPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position); + var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation); + + var pickRay = { + origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, + direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), + Quat.getFront(Camera.orientation), + HAND_HEAD_MIX_RATIO), + length: PICK_MAX_DISTANCE + }; + + var result = { + entityID: null, + searchRay: pickRay, + distance: PICK_MAX_DISTANCE + }; + + var now = Date.now(); + if (now - this.lastPickTime < MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { + return result; + } + this.lastPickTime = now; + + var directionNormalized = Vec3.normalize(pickRay.direction); + var directionBacked = Vec3.multiply(directionNormalized, PICK_BACKOFF_DISTANCE); + var pickRayBacked = { + origin: Vec3.subtract(pickRay.origin, directionBacked), + direction: pickRay.direction + }; + + var intersection; + if (USE_BLACKLIST === true && blacklist.length !== 0) { + intersection = Entities.findRayIntersection(pickRayBacked, true, [], blacklist); + } else { + intersection = Entities.findRayIntersection(pickRayBacked, true); + } + + var overlayIntersection = Overlays.findRayIntersection(pickRayBacked); + if (!intersection.intersects || (overlayIntersection.intersects && (intersection.distance > overlayIntersection.distance))) { + intersection = overlayIntersection; + } + + if (intersection.intersects) { + return { entityID: intersection.entityID, + searchRay: pickRay, + distance: Vec3.distance(pickRay.origin, intersection.intersection) } + } else { + return result; + } + }; + + this.entityWantsTrigger = function (entityID) { + var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); + return grabbableProps && grabbableProps.wantsTrigger; + }; + + this.entityIsEquippable = function (entityID, handPosition) { + var props = this.entityPropertyCache.getProps(entityID); + var distance = Vec3.distance(props.position, handPosition); + var grabProps = this.entityPropertyCache.getGrabProps(entityID); + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; + if (refCount > 0) { + if (debug) { + print("equip is skipping '" + props.name + "': it is already grabbed"); + } + return false; + } + + if (distance > NEAR_PICK_MAX_DISTANCE) { + if (debug) { + print("equip is skipping '" + props.name + "': too far away."); + } + return false; + } + + var wearableProps = this.entityPropertyCache.getWearableProps(entityID); + if (!wearableProps || !wearableProps.joints) { + if (debug) { + print("equip is skipping '" + props.name + "': no wearable attach-point"); + } + return false; + } + + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (!wearableProps.joints[handJointName]) { + if (debug) { + print("equip is skipping '" + props.name + "': no wearable joint for " + handJointName); + } + return false; + } + + return true; + }; + + this.entityIsGrabbable = function (entityID) { + var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); + var grabProps = this.entityPropertyCache.getGrabProps(entityID); + var props = this.entityPropertyCache.getProps(entityID); + var physical = propsArePhysical(props); + var grabbable = false; + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + if (physical) { + // physical things default to grabbable + grabbable = true; + } else { + // non-physical things default to non-grabbable unless they are already grabbed + if ("refCount" in grabProps && grabProps.refCount > 0) { + grabbable = true; + } else { + grabbable = false; + } + } + + if (grabbableProps.hasOwnProperty("grabbable")) { + grabbable = grabbableProps.grabbable; + } + + if (!grabbable && !grabbableProps.wantsTrigger) { + if (debug) { + print("grab is skipping '" + props.name + "': not grabbable."); + } + return false; + } + if (FORBIDDEN_GRAB_TYPES.indexOf(props.type) >= 0) { + if (debug) { + print("grab is skipping '" + props.name + "': forbidden entity type."); + } + return false; + } + if (props.locked && !grabbableProps.wantsTrigger) { + if (debug) { + print("grab is skipping '" + props.name + "': locked and not triggerable."); + } + return false; + } + if (FORBIDDEN_GRAB_NAMES.indexOf(props.name) >= 0) { + if (debug) { + print("grab is skipping '" + props.name + "': forbidden name."); + } + return false; + } + + return true; + }; + + this.entityIsDistanceGrabbable = function(entityID, handPosition) { + if (!this.entityIsGrabbable(entityID)) { + return false; + } + + var props = this.entityPropertyCache.getProps(entityID); + var distance = Vec3.distance(props.position, handPosition); + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + // we can't distance-grab non-physical + var isPhysical = propsArePhysical(props); + if (!isPhysical) { + if (debug) { + print("distance grab is skipping '" + props.name + "': not physical"); + } + return false; + } + + if (distance > PICK_MAX_DISTANCE) { + // too far away, don't grab + if (debug) { + print("distance grab is skipping '" + props.name + "': too far away."); + } + return false; + } + + if (entityIsGrabbedByOther(entityID)) { + // don't distance grab something that is already grabbed. + if (debug) { + print("distance grab is skipping '" + props.name + "': already grabbed by another."); + } + return false; + } + + return true; + }; + + this.entityIsNearGrabbable = function(entityID, handPosition) { + + if (!this.entityIsGrabbable(entityID)) { + return false; + } + + var props = this.entityPropertyCache.getProps(entityID); + var distance = Vec3.distance(props.position, handPosition); + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + if (distance > NEAR_PICK_MAX_DISTANCE) { + // too far away, don't grab + if (debug) { + print(" grab is skipping '" + props.name + "': too far away."); + } + return false; + } + + return true; + }; + this.search = function() { + var _this = this; + var name; + this.grabbedEntity = null; this.isInitialGrab = false; this.shouldResetParentOnRelease = false; this.checkForStrayChildren(); - if (this.state == STATE_SEARCHING && this.triggerSmoothedReleased()) { - this.setState(STATE_RELEASE); - return; - } - if (this.state == STATE_HOLD_SEARCHING && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + if (this.triggerSmoothedReleased()) { + this.setState(STATE_OFF, "trigger released"); return; } - // the trigger is being pressed, so do a ray test to see what we are hitting var handPosition = this.getHandPosition(); if (SHOW_GRAB_SPHERE) { this.grabSphereOn(); } - var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; - var currentHandRotation = Controller.getPoseValue(controllerHandInput).rotation; - var currentControllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, - Controller.getPoseValue(controllerHandInput).translation), - MyAvatar.position); - var handDeltaRotation = Quat.multiply(currentHandRotation, Quat.inverse(this.startingHandRotation)); + this.entityPropertyCache.clear(); + this.entityPropertyCache.findEntities(handPosition, GRAB_RADIUS); + var candidateEntities = this.entityPropertyCache.getEntities(); - var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? - Controller.Standard.RightHand : Controller.Standard.LeftHand); - var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation); + var equippableEntities = candidateEntities.filter(function (entity) { + return _this.entityIsEquippable(entity, handPosition); + }); - var distantPickRay = { - origin: PICK_WITH_HAND_RAY ? currentControllerPosition : Camera.position, - direction: PICK_WITH_HAND_RAY ? Quat.getUp(controllerRotation) : Vec3.mix(Quat.getUp(controllerRotation), - Quat.getFront(Camera.orientation), - HAND_HEAD_MIX_RATIO), - length: PICK_MAX_DISTANCE - }; - - // Pick at some maximum rate, not always - var pickRays = []; - var now = Date.now(); - if (now - this.lastPickTime > MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { - pickRays = [distantPickRay]; - this.lastPickTime = now; - } - - rayPickedCandidateEntities = []; // the list of candidates to consider grabbing - - this.intersectionDistance = 0.0; - for (var index = 0; index < pickRays.length; ++index) { - var pickRay = pickRays[index]; - var directionNormalized = Vec3.normalize(pickRay.direction); - var directionBacked = Vec3.multiply(directionNormalized, PICK_BACKOFF_DISTANCE); - var pickRayBacked = { - origin: Vec3.subtract(pickRay.origin, directionBacked), - direction: pickRay.direction - }; - - var intersection; - - if (USE_BLACKLIST === true && blacklist.length !== 0) { - intersection = Entities.findRayIntersection(pickRayBacked, true, [], blacklist); - } else { - intersection = Entities.findRayIntersection(pickRayBacked, true); - } - - if (intersection.intersects) { - rayPickedCandidateEntities.push(intersection.entityID); - this.intersectionDistance = Vec3.distance(pickRay.origin, intersection.intersection); - } - } - - nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); - candidateEntities = rayPickedCandidateEntities.concat(nearPickedCandidateEntities); - - var forbiddenNames = ["Grab Debug Entity", "grab pointer"]; - var forbiddenTypes = ['Unknown', 'Light', 'PolyLine', 'Zone']; - - var minDistance = PICK_MAX_DISTANCE; - var i, props, distance, grabbableData; - this.grabbedEntity = null; - for (i = 0; i < candidateEntities.length; i++) { - var grabbableDataForCandidate = - getEntityCustomData(GRABBABLE_DATA_KEY, candidateEntities[i], DEFAULT_GRABBABLE_DATA); - var grabDataForCandidate = getEntityCustomData(GRAB_USER_DATA_KEY, candidateEntities[i], {}); - var propsForCandidate = Entities.getEntityProperties(candidateEntities[i], GRABBABLE_PROPERTIES); - var near = (nearPickedCandidateEntities.indexOf(candidateEntities[i]) >= 0); - - var isPhysical = this.propsArePhysical(propsForCandidate); - var grabbable; - if (isPhysical) { - // physical things default to grabbable - grabbable = true; - } else { - // non-physical things default to non-grabbable unless they are already grabbed - if ("refCount" in grabDataForCandidate && grabDataForCandidate.refCount > 0) { - grabbable = true; - } else { - grabbable = false; - } - } - - if ("grabbable" in grabbableDataForCandidate) { - // if userData indicates that this is grabbable or not, override the default. - grabbable = grabbableDataForCandidate.grabbable; - } - - if (!grabbable && !grabbableDataForCandidate.wantsTrigger) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': not grabbable."); - } - continue; - } - if (forbiddenTypes.indexOf(propsForCandidate.type) >= 0) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': forbidden entity type."); - } - continue; - } - if (propsForCandidate.locked && !grabbableDataForCandidate.wantsTrigger) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': locked and not triggerable."); - } - continue; - } - if (forbiddenNames.indexOf(propsForCandidate.name) >= 0) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': forbidden name."); - } - continue; - } - - distance = Vec3.distance(propsForCandidate.position, handPosition); - if (distance > PICK_MAX_DISTANCE) { - // too far away, don't grab - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': too far away."); - } - continue; - } - if (propsForCandidate.parentID != NULL_UUID && this.state == STATE_HOLD_SEARCHING) { - // don't allow a double-equip - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': it's a child"); - } - continue; - } - - if (this.state == STATE_SEARCHING && - !isPhysical && distance > NEAR_PICK_MAX_DISTANCE && !near && !grabbableDataForCandidate.wantsTrigger) { - // we can't distance-grab non-physical - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': not physical and too far for near-grab"); - } - continue; - } - - if (distance < minDistance) { - this.grabbedEntity = candidateEntities[i]; - minDistance = distance; - props = propsForCandidate; - grabbableData = grabbableDataForCandidate; - } - } - if ((this.grabbedEntity !== null) && (this.triggerSmoothedGrab() || this.secondarySqueezed())) { - // We are squeezing enough to grab, and we've found an entity that we'll try to do something with. - var near = (nearPickedCandidateEntities.indexOf(this.grabbedEntity) >= 0) || minDistance <= NEAR_PICK_MAX_DISTANCE; - var isPhysical = this.propsArePhysical(props); - - // near or far trigger - if (grabbableData.wantsTrigger) { - this.setState(near ? STATE_NEAR_TRIGGER : STATE_FAR_TRIGGER); + var entity; + if (equippableEntities.length > 0) { + // sort by distance + equippableEntities.sort(function (a, b) { + var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition); + var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition); + return aDistance - bDistance; + }); + entity = equippableEntities[0]; + if (this.triggerSmoothedGrab()) { + this.grabbedEntity = entity; + this.setState(STATE_HOLD, "eqipping '" + this.entityPropertyCache.getProps(entity).name + "'"); return; + } else { + // TODO: highlight the equippable object? } - // near grab with action or equip - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); - var refCount = ("refCount" in grabData) ? grabData.refCount : 0; - if (near && (refCount < 1 || entityHasActions(this.grabbedEntity))) { - if (this.state == STATE_SEARCHING) { - this.setState(STATE_NEAR_GRABBING); - } else { // (this.state == STATE_HOLD_SEARCHING) - // if there was already an action, we'll need to set the parent back to null once we release - this.shouldResetParentOnRelease = true; - this.previousParentID = props.parentID; - this.previousParentJointIndex = props.parentJointIndex; - this.setState(STATE_HOLD); - } - return; - } - // far grab - if (isPhysical && !near) { - if (entityIsGrabbedByOther(this.grabbedEntity)) { - // don't distance grab something that is already grabbed. - if (WANT_DEBUG_SEARCH_NAME && props.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': already grabbed by another."); - } + } + + var rayPickInfo = this.calcRayPickInfo(this.hand); + this.intersectionDistance = rayPickInfo.distance; + if (rayPickInfo.entityID) { + candidateEntities.push(rayPickInfo.entityID); + this.entityPropertyCache.addEntity(rayPickInfo.entityID); + } + + var grabbableEntities = candidateEntities.filter(function (entity) { + return _this.entityIsNearGrabbable(entity, handPosition); + }); + + if (grabbableEntities.length > 0) { + // sort by distance + grabbableEntities.sort(function (a, b) { + var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition); + var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition); + return aDistance - bDistance; + }); + entity = grabbableEntities[0]; + name = this.entityPropertyCache.getProps(entity).name; + this.grabbedEntity = entity; + if (this.entityWantsTrigger(entity)) { + if (this.triggerSmoothedGrab()) { + this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'"); return; + } else { + // TODO: highlight the near-triggerable object? } - this.temporaryPositionOffset = null; - if (!this.hasPresetOffsets()) { - // We want to give a temporary position offset to this object so it is pulled close to hand - var intersectionPointToCenterDistance = Vec3.length(Vec3.subtract(intersection.intersection, - intersection.properties.position)); - var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - var handJointPosition = MyAvatar.getJointPosition(handJointIndex); - this.temporaryPositionOffset = - Vec3.normalize(Vec3.subtract(intersection.properties.position, handJointPosition)); - this.temporaryPositionOffset = Vec3.multiply(this.temporaryPositionOffset, - intersectionPointToCenterDistance * - FAR_TO_NEAR_GRAB_PADDING_FACTOR); - } - this.setState(STATE_DISTANCE_HOLDING); - - this.searchSphereOff(); - return; - } - - // else this thing isn't physical. grab it by reparenting it (but not if we've already - // grabbed it). - if (refCount < 1) { - if (this.state == STATE_SEARCHING) { - this.setState(STATE_NEAR_GRABBING); - } else { // this.state == STATE_HOLD_SEARCHING) - this.setState(STATE_HOLD); - } - return; } else { - // it's not physical and it's already held via parenting. go ahead and grab it, but - // save off the current parent and joint. this wont always be right if there are more than - // two grabs and the order of release isn't opposite of the order of grabs. - this.shouldResetParentOnRelease = true; - this.previousParentID = props.parentID; - this.previousParentJointIndex = props.parentJointIndex; - if (this.state == STATE_SEARCHING) { - this.setState(STATE_NEAR_GRABBING); - } else { // (this.state == STATE_HOLD_SEARCHING) - this.setState(STATE_HOLD); + if (this.triggerSmoothedGrab()) { + + var props = this.entityPropertyCache.getProps(entity); + var grabProps = this.entityPropertyCache.getGrabProps(entity); + var refCount = grabProps.refCount ? grabProps.refCount : 0; + if (refCount >= 1) { + // if another person is holding the object, remember to restore the + // parent info, when we are finished grabbing it. + this.shouldResetParentOnRelease = true; + this.previousParentID = props.parentID; + this.previousParentJointIndex = props.parentJointIndex; + } + + this.setState(STATE_NEAR_GRABBING, "near grab '" + name + "'"); + return; + } else { + // TODO: highlight the grabbable object? } - return; } - if (WANT_DEBUG_SEARCH_NAME && props.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': fell through."); + return; + } + + if (rayPickInfo.entityID) { + entity = rayPickInfo.entityID; + name = this.entityPropertyCache.getProps(entity).name; + if (this.entityWantsTrigger(entity)) { + if (this.triggerSmoothedGrab()) { + this.grabbedEntity = entity; + this.setState(STATE_FAR_TRIGGER, "far trigger '" + name + "'"); + return; + } else { + // TODO: highlight the far-triggerable object? + } + } else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) { + if (this.triggerSmoothedGrab()) { + this.grabbedEntity = entity; + this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); + return; + } else { + // TODO: highlight the far-grabbable object? + } } } //search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { - this.lineOn(distantPickRay.origin, Vec3.multiply(distantPickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + this.lineOn(rayPickInfo.searchRay.origin, Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); } - this.searchIndicatorOn(distantPickRay); + this.searchIndicatorOn(rayPickInfo.searchRay); Reticle.setVisible(false); - }; this.distanceGrabTimescale = function(mass, distance) { @@ -1105,7 +1268,7 @@ function MyController(hand) { return (dimensions.x * dimensions.y * dimensions.z) * density; } - this.distanceHolding = function() { + this.distanceHoldingEnter = function() { // controller pose is in avatar frame var avatarControllerPose = @@ -1154,7 +1317,6 @@ function MyController(hand) { this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); if (this.actionID !== null) { - this.setState(STATE_CONTINUE_DISTANCE_HOLDING); this.activateEntity(this.grabbedEntity, grabbedProperties, false); this.callEntityMethodOnGrabbed("startDistanceGrab"); } @@ -1165,10 +1327,10 @@ function MyController(hand) { this.previousControllerRotation = controllerRotation; }; - this.continueDistanceHolding = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + this.distanceHolding = function() { + if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("releaseGrab"); + this.setState(STATE_OFF, "trigger released"); return; } @@ -1186,7 +1348,7 @@ function MyController(hand) { var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); var now = Date.now(); - var deltaTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds + var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds this.currentObjectTime = now; // the action was set up when this.distanceHolding was called. update the targets. @@ -1200,11 +1362,12 @@ function MyController(hand) { var handMoved = Vec3.multiply(Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar), radius); - // double delta controller rotation - var handChange = Quat.multiply(Quat.slerp(this.previousControllerRotation, - controllerRotation, - DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), - Quat.inverse(this.previousControllerRotation)); + /// double delta controller rotation + // var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did + // var handChange = Quat.multiply(Quat.slerp(this.previousControllerRotation, + // controllerRotation, + // DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), + // Quat.inverse(this.previousControllerRotation)); // update the currentObject position and rotation. this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved); @@ -1220,17 +1383,17 @@ function MyController(hand) { // Update radialVelocity var lastVelocity = Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar); - lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaTime); + lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaObjectTime); var newRadialVelocity = Vec3.dot(lastVelocity, Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); var VELOCITY_AVERAGING_TIME = 0.016; - this.grabRadialVelocity = (deltaTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity + - (1.0 - (deltaTime / VELOCITY_AVERAGING_TIME)) * this.grabRadialVelocity; + this.grabRadialVelocity = (deltaObjectTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity + + (1.0 - (deltaObjectTime / VELOCITY_AVERAGING_TIME)) * this.grabRadialVelocity; var RADIAL_GRAB_AMPLIFIER = 10.0; if (Math.abs(this.grabRadialVelocity) > 0.0) { - this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaTime * this.grabRadius * RADIAL_GRAB_AMPLIFIER); + this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * this.grabRadius * RADIAL_GRAB_AMPLIFIER); } var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(controllerRotation)); @@ -1258,26 +1421,26 @@ function MyController(hand) { } } - var defaultConstraintData = { - axisStart: false, - axisEnd: false, - } - - var constraintData = getEntityCustomData('lightModifierKey', this.grabbedEntity, defaultConstraintData); - var clampedVector; - var targetPosition; - if (constraintData.axisStart !== false) { - clampedVector = this.projectVectorAlongAxis(this.currentObjectPosition, - constraintData.axisStart, - constraintData.axisEnd); - targetPosition = clampedVector; - } else { - targetPosition = { - x: this.currentObjectPosition.x, - y: this.currentObjectPosition.y, - z: this.currentObjectPosition.z - } - } + // var defaultConstraintData = { + // axisStart: false, + // axisEnd: false + // } + // + // var constraintData = getEntityCustomData('lightModifierKey', this.grabbedEntity, defaultConstraintData); + // var clampedVector; + // var targetPosition; + // if (constraintData.axisStart !== false) { + // clampedVector = this.projectVectorAlongAxis(this.currentObjectPosition, + // constraintData.axisStart, + // constraintData.axisEnd); + // targetPosition = clampedVector; + // } else { + // targetPosition = { + // x: this.currentObjectPosition.x, + // y: this.currentObjectPosition.y, + // z: this.currentObjectPosition.z + // } + // } var handPosition = this.getHandPosition(); @@ -1382,23 +1545,56 @@ function MyController(hand) { } } - this.nearGrabbing = function() { - var now = Date.now(); + this.dropGestureReset = function() { + this.fastHandMoveDetected = false; + this.fastHandMoveTimer = 0; + }; - if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) { - this.setState(STATE_RELEASE); - this.callEntityMethodOnGrabbed("releaseGrab"); - return; + this.dropGestureProcess = function(deltaTime) { + var pose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); + var worldHandVelocity = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity); + var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation); + + if (this.fastHandMoveDetected) { + this.fastHandMoveTimer -= deltaTime; } - if (this.state == STATE_HOLD && this.secondaryReleased()) { - this.setState(STATE_RELEASE); - this.callEntityMethodOnGrabbed("releaseGrab"); - return; + if (this.fastHandMoveTimer < 0) { + this.fastHandMoveDetected = false; } + var FAST_HAND_SPEED_REST_TIME = 1; // sec + var FAST_HAND_SPEED_THRESHOLD = 0.4; // m/sec + if (Vec3.length(worldHandVelocity) > FAST_HAND_SPEED_THRESHOLD) { + this.fastHandMoveDetected = true; + this.fastHandMoveTimer = FAST_HAND_SPEED_REST_TIME; + } + + var localHandUpAxis = this.hand === RIGHT_HAND ? {x: 1, y: 0, z: 0} : {x: -1, y: 0, z: 0}; + var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis); + var DOWN = {x: 0, y: -1, z: 0}; + var ROTATION_THRESHOLD = Math.cos(Math.PI / 8); + + var handIsUpsideDown = false; + if (Vec3.dot(worldHandUpAxis, DOWN) > ROTATION_THRESHOLD) { + handIsUpsideDown = true; + } + + var WANT_DEBUG = false; + if (WANT_DEBUG) { + print("zAxis = " + worldHandUpAxis.x + ", " + worldHandUpAxis.y + ", " + worldHandUpAxis.z); + print("dot = " + Vec3.dot(worldHandUpAxis, DOWN) + ", ROTATION_THRESHOLD = " + ROTATION_THRESHOLD); + print("handMove = " + this.fastHandMoveDetected + ", handIsUpsideDown = " + handIsUpsideDown); + } + + return (DROP_WITHOUT_SHAKE || this.fastHandMoveDetected) && handIsUpsideDown; + }; + + this.nearGrabbingEnter = function() { this.lineOff(); this.overlayLineOff(); + this.dropGestureReset(); + if (this.entityActivated) { var saveGrabbedID = this.grabbedEntity; this.release(); @@ -1413,7 +1609,7 @@ function MyController(hand) { var handPosition = this.getHandPosition(); var hasPresetPosition = false; - if ((this.state == STATE_EQUIP || this.state == STATE_HOLD) && this.hasPresetOffsets()) { + if (this.state == STATE_HOLD && this.hasPresetOffsets()) { var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); // if an object is "equipped" and has a predefined offset, use it. this.ignoreIK = grabbableData.ignoreIK ? grabbableData.ignoreIK : false; @@ -1429,13 +1625,13 @@ function MyController(hand) { var currentObjectPosition = grabbedProperties.position; var offset = Vec3.subtract(currentObjectPosition, handPosition); this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset); - if (this.temporaryPositionOffset && (this.state == STATE_EQUIP)) { + if (this.temporaryPositionOffset) { this.offsetPosition = this.temporaryPositionOffset; // hasPresetPosition = true; } } - var isPhysical = this.propsArePhysical(grabbedProperties) || entityHasActions(this.grabbedEntity); + var isPhysical = propsArePhysical(grabbedProperties) || entityHasActions(this.grabbedEntity); if (isPhysical && this.state == STATE_NEAR_GRABBING) { // grab entity via action if (!this.setupHoldAction()) { @@ -1449,7 +1645,7 @@ function MyController(hand) { // grab entity via parenting this.actionID = null; var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - reparentProps = { + var reparentProps = { parentID: MyAvatar.sessionUUID, parentJointIndex: handJointIndex } @@ -1473,21 +1669,10 @@ function MyController(hand) { if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("startNearGrab"); - } else { // this.state == STATE_EQUIP || this.state == STATE_HOLD + } else { // this.state == STATE_HOLD this.callEntityMethodOnGrabbed("startEquip"); } - if (this.state == STATE_NEAR_GRABBING) { - // near grabbing - this.setState(STATE_CONTINUE_NEAR_GRABBING); - } else if (this.state == STATE_HOLD) { - // holding - this.setState(STATE_CONTINUE_HOLD); - } else { // (this.state == STATE_EQUIP) - // equipping - this.setState(STATE_CONTINUE_EQUIP); - } - this.currentHandControllerTipPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition; this.currentObjectTime = Date.now(); @@ -1498,31 +1683,28 @@ function MyController(hand) { this.currentAngularVelocity = ZERO_VEC; }; - this.continueNearGrabbing = function() { - if (this.state == STATE_CONTINUE_NEAR_GRABBING && this.triggerSmoothedReleased()) { - this.setState(STATE_RELEASE); + this.nearGrabbing = function(deltaTime) { + + var dropDetected = this.dropGestureProcess(deltaTime); + + if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("releaseGrab"); + this.setState(STATE_OFF, "trigger released"); return; } - if (this.state == STATE_CONTINUE_HOLD && this.secondaryReleased()) { - this.setState(STATE_RELEASE); - this.callEntityMethodOnGrabbed("releaseEquip"); - return; - } - if (this.state == STATE_CONTINUE_EQUIP && this.thumbPressed()) { - this.setState(STATE_WAITING_FOR_RELEASE_THUMB_RELEASE); - this.callEntityMethodOnGrabbed("releaseEquip"); - return; - } - if (this.state == STATE_CONTINUE_NEAR_GRABBING && this.thumbPressed()) { - this.setState(STATE_WAITING_FOR_EQUIP_THUMB_RELEASE); - this.callEntityMethodOnGrabbed("releaseGrab"); - this.callEntityMethodOnGrabbed("startEquip"); - return; - } - if (this.state == STATE_CONTINUE_HOLD && this.thumbPressed()) { - this.setState(STATE_WAITING_FOR_EQUIP_THUMB_RELEASE); - return; + + if (this.state == STATE_HOLD) { + if (dropDetected && this.triggerSmoothedGrab()) { + this.callEntityMethodOnGrabbed("releaseEquip"); + this.setState(STATE_OFF, "drop gesture detected"); + return; + } + + if (this.thumbPressed()) { + this.callEntityMethodOnGrabbed("releaseEquip"); + this.setState(STATE_OFF, "drop via thumb press"); + return; + } } this.heartBeat(this.grabbedEntity); @@ -1530,8 +1712,8 @@ function MyController(hand) { var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position", "rotation"]); if (!props.position) { // server may have reset, taking our equipped entity with it. move back to "off" stte - this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("releaseGrab"); + this.setState(STATE_OFF, "entity has no position property"); return; } @@ -1545,17 +1727,18 @@ function MyController(hand) { var handPosition = this.getHandPosition(); // the center of the equipped object being far from the hand isn't enough to autoequip -- we also // need to fail the findEntities test. - nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); + var nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + props.parentID + " " + vec3toStr(props.position)); - this.setState(STATE_RELEASE); - if (this.state == STATE_CONTINUE_NEAR_GRABBING) { + + if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("releaseGrab"); - } else { // (this.state == STATE_CONTINUE_EQUIP || this.state == STATE_CONTINUE_HOLD) + } else { // this.state == STATE_HOLD this.callEntityMethodOnGrabbed("releaseEquip"); } + this.setState(STATE_OFF, "held object too far away"); return; } } @@ -1569,20 +1752,18 @@ function MyController(hand) { // from the palm. var handControllerPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition; - var now = Date.now(); - var deltaPosition = Vec3.subtract(handControllerPosition, this.currentHandControllerTipPosition); // meters - var deltaTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds + var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds - if (deltaTime > 0.0) { + if (deltaObjectTime > 0.0) { var worldDeltaPosition = Vec3.subtract(props.position, this.currentObjectPosition); var previousEulers = Quat.safeEulerAngles(this.currentObjectRotation); var newEulers = Quat.safeEulerAngles(props.rotation); var worldDeltaRotation = Vec3.subtract(newEulers, previousEulers); - this.currentVelocity = Vec3.multiply(worldDeltaPosition, 1.0 / deltaTime); - this.currentAngularVelocity = Vec3.multiply(worldDeltaRotation, Math.PI / (deltaTime * 180.0)); + this.currentVelocity = Vec3.multiply(worldDeltaPosition, 1.0 / deltaObjectTime); + this.currentAngularVelocity = Vec3.multiply(worldDeltaRotation, Math.PI / (deltaObjectTime * 180.0)); this.currentObjectPosition = props.position; this.currentObjectRotation = props.rotation; @@ -1591,11 +1772,10 @@ function MyController(hand) { this.currentHandControllerTipPosition = handControllerPosition; this.currentObjectTime = now; - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); - if (this.state === STATE_CONTINUE_EQUIP || this.state === STATE_CONTINUE_HOLD) { + if (this.state === STATE_HOLD) { this.callEntityMethodOnGrabbed("continueEquip"); } - if (this.state == STATE_CONTINUE_NEAR_GRABBING) { + if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("continueNearGrab"); } @@ -1621,50 +1801,27 @@ function MyController(hand) { } }; - this.waitingForEquipThumbRelease = function() { - if (this.thumbReleased() && this.triggerSmoothedReleased()) { - this.setState(STATE_EQUIP); - } + this.nearTriggerEnter = function() { + this.callEntityMethodOnGrabbed("startNearTrigger"); }; - this.waitingForReleaseThumbRelease = function() { - if (this.thumbReleased() && this.triggerSmoothedReleased()) { - this.setState(STATE_RELEASE); - } + + this.farTriggerEnter = function() { + this.callEntityMethodOnGrabbed("startFarTrigger"); }; this.nearTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); - this.callEntityMethodOnGrabbed("stopNearTrigger"); - return; - } - this.callEntityMethodOnGrabbed("startNearTrigger"); - this.setState(STATE_CONTINUE_NEAR_TRIGGER); - }; - - this.farTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); - this.callEntityMethodOnGrabbed("stopFarTrigger"); - return; - } - this.callEntityMethodOnGrabbed("startFarTrigger"); - this.setState(STATE_CONTINUE_FAR_TRIGGER); - }; - - this.continueNearTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopNearTrigger"); + this.setState(STATE_OFF, "trigger released"); return; } this.callEntityMethodOnGrabbed("continueNearTrigger"); }; - this.continueFarTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + this.farTrigger = function() { + if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopFarTrigger"); + this.setState(STATE_OFF, "trigger released"); return; } @@ -1680,8 +1837,8 @@ function MyController(hand) { if (intersection.accurate) { this.lastPickTime = now; if (intersection.entityID != this.grabbedEntity) { - this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("stopFarTrigger"); + this.setState(STATE_OFF, "laser moved off of entity"); return; } if (intersection.intersects) { @@ -1694,6 +1851,10 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("continueFarTrigger"); }; + this.offEnter = function() { + this.release(); + }; + this.release = function() { this.turnLightsOff(); this.turnOffVisualizations(); @@ -1717,7 +1878,6 @@ function MyController(hand) { this.deactivateEntity(this.grabbedEntity, noVelocity); this.actionID = null; - this.setState(STATE_OFF); Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'release', @@ -1726,6 +1886,10 @@ function MyController(hand) { })); this.grabbedEntity = null; + + if (this.triggerSmoothedGrab()) { + this.waitForTriggerRelease = true; + } }; this.cleanup = function() { @@ -1827,6 +1991,8 @@ function MyController(hand) { } this.deactivateEntity = function(entityID, noVelocity) { + var deactiveProps; + if (!this.entityActivated) { return; } @@ -1836,7 +2002,7 @@ function MyController(hand) { if (data && data["refCount"]) { data["refCount"] = data["refCount"] - 1; if (data["refCount"] < 1) { - var deactiveProps = { + deactiveProps = { gravity: data["gravity"], collidesWith: data["collidesWith"], collisionless: data["collisionless"], @@ -1849,10 +2015,9 @@ function MyController(hand) { // it looks like the dropped thing should fall, give it a little velocity. var props = Entities.getEntityProperties(entityID, ["parentID", "velocity", "dynamic", "shapeType"]) var parentID = props.parentID; - var forceVelocity = false; var doSetVelocity = false; - if (parentID != NULL_UUID && deactiveProps.parentID == NULL_UUID && this.propsArePhysical(props)) { + if (parentID != NULL_UUID && deactiveProps.parentID == NULL_UUID && propsArePhysical(props)) { // TODO: EntityScriptingInterface::convertLocationToScriptSemantics should be setting up // props.velocity to be a world-frame velocity and localVelocity to be vs parent. Until that // is done, we use a measured velocity here so that things held via a bumper-grab / parenting-grab @@ -1884,7 +2049,7 @@ function MyController(hand) { // the parent causes it to go off in the wrong direction. This is a bug that should // be fixed. Entities.editEntity(entityID, { - velocity: this.currentVelocity, + velocity: this.currentVelocity // angularVelocity: this.currentAngularVelocity }); } @@ -1892,7 +2057,7 @@ function MyController(hand) { data = null; } else if (this.shouldResetParentOnRelease) { // we parent-grabbed this from another parent grab. try to put it back where we found it. - var deactiveProps = { + deactiveProps = { parentID: this.previousParentID, parentJointIndex: this.previousParentJointIndex, velocity: {x: 0.0, y: 0.0, z: 0.0}, @@ -1909,29 +2074,7 @@ function MyController(hand) { } setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); }; - - this.checkNewlyLoaded = function(loadedEntityID) { - if (this.state == STATE_OFF || - this.state == STATE_SEARCHING || - this.state == STATE_HOLD_SEARCHING) { - var loadedProps = Entities.getEntityProperties(loadedEntityID); - if (loadedProps.parentID != MyAvatar.sessionUUID) { - return; - } - var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - if (loadedProps.parentJointIndex != handJointIndex) { - return; - } - print("--- handControllerGrab found loaded entity ---"); - // an entity has been loaded and it's where this script would have equipped something, so switch states. - this.grabbedEntity = loadedEntityID; - this.activateEntity(this.grabbedEntity, loadedProps, true); - this.isInitialGrab = true; - this.callEntityMethodOnGrabbed("startEquip"); - this.setState(STATE_CONTINUE_EQUIP); - } - } -}; +} var rightController = new MyController(RIGHT_HAND); var leftController = new MyController(LEFT_HAND); @@ -1955,12 +2098,12 @@ Controller.enableMapping(MAPPING_NAME); //the section below allows the grab script to listen for messages that disable either one or both hands. useful for two handed items var handToDisable = 'none'; -function update() { +function update(deltaTime) { if (handToDisable !== LEFT_HAND && handToDisable !== 'both') { - leftController.update(); + leftController.update(deltaTime); } if (handToDisable !== RIGHT_HAND && handToDisable !== 'both') { - rightController.update(); + rightController.update(deltaTime); } } @@ -1969,7 +2112,8 @@ Messages.subscribe('Hifi-Hand-Grab'); Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.subscribe('Hifi-Object-Manipulation'); -handleHandMessages = function(channel, message, sender) { +var handleHandMessages = function(channel, message, sender) { + var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Disabler') { if (message === 'left') { @@ -1983,17 +2127,19 @@ handleHandMessages = function(channel, message, sender) { } } else if (channel === 'Hifi-Hand-Grab') { try { - var data = JSON.parse(message); + data = JSON.parse(message); var selectedController = (data.hand === 'left') ? leftController : rightController; selectedController.release(); - selectedController.setState(STATE_EQUIP); + selectedController.setState(STATE_HOLD, "Hifi-Hand-Grab msg received"); selectedController.grabbedEntity = data.entityID; - } catch (e) {} + } catch (e) { + print("WARNING: error parsing Hifi-Hand-Grab message"); + } } else if (channel === 'Hifi-Hand-RayPick-Blacklist') { try { - var data = JSON.parse(message); + data = JSON.parse(message); var action = data.action; var id = data.id; var index = blacklist.indexOf(id); @@ -2007,23 +2153,8 @@ handleHandMessages = function(channel, message, sender) { } } - } catch (e) {} - } else if (channel === 'Hifi-Object-Manipulation') { - if (sender !== MyAvatar.sessionUUID) { - return; - } - - var parsedMessage = null; - try { - parsedMessage = JSON.parse(message); } catch (e) { - print('error parsing Hifi-Object-Manipulation message'); - return; - } - - if (parsedMessage.action === 'loaded') { - rightController.checkNewlyLoaded(parsedMessage['grabbedEntity']); - leftController.checkNewlyLoaded(parsedMessage['grabbedEntity']); + print("WARNING: error parsing Hifi-Hand-RayPick-Blacklist message"); } } } @@ -2036,6 +2167,24 @@ function cleanup() { leftController.cleanup(); Controller.disableMapping(MAPPING_NAME); Reticle.setVisible(true); + Menu.removeMenuItem("Developer > Hands", "Drop Without Shake"); } + Script.scriptEnding.connect(cleanup); -Script.update.connect(update); \ No newline at end of file +Script.update.connect(update); + +Menu.addMenuItem({ + menuName: "Developer > Hands", + menuItemName: "Drop Without Shake", + isCheckable: true, + isChecked: DROP_WITHOUT_SHAKE +}); + +function handleMenuItemEvent(menuItem) { + if (menuItem === "Drop Without Shake") { + DROP_WITHOUT_SHAKE = Menu.isOptionChecked("Drop Without Shake"); + } +} + +Menu.menuItemEvent.connect(handleMenuItemEvent); + diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index ca3b5e8cf2..374be0d1a1 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -1,6 +1,6 @@ "use strict"; /*jslint vars: true, plusplus: true*/ -/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Menu, ScriptDiscoveryService, Window, Vec3, Quat, print */ +/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Menu, ScriptDiscoveryService, Window, Vec3, Quat, print*/ // // handControllerPointer.js @@ -14,19 +14,16 @@ // // Control the "mouse" using hand controller. (HMD and desktop.) -// For now: -// Hydra thumb button 3 is left-mouse, button 4 is right-mouse. -// A click in the center of the vive thumb pad is left mouse. Vive menu button is context menu (right mouse). // First-person only. // Starts right handed, but switches to whichever is free: Whichever hand was NOT most recently squeezed. // (For now, the thumb buttons on both controllers are always on.) -// When over a HUD element, the reticle is shown where the active hand controller beam intersects the HUD. -// Otherwise, the active hand controller shows a red ball where a click will act. - +// When partially squeezing over a HUD element, a laser or the reticle is shown where the active hand +// controller beam intersects the HUD. // UTILITIES ------------- // +function ignore() { } // Utility to make it easier to setup and disconnect cleanly. function setupHandler(event, handler) { @@ -47,21 +44,61 @@ function TimeLock(expiration) { } var handControllerLockOut = new TimeLock(2000); -// Calls onFunction() or offFunction() when swtich(on), but only if it is to a new value. -function LatchedToggle(onFunction, offFunction, state) { - this.getState = function () { - return state; +function Trigger(label) { + // This part is copied and adapted from handControllerGrab.js. Maybe we should refactor this. + var that = this; + that.label = label; + that.TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing + that.TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab + that.TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab + that.TRIGGER_OFF_VALUE = 0.15; + that.rawTriggerValue = 0; + that.triggerValue = 0; // rolling average of trigger value + that.triggerPress = function (value) { + that.rawTriggerValue = value; }; - this.setState = function (on) { - if (state === on) { - return; - } - state = on; - if (on) { - onFunction(); - } else { - offFunction(); + that.updateSmoothedTrigger = function () { // e.g., call once/update for effect + var triggerValue = that.rawTriggerValue; + // smooth out trigger value + that.triggerValue = (that.triggerValue * that.TRIGGER_SMOOTH_RATIO) + + (triggerValue * (1.0 - that.TRIGGER_SMOOTH_RATIO)); + }; + // Current smoothed state, without hysteresis. Answering booleans. + that.triggerSmoothedGrab = function () { + return that.triggerValue > that.TRIGGER_GRAB_VALUE; + }; + that.triggerSmoothedSqueezed = function () { + return that.triggerValue > that.TRIGGER_ON_VALUE; + }; + that.triggerSmoothedReleased = function () { + return that.triggerValue < that.TRIGGER_OFF_VALUE; + }; + + // This part is not from handControllerGrab.js + that.state = null; // tri-state: falsey, 'partial', 'full' + that.update = function () { // update state, called from an update function + var state = that.state; + that.updateSmoothedTrigger(); + + // The first two are independent of previous state: + if (that.triggerSmoothedGrab()) { + state = 'full'; + } else if (that.triggerSmoothedReleased()) { + state = null; + } else if (that.triggerSmoothedSqueezed()) { + // Another way to do this would be to have hysteresis in this branch, but that seems to make things harder to use. + // In particular, the vive has a nice detent as you release off of full, and we want that to be a transition from + // full to partial. + state = 'partial'; } + that.state = state; + }; + // Answer a controller source function (answering either 0.0 or 1.0). + that.partial = function () { + return that.state ? 1.0 : 0.0; // either 'partial' or 'full' + }; + that.full = function () { + return (that.state === 'full') ? 1.0 : 0.0; }; } @@ -107,9 +144,8 @@ function isPointingAtOverlay(optionalHudPosition2d) { } // Generalized HUD utilities, with or without HMD: -// These two "vars" are for documentation. Do not change their values! -var SPHERICAL_HUD_DISTANCE = 1; // meters. -var PLANAR_PERPENDICULAR_HUD_DISTANCE = SPHERICAL_HUD_DISTANCE; +// This "var" is for documentation. Do not change the value! +var PLANAR_PERPENDICULAR_HUD_DISTANCE = 1; function calculateRayUICollisionPoint(position, direction) { // Answer the 3D intersection of the HUD by the given ray, or falsey if no intersection. if (HMD.active) { @@ -185,10 +221,9 @@ function updateSeeking() { } averageMouseVelocity = lastIntegration = 0; var lookAt2D = HMD.getHUDLookAtPosition2D(); - if (!lookAt2D) { - // FIXME - determine if this message is useful but make it so it doesn't spam the - // log in the case that it is happening - //print('Cannot seek without lookAt position'); + if (!lookAt2D) { // If this happens, something has gone terribly wrong. + print('Cannot seek without lookAt position'); + isSeeking = false; return; } // E.g., if parallel to location in HUD var copy = Reticle.position; @@ -229,6 +264,11 @@ function expireMouseCursor(now) { Reticle.visible = false; } } +function hudReticleDistance() { // 3d distance from camera to the reticle position on hud + // (The camera is only in the center of the sphere on reset.) + var reticlePositionOnHUD = HMD.worldPointFromOverlay(Reticle.position); + return Vec3.distance(reticlePositionOnHUD, HMD.position); +} function onMouseMove() { // Display cursor at correct depth (as in depthReticle.js), and updateMouseActivity. if (ignoreMouseActivity()) { @@ -238,11 +278,10 @@ function onMouseMove() { if (HMD.active) { // set depth updateSeeking(); if (isPointingAtOverlay()) { - Reticle.setDepth(SPHERICAL_HUD_DISTANCE); // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! + Reticle.depth = hudReticleDistance(); } else { var result = findRayIntersection(Camera.computePickRay(Reticle.position.x, Reticle.position.y)); - var depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; - Reticle.setDepth(depth); + Reticle.depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; } } updateMouseActivity(); // After the above, just in case the depth movement is awkward when becoming visible. @@ -257,113 +296,112 @@ setupHandler(Controller.mouseDoublePressEvent, onMouseClick); // CONTROLLER MAPPING --------- // +var leftTrigger = new Trigger('left'); +var rightTrigger = new Trigger('right'); +var activeTrigger = rightTrigger; var activeHand = Controller.Standard.RightHand; -function toggleHand() { +var LEFT_HUD_LASER = 1; +var RIGHT_HUD_LASER = 2; +var BOTH_HUD_LASERS = LEFT_HUD_LASER + RIGHT_HUD_LASER; +var activeHudLaser = RIGHT_HUD_LASER; +function toggleHand() { // unequivocally switch which hand controls mouse position if (activeHand === Controller.Standard.RightHand) { activeHand = Controller.Standard.LeftHand; + activeTrigger = leftTrigger; + activeHudLaser = LEFT_HUD_LASER; } else { activeHand = Controller.Standard.RightHand; + activeTrigger = rightTrigger; + activeHudLaser = RIGHT_HUD_LASER; } + clearSystemLaser(); +} +function makeToggleAction(hand) { // return a function(0|1) that makes the specified hand control mouse when 1 + return function (on) { + if (on && (activeHand !== hand)) { + toggleHand(); + } + }; } var clickMapping = Controller.newMapping(Script.resolvePath('') + '-click'); Script.scriptEnding.connect(clickMapping.disable); -clickMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(Controller.Actions.ReticleClick); -clickMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(Controller.Actions.ReticleClick); +// Gather the trigger data for smoothing. +clickMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); +clickMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); +// Full smoothed trigger is a click. +function isPointingAtOverlayStartedNonFullTrigger(trigger) { + // true if isPointingAtOverlay AND we were NOT full triggered when we became so. + // The idea is to not count clicks when we're full-triggering and reach the edge of a window. + var lockedIn = false; + return function () { + if (trigger !== activeTrigger) { + return lockedIn = false; + } + if (!isPointingAtOverlay()) { + return lockedIn = false; + } + if (lockedIn) { + return true; + } + lockedIn = !trigger.full(); + return lockedIn; + } +} +clickMapping.from(rightTrigger.full).when(isPointingAtOverlayStartedNonFullTrigger(rightTrigger)).to(Controller.Actions.ReticleClick); +clickMapping.from(leftTrigger.full).when(isPointingAtOverlayStartedNonFullTrigger(leftTrigger)).to(Controller.Actions.ReticleClick); clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu); clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(Controller.Actions.ContextMenu); -clickMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(function (on) { - if (on && (activeHand !== Controller.Standard.RightHand)) { - toggleHand(); - } -}); -clickMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(function (on) { - if (on && (activeHand !== Controller.Standard.LeftHand)) { - toggleHand(); - } -}); +// Partial smoothed trigger is activation. +clickMapping.from(rightTrigger.partial).to(makeToggleAction(Controller.Standard.RightHand)); +clickMapping.from(leftTrigger.partial).to(makeToggleAction(Controller.Standard.LeftHand)); clickMapping.enable(); // VISUAL AID ----------- // Same properties as handControllerGrab search sphere -var BALL_SIZE = 0.011; -var BALL_ALPHA = 0.5; -var fakeProjectionBall = Overlays.addOverlay("sphere", { - size: 5 * BALL_SIZE, - color: {red: 255, green: 10, blue: 10}, - ignoreRayIntersection: true, - alpha: BALL_ALPHA, - visible: false, - solid: true, - drawInFront: true // Even when burried inside of something, show it. -}); -var overlays = [fakeProjectionBall]; // If we want to try showing multiple balls and lasers. -Script.scriptEnding.connect(function () { - overlays.forEach(Overlays.deleteOverlay); -}); -var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation. -function turnOffVisualization(optionalEnableClicks) { // because we're showing cursor on HUD - if (!optionalEnableClicks) { - expireMouseCursor(); - } - if (!visualizationIsShowing) { +var LASER_ALPHA = 0.5; +var LASER_SEARCH_COLOR_XYZW = {x: 10 / 255, y: 10 / 255, z: 255 / 255, w: LASER_ALPHA}; +var LASER_TRIGGER_COLOR_XYZW = {x: 250 / 255, y: 10 / 255, z: 10 / 255, w: LASER_ALPHA}; +var SYSTEM_LASER_DIRECTION = {x: 0, y: 0, z: -1}; +var systemLaserOn = false; +function clearSystemLaser() { + if (!systemLaserOn) { return; } - visualizationIsShowing = false; - overlays.forEach(function (overlay) { - Overlays.editOverlay(overlay, {visible: false}); - }); + HMD.disableHandLasers(BOTH_HUD_LASERS); + systemLaserOn = false; } -var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. -function updateVisualization(controllerPosition, controllerDirection, hudPosition3d, hudPosition2d) { - // Show an indication of where the cursor will appear when crossing a HUD element, - // and where in-world clicking will occur. - // - // There are a number of ways we could do this, but for now, it's a blue sphere that rolls along - // the HUD surface, and a red sphere that rolls along the 3d objects that will receive the click. - // We'll leave it to other scripts (like handControllerGrab) to show a search beam when desired. +function setColoredLaser() { // answer trigger state if lasers supported, else falsey. + var color = (activeTrigger.state === 'full') ? LASER_TRIGGER_COLOR_XYZW : LASER_SEARCH_COLOR_XYZW; + return HMD.setHandLasers(activeHudLaser, true, color, SYSTEM_LASER_DIRECTION) && activeTrigger.state; - function intersection3d(position, direction) { - // Answer in-world intersection (entity or 3d overlay), or way-out point - var pickRay = {origin: position, direction: direction}; - var result = findRayIntersection(pickRay); - return result.intersects ? result.intersection : Vec3.sum(position, Vec3.multiply(MAX_RAY_SCALE, direction)); - } - - visualizationIsShowing = true; - // We'd rather in-world interactions be done at the termination of the hand beam - // -- intersection3d(controllerPosition, controllerDirection). Maybe have handControllerGrab - // direclty manipulate both entity and 3d overlay objects. - // For now, though, we present a false projection of the cursor onto whatever is below it. This is - // different from the hand beam termination because the false projection is from the camera, while - // the hand beam termination is from the hand. - var eye = Camera.getPosition(); - var falseProjection = intersection3d(eye, Vec3.subtract(hudPosition3d, eye)); - Overlays.editOverlay(fakeProjectionBall, {visible: true, position: falseProjection}); - Reticle.visible = false; - - return visualizationIsShowing; // In case we change caller to act conditionally. } // MAIN OPERATIONS ----------- // function update() { var now = Date.now(); - if (!handControllerLockOut.expired(now)) { - return turnOffVisualization(); - } // Let them use mouse it in peace. - if (!Menu.isOptionChecked("First Person")) { - return turnOffVisualization(); - } // What to do? menus can be behind hand! - if (!Window.hasFocus()) { // Don't mess with other apps - return turnOffVisualization(); + function off() { + expireMouseCursor(); + clearSystemLaser(); } + if (!handControllerLockOut.expired(now)) { + return off(); // Let them use mouse it in peace. + } + if (!Menu.isOptionChecked("First Person")) { + return off(); // What to do? menus can be behind hand! + } + if (!Window.hasFocus() || !Reticle.allowMouseCapture) { + return off(); // Don't mess with other apps or paused mouse activity + } + leftTrigger.update(); + rightTrigger.update(); var controllerPose = Controller.getPoseValue(activeHand); // Valid if any plugged-in hand controller is "on". (uncradled Hydra, green-lighted Vive...) if (!controllerPose.valid) { - return turnOffVisualization(); - } // Controller is cradled. + return off(); // Controller is cradled. + } var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), MyAvatar.position); // This gets point direction right, but if you want general quaternion it would be more complicated: @@ -371,10 +409,10 @@ function update() { var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); if (!hudPoint3d) { - // FIXME - determine if this message is useful but make it so it doesn't spam the - // log in the case that it is happening - //print('Controller is parallel to HUD'); - return turnOffVisualization(); + if (Menu.isOptionChecked("Overlays")) { // With our hud resetting strategy, hudPoint3d should be valid here + print('Controller is parallel to HUD'); // so let us know that our assumptions are wrong. + } + return off(); } var hudPoint2d = overlayFromWorldPoint(hudPoint3d); @@ -383,14 +421,25 @@ function update() { setReticlePosition(hudPoint2d); // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. if (isPointingAtOverlay(hudPoint2d)) { - if (HMD.active) { // Doesn't hurt anything without the guard, but consider it documentation. - Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! + if (HMD.active) { + Reticle.depth = hudReticleDistance(); } - Reticle.visible = true; - return turnOffVisualization(true); + if (activeTrigger.state && (!systemLaserOn || (systemLaserOn !== activeTrigger.state))) { // last=>wrong color + // If the active plugin doesn't implement hand lasers, show the mouse reticle instead. + systemLaserOn = setColoredLaser(); + Reticle.visible = !systemLaserOn; + } else if ((systemLaserOn || Reticle.visible) && !activeTrigger.state) { + clearSystemLaser(); + Reticle.visible = false; + } + return; } // We are not pointing at a HUD element (but it could be a 3d overlay). - updateVisualization(controllerPosition, controllerDirection, hudPoint3d, hudPoint2d); + if (!activeTrigger.state) { + return off(); // No trigger + } + clearSystemLaser(); + Reticle.visible = false; } var UPDATE_INTERVAL = 50; // milliseconds. Script.update is too frequent. diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 42eddf11c3..d1ac32e6f4 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -150,6 +150,8 @@ function showMarketplace(marketplaceID) { marketplaceWindow.setURL(url); marketplaceWindow.setVisible(true); marketplaceWindow.raise(); + + UserActivityLogger.logAction("opened_marketplace"); } function hideMarketplace() { @@ -181,7 +183,7 @@ var toolBar = (function() { function initialize() { toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.edit.toolbar", function(windowDimensions, toolbar) { return { - x: windowDimensions.x / 2, + x: (windowDimensions.x / 2) + (Tool.IMAGE_WIDTH * 2), y: windowDimensions.y }; }, { @@ -347,6 +349,7 @@ var toolBar = (function() { selectionManager.clearSelections(); cameraManager.disable(); } else { + UserActivityLogger.enabledEdit(); hasShownPropertiesTool = false; entityListTool.setVisible(true); gridTool.setVisible(true); diff --git a/scripts/system/examples.js b/scripts/system/examples.js index 9d33e473af..a948f9e563 100644 --- a/scripts/system/examples.js +++ b/scripts/system/examples.js @@ -9,10 +9,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -Script.include([ - "libraries/toolBars.js", -]); - var toolIconUrl = Script.resolvePath("assets/images/tools/"); var EXAMPLES_URL = "https://metaverse.highfidelity.com/examples"; @@ -37,6 +33,8 @@ function showExamples(marketplaceID) { print("setting examples URL to " + url); examplesWindow.setURL(url); examplesWindow.setVisible(true); + + UserActivityLogger.openedMarketplace(); } function hideExamples() { @@ -52,87 +50,21 @@ function toggleExamples() { } } -var toolBar = (function() { - var that = {}, - toolBar, - browseExamplesButton; +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - function initialize() { - toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.examples.toolbar", function(windowDimensions, toolbar) { - return { - x: windowDimensions.x / 2, - y: windowDimensions.y - }; - }, { - x: -toolWidth / 2, - y: -TOOLBAR_MARGIN_Y - toolHeight - }); - browseExamplesButton = toolBar.addTool({ - imageURL: toolIconUrl + "examples-01.svg", - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: toolWidth, - height: toolHeight, - alpha: 0.9, - visible: true, - showButtonDown: true - }); +var browseExamplesButton = toolBar.addButton({ + imageURL: toolIconUrl + "examples-01.svg", + objectName: "examples", + yOffset: 50, + alpha: 0.9, +}); - toolBar.showTool(browseExamplesButton, true); - } +var browseExamplesButtonDown = false; - var browseExamplesButtonDown = false; - that.mousePressEvent = function(event) { - var clickedOverlay, - url, - file; +browseExamplesButton.clicked.connect(function(){ + toggleExamples(); +}); - if (!event.isLeftButton) { - // if another mouse button than left is pressed ignore it - return false; - } - - clickedOverlay = Overlays.getOverlayAtPoint({ - x: event.x, - y: event.y - }); - - if (browseExamplesButton === toolBar.clicked(clickedOverlay)) { - toggleExamples(); - return true; - } - - return false; - }; - - that.mouseReleaseEvent = function(event) { - var handled = false; - - - if (browseExamplesButtonDown) { - var clickedOverlay = Overlays.getOverlayAtPoint({ - x: event.x, - y: event.y - }); - } - - newModelButtonDown = false; - browseExamplesButtonDown = false; - - return handled; - } - - that.cleanup = function() { - toolBar.cleanup(); - }; - - initialize(); - return that; -}()); - -Controller.mousePressEvent.connect(toolBar.mousePressEvent) -Script.scriptEnding.connect(toolBar.cleanup); \ No newline at end of file +Script.scriptEnding.connect(function () { + browseExamplesButton.clicked.disconnect(); +}); diff --git a/scripts/system/goto.js b/scripts/system/goto.js new file mode 100644 index 0000000000..a2ade02a78 --- /dev/null +++ b/scripts/system/goto.js @@ -0,0 +1,29 @@ +// +// goto.js +// scripts/system/ +// +// Created by Howard Stearns on 2 Jun 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + + +var button = toolBar.addButton({ + objectName: "goto", + imageURL: Script.resolvePath("assets/images/tools/directory-01.svg"), + visible: true, + yOffset: 50, + alpha: 0.9, +}); + +button.clicked.connect(function(){ + DialogsManager.toggleAddressBar(); +}); + +Script.scriptEnding.connect(function () { + button.clicked.disconnect(); +}); diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js new file mode 100644 index 0000000000..2965c0d254 --- /dev/null +++ b/scripts/system/hmd.js @@ -0,0 +1,42 @@ +// +// hmd.js +// scripts/system/ +// +// Created by Howard Stearns on 2 Jun 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var headset; // The preferred headset. Default to the first one found in the following list. +var displayMenuName = "Display"; +var desktopMenuItemName = "Desktop"; +['OpenVR (Vive)', 'Oculus Rift'].forEach(function (name) { + if (!headset && Menu.menuItemExists(displayMenuName, name)) { + headset = name; + } +}); + +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); +var button; + +if (headset) { + button = toolBar.addButton({ + objectName: "hmdToggle", + imageURL: Script.resolvePath("assets/images/tools/hmd-switch-01.svg"), + visible: true, + yOffset: 50, + alpha: 0.9, + }); + + button.clicked.connect(function(){ + var isDesktop = Menu.isOptionChecked(desktopMenuItemName); + Menu.setIsOptionChecked(isDesktop ? headset : desktopMenuItemName, true); + }); + + Script.scriptEnding.connect(function () { + button.clicked.disconnect(); + }); +} + diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 0af199ef56..121e38c340 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -1646,6 +1646,7 @@ +
diff --git a/scripts/system/mute.js b/scripts/system/mute.js new file mode 100644 index 0000000000..f66b6852ea --- /dev/null +++ b/scripts/system/mute.js @@ -0,0 +1,29 @@ +// +// goto.js +// scripts/system/ +// +// Created by Howard Stearns on 2 Jun 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + + +var button = toolBar.addButton({ + objectName: "mute", + imageURL: Script.resolvePath("assets/images/tools/microphone.svg"), + visible: true, + alpha: 0.9, +}); + +button.clicked.connect(function(){ + var menuItem = "Mute Microphone"; + Menu.setIsOptionChecked(menuItem, !Menu.isOptionChecked(menuItem)); +}); + +Script.scriptEnding.connect(function () { + button.clicked.disconnect(); +}); diff --git a/tests/physics/src/ShapeManagerTests.cpp b/tests/physics/src/ShapeManagerTests.cpp index 66ac9d0c4a..c8805132fa 100644 --- a/tests/physics/src/ShapeManagerTests.cpp +++ b/tests/physics/src/ShapeManagerTests.cpp @@ -194,23 +194,23 @@ void ShapeManagerTests::addCompoundShape() { int numHullPoints = tetrahedron.size(); // compute the points of the hulls - QVector< QVector > hulls; + ShapeInfo::PointCollection pointCollection; int numHulls = 5; glm::vec3 offsetNormal(1.0f, 0.0f, 0.0f); for (int i = 0; i < numHulls; ++i) { glm::vec3 offset = (float)(i - numHulls/2) * offsetNormal; - QVector hull; + ShapeInfo::PointList pointList; float radius = (float)(i + 1); for (int j = 0; j < numHullPoints; ++j) { glm::vec3 point = radius * tetrahedron[j] + offset; - hull.push_back(point); + pointList.push_back(point); } - hulls.push_back(hull); + pointCollection.push_back(pointList); } // create the ShapeInfo ShapeInfo info; - info.setConvexHulls(hulls); + info.setPointCollection(hulls); // create the shape ShapeManager shapeManager; diff --git a/tests/ui/qml/Stubs.qml b/tests/ui/qml/Stubs.qml index 8f828a0186..8c1465d54c 100644 --- a/tests/ui/qml/Stubs.qml +++ b/tests/ui/qml/Stubs.qml @@ -23,11 +23,23 @@ Item { function getUsername() { return "Jherico"; } } + Item { + objectName: "GL" + property string vendor: "" + } + Item { objectName: "ApplicationCompositor" property bool reticleOverDesktop: true } + Item { + objectName: "Controller" + function getRecommendedOverlayRect() { + return Qt.rect(0, 0, 1920, 1080); + } + } + Item { objectName: "Preferences" // List of categories obtained by logging categories as they are added in Interface in Preferences::addPreference(). diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index 97f0c0a613..47d0f6d601 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -5,7 +5,7 @@ import Qt.labs.settings 1.0 import "../../../interface/resources/qml" //import "../../../interface/resources/qml/windows" -import "../../../interface/resources/qml/windows-uit" +import "../../../interface/resources/qml/windows" import "../../../interface/resources/qml/dialogs" import "../../../interface/resources/qml/hifi" import "../../../interface/resources/qml/hifi/dialogs" @@ -17,6 +17,311 @@ ApplicationWindow { width: 1280 height: 800 title: qsTr("Scratch App") + toolBar: Row { + id: testButtons + anchors { margins: 8; left: parent.left; top: parent.top } + spacing: 8 + property int count: 0 + + property var tabs: []; + property var urls: []; + property var toolbar; + property var lastButton; + + // Window visibility + Button { + text: "toggle desktop" + onClicked: desktop.togglePinned() + } + + Button { + text: "Create Toolbar" + onClicked: testButtons.toolbar = desktop.getToolbar("com.highfidelity.interface.toolbar.system"); + } + + Button { + text: "Toggle Toolbar Direction" + onClicked: testButtons.toolbar.horizontal = !testButtons.toolbar.horizontal + } + + Button { + readonly property var icons: [ + "edit-01.svg", + "model-01.svg", + "cube-01.svg", + "sphere-01.svg", + "light-01.svg", + "text-01.svg", + "web-01.svg", + "zone-01.svg", + "particle-01.svg", + ] + property int iconIndex: 0 + readonly property string toolIconUrl: "../../../../../scripts/system/assets/images/tools/" + text: "Create Button" + onClicked: { + var name = icons[iconIndex]; + var url = toolIconUrl + name; + iconIndex = (iconIndex + 1) % icons.length; + var button = testButtons.lastButton = testButtons.toolbar.addButton({ + imageURL: url, + objectName: name, + subImage: { + y: 50, + }, + alpha: 0.9 + }); + + button.clicked.connect(function(){ + console.log("Clicked on button " + button.imageURL + " alpha " + button.alpha) + }); + } + } + + Button { + text: "Toggle Button Visible" + onClicked: testButtons.lastButton.visible = !testButtons.lastButton.visible + } + + + + + // Error alerts + /* + Button { + // Message without title. + text: "Show Error" + onClicked: { + var messageBox = desktop.messageBox({ + text: "Diagnostic cycle will be complete in 30 seconds", + icon: hifi.icons.critical, + }); + messageBox.selected.connect(function(button) { + console.log("You clicked " + button) + }) + } + } + Button { + // detailedText is not currently used anywhere in Interface but it is easier to leave in and style good enough. + text: "Show Long Error" + onClicked: { + desktop.messageBox({ + informativeText: "Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds ", + text: "Baloney", + icon: hifi.icons.warning, + detailedText: "sakjd;laskj dksa;dl jka;lsd j;lkjas ;dlkaj s;dlakjd ;alkjda; slkjda; lkjda;lksjd ;alksjd; alksjd ;alksjd; alksjd; alksdjas;ldkjas;lkdja ;kj ;lkasjd; lkj as;dlka jsd;lka jsd;laksjd a" + }); + } + } + */ + + // query + /* + // There is no such desktop.queryBox() function; may need to update test to cover QueryDialog.qml? + Button { + text: "Show Query" + onClicked: { + var queryBox = desktop.queryBox({ + text: "Have you stopped beating your wife?", + placeholderText: "Are you sure?", + // icon: hifi.icons.critical, + }); + queryBox.selected.connect(function(result) { + console.log("User responded with " + result); + }); + + queryBox.canceled.connect(function() { + console.log("User cancelled query box "); + }) + } + } + */ + + // Browser + /* + Button { + text: "Open Browser" + onClicked: builder.createObject(desktop); + property var builder: Component { + Browser {} + } + } + */ + + + // file dialog + /* + + Button { + text: "Open Directory" + property var builder: Component { + FileDialog { selectDirectory: true } + } + + onClicked: { + var fileDialog = builder.createObject(desktop); + fileDialog.canceled.connect(function(){ + console.log("Cancelled") + }) + fileDialog.selectedFile.connect(function(file){ + console.log("Selected " + file) + }) + } + } + Button { + text: "Open File" + property var builder: Component { + FileDialog { + title: "Open File" + filter: "All Files (*.*)" + //filter: "HTML files (*.html);;Other(*.png)" + } + } + + onClicked: { + var fileDialog = builder.createObject(desktop); + fileDialog.canceled.connect(function(){ + console.log("Cancelled") + }) + fileDialog.selectedFile.connect(function(file){ + console.log("Selected " + file) + }) + } + } + */ + + // tabs + /* + Button { + text: "Add Tab" + onClicked: { + console.log(desktop.toolWindow); + desktop.toolWindow.addWebTab({ source: "Foo" }); + desktop.toolWindow.showTabForUrl("Foo", true); + } + } + + Button { + text: "Add Tab 2" + onClicked: { + console.log(desktop.toolWindow); + desktop.toolWindow.addWebTab({ source: "Foo 2" }); + desktop.toolWindow.showTabForUrl("Foo 2", true); + } + } + + Button { + text: "Add Tab 3" + onClicked: { + console.log(desktop.toolWindow); + desktop.toolWindow.addWebTab({ source: "Foo 3" }); + desktop.toolWindow.showTabForUrl("Foo 3", true); + } + } + + Button { + text: "Destroy Tab" + onClicked: { + console.log(desktop.toolWindow); + desktop.toolWindow.removeTabForUrl("Foo"); + } + } + */ + + // Hifi specific stuff + /* + Button { + // Shows the dialog with preferences sections but not each section's preference items + // because Preferences.preferencesByCategory() method is not stubbed out. + text: "Settings > General..." + property var builder: Component { + GeneralPreferencesDialog { } + } + onClicked: { + var runningScripts = builder.createObject(desktop); + } + } + + Button { + text: "Running Scripts" + property var builder: Component { + RunningScripts { } + } + onClicked: { + var runningScripts = builder.createObject(desktop); + } + } + + Button { + text: "Attachments" + property var builder: Component { + AttachmentsDialog { } + } + onClicked: { + var attachmentsDialog = builder.createObject(desktop); + } + } + Button { + // Replicates message box that pops up after selecting new avatar. Includes title. + text: "Confirm Avatar" + onClicked: { + var messageBox = desktop.messageBox({ + title: "Set Avatar", + text: "Would you like to use 'Albert' for your avatar?", + icon: hifi.icons.question, // Test question icon + //icon: hifi.icons.information, // Test informaton icon + //icon: hifi.icons.warning, // Test warning icon + //icon: hifi.icons.critical, // Test critical icon + //icon: hifi.icons.none, // Test no icon + buttons: OriginalDialogs.StandardButton.Ok + OriginalDialogs.StandardButton.Cancel, + defaultButton: OriginalDialogs.StandardButton.Ok + }); + messageBox.selected.connect(function(button) { + console.log("You clicked " + button) + }) + } + } + */ + // bookmarks + /* + Button { + text: "Bookmark Location" + onClicked: { + desktop.inputDialog({ + title: "Bookmark Location", + icon: hifi.icons.placemark, + label: "Name" + }); + } + } + Button { + text: "Delete Bookmark" + onClicked: { + desktop.inputDialog({ + title: "Delete Bookmark", + icon: hifi.icons.placemark, + label: "Select the bookmark to delete", + items: ["Bookmark A", "Bookmark B", "Bookmark C"] + }); + } + } + Button { + text: "Duplicate Bookmark" + onClicked: { + desktop.messageBox({ + title: "Duplicate Bookmark", + icon: hifi.icons.warning, + text: "The bookmark name you entered alread exists in yoru list.", + informativeText: "Would you like to overwrite it?", + buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No, + defaultButton: OriginalDialogs.StandardButton.Yes + }); + } + } + */ + + } + HifiConstants { id: hifi } @@ -35,249 +340,13 @@ ApplicationWindow { } */ - Row { - id: testButtons - anchors { margins: 8; left: parent.left; top: parent.top } - spacing: 8 - property int count: 0 - - property var tabs: []; - property var urls: []; - - Button { - // Shows the dialog with preferences sections but not each section's preference items - // because Preferences.preferencesByCategory() method is not stubbed out. - text: "Settings > General..." - property var builder: Component { - GeneralPreferencesDialog { } - } - onClicked: { - var runningScripts = builder.createObject(desktop); - } - } - - Button { - text: "Running Scripts" - property var builder: Component { - RunningScripts { } - } - onClicked: { - var runningScripts = builder.createObject(desktop); - } - } - - Button { - text: "Attachments" - property var builder: Component { - AttachmentsDialog { } - } - onClicked: { - var attachmentsDialog = builder.createObject(desktop); - } - } - - /* - Button { - text: "restore all" - onClicked: { - for (var i = 0; i < desktop.windows.length; ++i) { - desktop.windows[i].visible = true - } - } - } - Button { - text: "toggle blue visible" - onClicked: { - blue.visible = !blue.visible - } - } - Button { - text: "toggle blue enabled" - onClicked: { - blue.enabled = !blue.enabled - } - } - */ - Button { - // Replicates message box that pops up after selecting new avatar. Includes title. - text: "Confirm Avatar" - onClicked: { - var messageBox = desktop.messageBox({ - title: "Set Avatar", - text: "Would you like to use 'Albert' for your avatar?", - icon: hifi.icons.question, // Test question icon - //icon: hifi.icons.information, // Test informaton icon - //icon: hifi.icons.warning, // Test warning icon - //icon: hifi.icons.critical, // Test critical icon - //icon: hifi.icons.none, // Test no icon - buttons: OriginalDialogs.StandardButton.Ok + OriginalDialogs.StandardButton.Cancel, - defaultButton: OriginalDialogs.StandardButton.Ok - }); - messageBox.selected.connect(function(button) { - console.log("You clicked " + button) - }) - } - } - Button { - // Message without title. - text: "Show Error" - onClicked: { - var messageBox = desktop.messageBox({ - text: "Diagnostic cycle will be complete in 30 seconds", - icon: hifi.icons.critical, - }); - messageBox.selected.connect(function(button) { - console.log("You clicked " + button) - }) - } - } - Button { - // detailedText is not currently used anywhere in Interface but it is easier to leave in and style good enough. - text: "Show Long Error" - onClicked: { - desktop.messageBox({ - informativeText: "Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds ", - text: "Baloney", - icon: hifi.icons.warning, - detailedText: "sakjd;laskj dksa;dl jka;lsd j;lkjas ;dlkaj s;dlakjd ;alkjda; slkjda; lkjda;lksjd ;alksjd; alksjd ;alksjd; alksjd; alksdjas;ldkjas;lkdja ;kj ;lkasjd; lkj as;dlka jsd;lka jsd;laksjd a" - }); - } - } - Button { - text: "Bookmark Location" - onClicked: { - desktop.inputDialog({ - title: "Bookmark Location", - icon: hifi.icons.placemark, - label: "Name" - }); - } - } - Button { - text: "Delete Bookmark" - onClicked: { - desktop.inputDialog({ - title: "Delete Bookmark", - icon: hifi.icons.placemark, - label: "Select the bookmark to delete", - items: ["Bookmark A", "Bookmark B", "Bookmark C"] - }); - } - } - Button { - text: "Duplicate Bookmark" - onClicked: { - desktop.messageBox({ - title: "Duplicate Bookmark", - icon: hifi.icons.warning, - text: "The bookmark name you entered alread exists in yoru list.", - informativeText: "Would you like to overwrite it?", - buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No, - defaultButton: OriginalDialogs.StandardButton.Yes - }); - } - } - /* - // There is no such desktop.queryBox() function; may need to update test to cover QueryDialog.qml? - Button { - text: "Show Query" - onClicked: { - var queryBox = desktop.queryBox({ - text: "Have you stopped beating your wife?", - placeholderText: "Are you sure?", - // icon: hifi.icons.critical, - }); - queryBox.selected.connect(function(result) { - console.log("User responded with " + result); - }); - - queryBox.canceled.connect(function() { - console.log("User cancelled query box "); - }) - } - } - */ - Button { - text: "Open Directory" - property var builder: Component { - FileDialog { selectDirectory: true } - } - - onClicked: { - var fileDialog = builder.createObject(desktop); - fileDialog.canceled.connect(function(){ - console.log("Cancelled") - }) - fileDialog.selectedFile.connect(function(file){ - console.log("Selected " + file) - }) - } - } - - Button { - text: "Open File" - property var builder: Component { - FileDialog { - title: "Open File" - filter: "All Files (*.*)" - //filter: "HTML files (*.html);;Other(*.png)" - } - } - - onClicked: { - var fileDialog = builder.createObject(desktop); - fileDialog.canceled.connect(function(){ - console.log("Cancelled") - }) - fileDialog.selectedFile.connect(function(file){ - console.log("Selected " + file) - }) - } - } - - Button { - text: "Add Tab" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo" }); - desktop.toolWindow.showTabForUrl("Foo", true); - } - } - - Button { - text: "Add Tab 2" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo 2" }); - desktop.toolWindow.showTabForUrl("Foo 2", true); - } - } - - Button { - text: "Add Tab 3" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo 3" }); - desktop.toolWindow.showTabForUrl("Foo 3", true); - } - } - - Button { - text: "Destroy Tab" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.removeTabForUrl("Foo"); - } - } - - } /* Window { id: blue closable: true visible: true resizable: true - destroyOnInvisible: false + destroyOnHidden: false width: 100; height: 100 x: 1280 / 2; y: 720 / 2 @@ -296,32 +365,35 @@ ApplicationWindow { Component.onDestruction: console.log("Blue destroyed") } } - */ - /* + + + Rectangle { width: 100; height: 100; x: 100; y: 100; color: "#00f" } + Window { id: green alwaysOnTop: true + frame: HiddenFrame{} + hideBackground: true closable: true visible: true resizable: false x: 1280 / 2; y: 720 / 2 - Settings { - category: "TestWindow.Green" - property alias x: green.x - property alias y: green.y - property alias width: green.width - property alias height: green.height - } width: 100; height: 100 - Rectangle { anchors.fill: parent; color: "green" } + Rectangle { + color: "#0f0" + width: green.width; + height: green.height; + } } + */ +/* Window { id: yellow - objectName: "Yellow" closable: true visible: true resizable: true + x: 100; y: 100 width: 100; height: 100 Rectangle { anchors.fill: parent @@ -329,10 +401,11 @@ ApplicationWindow { color: "yellow" } } - */ +*/ } Action { + id: openBrowserAction text: "Open Browser" shortcut: "Ctrl+Shift+X" onTriggered: { @@ -343,3 +416,7 @@ ApplicationWindow { } } } + + + + diff --git a/tests/ui/qmlscratch.pro b/tests/ui/qmlscratch.pro index 417d7dad5b..151893de2f 100644 --- a/tests/ui/qmlscratch.pro +++ b/tests/ui/qmlscratch.pro @@ -18,6 +18,7 @@ DISTFILES += \ qml/*.qml \ ../../interface/resources/qml/*.qml \ ../../interface/resources/qml/controls/*.qml \ + ../../interface/resources/qml/controls-uit/*.qml \ ../../interface/resources/qml/dialogs/*.qml \ ../../interface/resources/qml/dialogs/fileDialog/*.qml \ ../../interface/resources/qml/dialogs/preferences/*.qml \ @@ -25,9 +26,10 @@ DISTFILES += \ ../../interface/resources/qml/desktop/*.qml \ ../../interface/resources/qml/menus/*.qml \ ../../interface/resources/qml/styles/*.qml \ + ../../interface/resources/qml/styles-uit/*.qml \ ../../interface/resources/qml/windows/*.qml \ ../../interface/resources/qml/hifi/*.qml \ + ../../interface/resources/qml/hifi/toolbars/*.qml \ ../../interface/resources/qml/hifi/dialogs/*.qml \ ../../interface/resources/qml/hifi/dialogs/preferences/*.qml \ ../../interface/resources/qml/hifi/overlays/*.qml - diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 0cabfe28f5..e3cf37ba04 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -86,9 +86,11 @@ int main(int argc, char *argv[]) { setChild(engine, "offscreenFlags"); setChild(engine, "Account"); setChild(engine, "ApplicationCompositor"); + setChild(engine, "Controller"); setChild(engine, "Desktop"); setChild(engine, "ScriptDiscoveryService"); setChild(engine, "HMD"); + setChild(engine, "GL"); setChild(engine, "MenuHelper"); setChild(engine, "Preferences"); setChild(engine, "urlHandler"); @@ -101,3 +103,4 @@ int main(int argc, char *argv[]) { } #include "main.moc" +