diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 5836d5bfb5..90d2995e7d 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -39,7 +39,7 @@ Go to `Control Panel > System > Advanced System Settings > Environment Variables ### Step 6. Installing OpenSSL via vcpkg - * In the vcpkg directory, install the 64 bit OpenSSL package with the command `vcpkg install openssl:x64-windows` + * In the vcpkg directory, install the 64 bit OpenSSL package with the command `.\vcpkg install openssl:x64-windows` * Once the build completes you should have a file `ssl.h` in `${VCPKG_ROOT}/installed/x64-windows/include/openssl` ### Step 7. Running CMake to Generate Build Files diff --git a/README.md b/README.md index e0bbed3105..363064964a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ High Fidelity (hifi) is an early-stage technology lab experimenting with Virtual Worlds and VR. -In this repository you'll find the source to many of the components in our -alpha-stage virtual world. The project embraces distributed development -and if you'd like to help, we'll pay you -- find out more at [Worklist.net](https://worklist.net). +This repository contains the source to many of the components in our +alpha-stage virtual world. The project embraces distributed development. +If you'd like to help, we'll pay you -- find out more at [Worklist.net](https://worklist.net). If you find a small bug and have a fix, pull requests are welcome. If you'd like to get paid for your work, make sure you report the bug via a job on [Worklist.net](https://worklist.net). @@ -32,9 +32,10 @@ Running Interface When you launch interface, you will automatically connect to our default domain: "root.highfidelity.io". If you don't see anything, make sure your preferences are pointing to -root.highfidelity.io (set your domain via Cmnd+D/Cntrl+D), if you still have no luck it's possible our servers are -simply down; if you're experiencing a major bug, let us know by adding an issue to this repository. -Make sure to include details about your computer and how to reproduce the bug. +root.highfidelity.io (set your domain via Cmnd+D/Cntrl+D). If you still have no luck, +it's possible our servers are down. If you're experiencing a major bug, let us know by +adding an issue to this repository. Include details about your computer and how to +reproduce the bug in your issue. To move around in-world, use the arrow keys (and Shift + up/down to fly up or down) or W A S D, and E or C to fly up/down. All of the other possible options @@ -48,7 +49,8 @@ you to run the full stack of the virtual world. In order to set up your own virtual world, you need to set up and run your own local "domain". -The domain-server gives a number different types of assignments to the assignment-client for different features: audio, avatars, voxels, particles, meta-voxels and models. +The domain-server gives a number different types of assignments to the assignment-client +for different features: audio, avatars, voxels, particles, meta-voxels and models. Follow the instructions in the [build guide](BUILD.md) to build the various components. @@ -56,7 +58,8 @@ From the domain-server build directory, launch a domain-server. ./domain-server -Then, run an assignment-client. The assignment-client uses localhost as its assignment-server and talks to it on port 40102 (the default domain-server port). +Then, run an assignment-client. The assignment-client uses localhost as its assignment-server +and talks to it on port 40102 (the default domain-server port). In a new Terminal window, run: @@ -64,13 +67,20 @@ In a new Terminal window, run: Any target can be terminated with Ctrl-C (SIGINT) in the associated Terminal window. -This assignment-client will grab one assignment from the domain-server. You can tell the assignment-client what type you want it to be with the `-t` option. You can also run an assignment-client that forks off *n* assignment-clients with the `-n` option. The `-min` and `-max` options allow you to set a range of required assignment-clients, this allows you to have flexibility in the number of assignment-clients that are running. See `--help` for more options. +This assignment-client will grab one assignment from the domain-server. You can tell the +assignment-client what type you want it to be with the `-t` option. You can also run an +assignment-client that forks off *n* assignment-clients with the `-n` option. The `-min` +and `-max` options allow you to set a range of required assignment-clients. This allows +you to have flexibility in the number of assignment-clients that are running. +See `--help` for more options. ./assignment-client --min 6 --max 20 -To test things out you'll want to run the Interface client. +To test things out, you'll need to run the Interface client. -To access your local domain in Interface, open your Preferences -- on OS X this is available in the Interface menu, on Linux you'll find it in the File menu. Enter "localhost" in the "Domain server" field. +To access your local domain in Interface, open your Preferences. On OS X, this is available +in the Interface menu. On Linux, you'll find it in the File menu. Enter "localhost" in the +"Domain server" field. -If everything worked you should see that you are connected to at least one server. +If everything worked, you should see that you are connected to at least one server. Nice work! diff --git a/android/app/build.gradle b/android/app/build.gradle index 3537df033d..d5058a7f40 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,10 +1,12 @@ +import org.apache.tools.ant.taskdefs.condition.Os + apply plugin: 'com.android.application' android { compileSdkVersion 26 //buildToolsVersion '27.0.3' - def appVersionCode = Integer.valueOf(RELEASE_NUMBER ?: 1) + def appVersionCode = Integer.valueOf(VERSION_CODE ?: 1) def appVersionName = RELEASE_NUMBER ?: "1.0" defaultConfig { @@ -74,10 +76,12 @@ android { // so our merge has to depend on the external native build variant.externalNativeBuildTasks.each { task -> variant.mergeResources.dependsOn(task) - def uploadDumpSymsTask = rootProject.getTasksByName("uploadBreakpadDumpSyms${variant.name.capitalize()}", false).first() - def runDumpSymsTask = rootProject.getTasksByName("runBreakpadDumpSyms${variant.name.capitalize()}", false).first() - runDumpSymsTask.dependsOn(task) - variant.assemble.dependsOn(uploadDumpSymsTask) + if (Os.isFamily(Os.FAMILY_UNIX)) { + def uploadDumpSymsTask = rootProject.getTasksByName("uploadBreakpadDumpSyms${variant.name.capitalize()}", false).first() + def runDumpSymsTask = rootProject.getTasksByName("runBreakpadDumpSyms${variant.name.capitalize()}", false).first() + runDumpSymsTask.dependsOn(task) + variant.assemble.dependsOn(uploadDumpSymsTask) + } } variant.mergeAssets.doLast { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e763d471cb..aa70d88c52 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -29,6 +29,7 @@ + + + + + + diff --git a/interface/resources/icons/tablet-icons/avatar-i.svg b/interface/resources/icons/tablet-icons/avatar-i.svg new file mode 100644 index 0000000000..2d3f80cbf8 --- /dev/null +++ b/interface/resources/icons/tablet-icons/avatar-i.svg @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/interface/resources/images/FavoriteIconActive.svg b/interface/resources/images/FavoriteIconActive.svg new file mode 100644 index 0000000000..5f03217d27 --- /dev/null +++ b/interface/resources/images/FavoriteIconActive.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/interface/resources/images/FavoriteIconInActive.svg b/interface/resources/images/FavoriteIconInActive.svg new file mode 100644 index 0000000000..7cca31ac66 --- /dev/null +++ b/interface/resources/images/FavoriteIconInActive.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/interface/resources/images/avatarapp/AvatarIsland.jpg b/interface/resources/images/avatarapp/AvatarIsland.jpg new file mode 100644 index 0000000000..f5f649abea Binary files /dev/null and b/interface/resources/images/avatarapp/AvatarIsland.jpg differ diff --git a/interface/resources/images/avatarapp/BodyMart.PNG b/interface/resources/images/avatarapp/BodyMart.PNG new file mode 100644 index 0000000000..c51ca880cb Binary files /dev/null and b/interface/resources/images/avatarapp/BodyMart.PNG differ diff --git a/interface/resources/images/avatarapp/guy-in-circle.svg b/interface/resources/images/avatarapp/guy-in-circle.svg new file mode 100644 index 0000000000..78cbf5186b --- /dev/null +++ b/interface/resources/images/avatarapp/guy-in-circle.svg @@ -0,0 +1,20 @@ + + + + + + + diff --git a/interface/resources/qml/+android/Web3DOverlay.qml b/interface/resources/qml/+android/Web3DOverlay.qml new file mode 100644 index 0000000000..d7b8306d6c --- /dev/null +++ b/interface/resources/qml/+android/Web3DOverlay.qml @@ -0,0 +1,76 @@ +// +// Web3DOverlay.qml +// +// Created by Gabriel Calero & Cristian Duarte on Jun 22, 2018 +// 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 + +Item { + + property string url + RadialGradient { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: "#262626" } + GradientStop { position: 1.0; color: "#000000" } + } + } + + function shortUrl(url) { + var hostBegin = url.indexOf("://"); + if (hostBegin > -1) { + url = url.substring(hostBegin + 3); + } + + var portBegin = url.indexOf(":"); + if (portBegin > -1) { + url = url.substring(0, portBegin); + } + + var pathBegin = url.indexOf("/"); + if (pathBegin > -1) { + url = url.substring(0, pathBegin); + } + + if (url.length > 45) { + url = url.substring(0, 45); + } + + return url; + } + + Text { + id: urlText + text: shortUrl(url) + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors.fill: parent + anchors.rightMargin: 10 + anchors.leftMargin: 10 + font.family: "Cairo" + font.weight: Font.DemiBold + font.pointSize: 48 + fontSizeMode: Text.Fit + color: "#FFFFFF" + minimumPixelSize: 5 + } + + Image { + id: hand + source: "../../icons/hand.svg" + width: 300 + height: 300 + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.bottomMargin: 100 + anchors.rightMargin: 100 + } + + +} diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index f0475dfebd..4474cfb2cd 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -26,6 +26,7 @@ ScrollingWindow { y: 100 Component.onCompleted: { + focus = true shown = true addressBar.text = webview.url } diff --git a/interface/resources/qml/InteractiveWindow.qml b/interface/resources/qml/InteractiveWindow.qml new file mode 100644 index 0000000000..800026710d --- /dev/null +++ b/interface/resources/qml/InteractiveWindow.qml @@ -0,0 +1,288 @@ +// +// InteractiveWindow.qml +// +// Created by Thijs Wenker on 2018-06-25 +// Copyright 2018 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.3 + +import "windows" as Windows +import "controls" +import "controls-uit" as Controls +import "styles" +import "styles-uit" + +Windows.Window { + id: root; + HifiConstants { id: hifi } + title: "InteractiveWindow"; + resizable: true; + // Virtual window visibility + shown: false; + focus: true; + property var channel; + // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer + destroyOnCloseButton: false; + + signal selfDestruct(); + + property var flags: 0; + + property var source; + property var dynamicContent; + property var nativeWindow; + + // custom visibility flag for interactiveWindow to proxy virtualWindow.shown / nativeWindow.visible + property var interactiveWindowVisible: true; + + property point interactiveWindowPosition; + + property size interactiveWindowSize; + + // Keyboard control properties in case needed by QML content. + property bool keyboardEnabled: false; + property bool keyboardRaised: false; + property bool punctuationMode: false; + + property int presentationMode: 0; + + property var initialized: false; + onSourceChanged: { + if (dynamicContent) { + dynamicContent.destroy(); + dynamicContent = null; + } + QmlSurface.load(source, contentHolder, function(newObject) { + dynamicContent = newObject; + if (dynamicContent && dynamicContent.anchors) { + dynamicContent.anchors.fill = contentHolder; + } + }); + } + + function updateInteractiveWindowPositionForMode() { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + x = interactiveWindowPosition.x; + y = interactiveWindowPosition.y; + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + if (interactiveWindowPosition.x === 0 && interactiveWindowPosition.y === 0) { + // default position for native window in center of main application window + nativeWindow.x = Math.floor(Window.x + (Window.innerWidth / 2) - (interactiveWindowSize.width / 2)); + nativeWindow.y = Math.floor(Window.y + (Window.innerHeight / 2) - (interactiveWindowSize.height / 2)); + } else { + nativeWindow.x = interactiveWindowPosition.x; + nativeWindow.y = interactiveWindowPosition.y; + } + } + } + + function updateInteractiveWindowSizeForMode() { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + width = interactiveWindowSize.width; + height = interactiveWindowSize.height; + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + nativeWindow.width = interactiveWindowSize.width; + nativeWindow.height = interactiveWindowSize.height; + } + } + + function updateContentParent() { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + contentHolder.parent = root; + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + contentHolder.parent = nativeWindow.contentItem; + } + } + + function setupPresentationMode() { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + if (nativeWindow) { + nativeWindow.setVisible(false); + } + updateContentParent(); + updateInteractiveWindowPositionForMode(); + shown = interactiveWindowVisible; + } else if (presentationMode === Desktop.PresentationMode.NATIVE) { + shown = false; + if (nativeWindow) { + updateContentParent(); + updateInteractiveWindowPositionForMode(); + nativeWindow.setVisible(interactiveWindowVisible); + } + } else if (presentationMode === modeNotSet) { + console.error("presentationMode should be set."); + } + } + + Component.onCompleted: { + // Fix for parent loss on OSX: + parent.heightChanged.connect(updateContentParent); + parent.widthChanged.connect(updateContentParent); + + x = interactiveWindowPosition.x; + y = interactiveWindowPosition.y; + width = interactiveWindowSize.width; + height = interactiveWindowSize.height; + + nativeWindow = Qt.createQmlObject(' + import QtQuick 2.3; + import QtQuick.Window 2.3; + + Window { + id: root; + Rectangle { + color: hifi.colors.baseGray + anchors.fill: parent + } + }', root, 'InteractiveWindow.qml->nativeWindow'); + nativeWindow.title = root.title; + var nativeWindowFlags = Qt.Window | + Qt.WindowTitleHint | + Qt.WindowSystemMenuHint | + Qt.WindowCloseButtonHint | + Qt.WindowMaximizeButtonHint | + Qt.WindowMinimizeButtonHint; + if ((flags & Desktop.ALWAYS_ON_TOP) === Desktop.ALWAYS_ON_TOP) { + nativeWindowFlags |= Qt.WindowStaysOnTopHint; + } + nativeWindow.flags = nativeWindowFlags; + + nativeWindow.x = interactiveWindowPosition.x; + nativeWindow.y = interactiveWindowPosition.y; + + nativeWindow.width = interactiveWindowSize.width; + nativeWindow.height = interactiveWindowSize.height; + + nativeWindow.xChanged.connect(function() { + if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + interactiveWindowPosition = Qt.point(nativeWindow.x, interactiveWindowPosition.y); + } + }); + nativeWindow.yChanged.connect(function() { + if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + interactiveWindowPosition = Qt.point(interactiveWindowPosition.x, nativeWindow.y); + } + }); + + nativeWindow.widthChanged.connect(function() { + if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + interactiveWindowSize = Qt.size(nativeWindow.width, interactiveWindowSize.height); + } + }); + nativeWindow.heightChanged.connect(function() { + if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + interactiveWindowSize = Qt.size(interactiveWindowSize.width, nativeWindow.height); + } + }); + + nativeWindow.closing.connect(function(closeEvent) { + closeEvent.accepted = false; + windowClosed(); + }); + + // finally set the initial window mode: + setupPresentationMode(); + + initialized = true; + } + + Component.onDestruction: { + parent.heightChanged.disconnect(updateContentParent); + parent.widthChanged.disconnect(updateContentParent); + } + + // Handle message traffic from the script that launched us to the loaded QML + function fromScript(message) { + if (root.dynamicContent && root.dynamicContent.fromScript) { + root.dynamicContent.fromScript(message); + } + } + + function show() { + interactiveWindowVisible = true; + raiseWindow(); + } + + function raiseWindow() { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + raise(); + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + nativeWindow.raise(); + } + } + + // Handle message traffic from our loaded QML to the script that launched us + signal sendToScript(var message); + + onDynamicContentChanged: { + if (dynamicContent && dynamicContent.sendToScript) { + dynamicContent.sendToScript.connect(sendToScript); + } + } + + onInteractiveWindowVisibleChanged: { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + shown = interactiveWindowVisible; + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + if (!nativeWindow.visible && interactiveWindowVisible) { + nativeWindow.showNormal(); + } else { + nativeWindow.setVisible(interactiveWindowVisible); + } + } + } + + onTitleChanged: { + if (nativeWindow) { + nativeWindow.title = title; + } + } + + onXChanged: { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + interactiveWindowPosition = Qt.point(x, interactiveWindowPosition.y); + } + } + + onYChanged: { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + interactiveWindowPosition = Qt.point(interactiveWindowPosition.x, y); + } + } + + onWidthChanged: { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + interactiveWindowSize = Qt.size(width, interactiveWindowSize.height); + } + } + + onHeightChanged: { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + interactiveWindowSize = Qt.size(interactiveWindowSize.width, height); + } + } + + onPresentationModeChanged: { + if (initialized) { + setupPresentationMode(); + } + } + + onWindowClosed: { + // set invisible on close, to make it not re-appear unintended after switching PresentationMode + interactiveWindowVisible = false; + + if ((flags & Desktop.CLOSE_BUTTON_HIDES) !== Desktop.CLOSE_BUTTON_HIDES) { + selfDestruct(); + } + } + + Item { + id: contentHolder + anchors.fill: parent + } +} diff --git a/interface/resources/qml/LoginDialog/+android/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/+android/LinkAccountBody.qml index 38e65af4ca..bf7807c85d 100644 --- a/interface/resources/qml/LoginDialog/+android/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/+android/LinkAccountBody.qml @@ -135,6 +135,8 @@ Item { placeholderText: qsTr("Password") echoMode: TextInput.Password + + Keys.onReturnPressed: linkAccountBody.login() } } diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 6cbd1c4837..814778a4b1 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -177,6 +177,8 @@ Item { root.text = ""; root.isPassword = true; } + + Keys.onReturnPressed: linkAccountBody.login() } CheckBox { diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index 5eb99e0ece..bb30696e4c 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -164,6 +164,8 @@ Item { root.text = ""; root.isPassword = focus } + + Keys.onReturnPressed: signupBody.signup() } Row { diff --git a/interface/resources/qml/OverlayWindowTest.qml b/interface/resources/qml/OverlayWindowTest.qml index 7b82b2f705..9c1b993ba8 100644 --- a/interface/resources/qml/OverlayWindowTest.qml +++ b/interface/resources/qml/OverlayWindowTest.qml @@ -12,7 +12,7 @@ Rectangle { } Label { - text: OverlayWindowTestString + text: "OverlayWindowTestString" anchors.centerIn: parent } } diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 2406fa048d..bff13cea54 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -263,6 +263,12 @@ Item { } StatText { text: "GPU: " + root.gpuFrameTime.toFixed(1) + " ms" + } + StatText { + text: "GPU (Per pixel): " + root.gpuFrameTimePerPixel.toFixed(5) + " ns/pp" + } + StatText { + text: "GPU frame size: " + root.gpuFrameSize.x + " x " + root.gpuFrameSize.y } StatText { text: "Triangles: " + root.triangles + diff --git a/interface/resources/qml/controls-uit/+android/Button.qml b/interface/resources/qml/controls-uit/+android/Button.qml deleted file mode 100644 index 2f05b35685..0000000000 --- a/interface/resources/qml/controls-uit/+android/Button.qml +++ /dev/null @@ -1,125 +0,0 @@ -// -// Button.qml -// -// Created by David Rowe on 16 Feb 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 as Original -import QtQuick.Controls.Styles 1.4 -import TabletScriptingInterface 1.0 - -import "../styles-uit" - -Original.Button { - id: root; - - property int color: 0 - property int colorScheme: hifi.colorSchemes.light - property string buttonGlyph: ""; - - width: hifi.dimensions.buttonWidth - height: hifi.dimensions.controlLineHeight - - HifiConstants { id: hifi } - - onHoveredChanged: { - if (hovered) { - Tablet.playSound(TabletEnums.ButtonHover); - } - } - - onFocusChanged: { - if (focus) { - Tablet.playSound(TabletEnums.ButtonHover); - } - } - - onClicked: { - Tablet.playSound(TabletEnums.ButtonClick); - } - - style: ButtonStyle { - - background: Rectangle { - radius: hifi.buttons.radius - - border.width: (control.color === hifi.buttons.none || - (control.color === hifi.buttons.noneBorderless && control.hovered) || - (control.color === hifi.buttons.noneBorderlessWhite && control.hovered) || - (control.color === hifi.buttons.noneBorderlessGray && control.hovered)) ? 1 : 0; - border.color: control.color === hifi.buttons.noneBorderless ? hifi.colors.blueHighlight : - (control.color === hifi.buttons.noneBorderlessGray ? hifi.colors.baseGray : hifi.colors.white); - - gradient: Gradient { - GradientStop { - position: 0.2 - color: { - if (!control.enabled) { - hifi.buttons.disabledColorStart[control.colorScheme] - } else if (control.pressed) { - hifi.buttons.pressedColor[control.color] - } else if (control.hovered) { - hifi.buttons.hoveredColor[control.color] - } else if (!control.hovered && control.focus) { - hifi.buttons.focusedColor[control.color] - } else { - hifi.buttons.colorStart[control.color] - } - } - } - GradientStop { - position: 1.0 - color: { - if (!control.enabled) { - hifi.buttons.disabledColorFinish[control.colorScheme] - } else if (control.pressed) { - hifi.buttons.pressedColor[control.color] - } else if (control.hovered) { - hifi.buttons.hoveredColor[control.color] - } else if (!control.hovered && control.focus) { - hifi.buttons.focusedColor[control.color] - } else { - hifi.buttons.colorFinish[control.color] - } - } - } - } - } - - label: Item { - HiFiGlyphs { - id: buttonGlyph; - visible: root.buttonGlyph !== ""; - text: root.buttonGlyph === "" ? hifi.glyphs.question : root.buttonGlyph; - // Size - size: 34; - // Anchors - anchors.right: buttonText.left; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - // Style - color: enabled ? hifi.buttons.textColor[control.color] - : hifi.buttons.disabledTextColor[control.colorScheme]; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - } - RalewayBold { - id: buttonText; - anchors.centerIn: parent; - font.capitalization: Font.AllUppercase - color: enabled ? hifi.buttons.textColor[control.color] - : hifi.buttons.disabledTextColor[control.colorScheme] - size: hifi.fontSizes.buttonLabel - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - text: control.text - } - } - } -} diff --git a/interface/resources/qml/controls-uit/+android/Table.qml b/interface/resources/qml/controls-uit/+android/Table.qml deleted file mode 100644 index 3c1d0fcd3c..0000000000 --- a/interface/resources/qml/controls-uit/+android/Table.qml +++ /dev/null @@ -1,165 +0,0 @@ -// -// Table.qml -// -// Created by David Rowe on 18 Feb 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 QtQuick.Controls 2.2 as QQC2 - -import "../styles-uit" - -TableView { - id: tableView - - property int colorScheme: hifi.colorSchemes.light - readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light - property bool expandSelectedRow: false - property bool centerHeaderText: false - readonly property real headerSpacing: 3 //spacing between sort indicator and table header title - property var titlePaintedPos: [] // storing extra data position behind painted - // title text and sort indicatorin table's header - signal titlePaintedPosSignal(int column) //signal that extradata position gets changed - - model: ListModel { } - - Component.onCompleted: { - if (flickableItem !== null && flickableItem !== undefined) { - tableView.flickableItem.QQC2.ScrollBar.vertical = scrollbar - } - } - - QQC2.ScrollBar { - id: scrollbar - parent: tableView.flickableItem - policy: QQC2.ScrollBar.AsNeeded - orientation: Qt.Vertical - visible: size < 1.0 - topPadding: tableView.headerVisible ? hifi.dimensions.tableHeaderHeight + 1 : 1 - anchors.top: tableView.top - anchors.left: tableView.right - anchors.bottom: tableView.bottom - - background: Item { - implicitWidth: hifi.dimensions.scrollbarBackgroundWidth - Rectangle { - anchors { - fill: parent; - topMargin: tableView.headerVisible ? hifi.dimensions.tableHeaderHeight : 0 - } - color: isLightColorScheme ? hifi.colors.tableScrollBackgroundLight - : hifi.colors.tableScrollBackgroundDark - } - } - - contentItem: Item { - implicitWidth: hifi.dimensions.scrollbarHandleWidth - Rectangle { - anchors.fill: parent - radius: (width - 4)/2 - color: isLightColorScheme ? hifi.colors.tableScrollHandleLight : hifi.colors.tableScrollHandleDark - } - } - } - - headerVisible: false - headerDelegate: Rectangle { - height: hifi.dimensions.tableHeaderHeight - color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark - - - RalewayRegular { - id: titleText - x: centerHeaderText ? (parent.width - paintedWidth - - ((sortIndicatorVisible && - sortIndicatorColumn === styleData.column) ? - (titleSort.paintedWidth / 5 + tableView.headerSpacing) : 0)) / 2 : - hifi.dimensions.tablePadding - text: styleData.value - size: hifi.fontSizes.tableHeading - font.capitalization: Font.AllUppercase - color: hifi.colors.baseGrayHighlight - horizontalAlignment: (centerHeaderText ? Text.AlignHCenter : Text.AlignLeft) - anchors.verticalCenter: parent.verticalCenter - } - - //actual image of sort indicator in glyph font only 20% of real font size - //i.e. if the charachter size set to 60 pixels, actual image is 12 pixels - HiFiGlyphs { - id: titleSort - text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn - color: hifi.colors.darkGray - opacity: 0.6; - size: hifi.fontSizes.tableHeadingIcon - anchors.verticalCenter: titleText.verticalCenter - anchors.left: titleText.right - anchors.leftMargin: -(hifi.fontSizes.tableHeadingIcon / 2.5) + tableView.headerSpacing - visible: sortIndicatorVisible && sortIndicatorColumn === styleData.column - onXChanged: { - titlePaintedPos[styleData.column] = titleText.x + titleText.paintedWidth + - paintedWidth / 5 + tableView.headerSpacing*2 - titlePaintedPosSignal(styleData.column) - } - } - - Rectangle { - width: 1 - anchors { - left: parent.left - top: parent.top - topMargin: 1 - bottom: parent.bottom - bottomMargin: 2 - } - color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight - visible: styleData.column > 0 - } - - Rectangle { - height: 1 - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } - color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight - } - } - - // Use rectangle to draw border with rounded corners. - frameVisible: false - Rectangle { - color: "#00000000" - anchors { fill: parent; margins: -2 } - border.color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight - border.width: 2 - } - anchors.margins: 2 // Shrink TableView to lie within border. - - backgroundVisible: true - - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - verticalScrollBarPolicy: Qt.ScrollBarAlwaysOff - - style: TableViewStyle { - // Needed in order for rows to keep displaying rows after end of table entries. - backgroundColor: tableView.isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark - alternateBackgroundColor: tableView.isLightColorScheme ? hifi.colors.tableRowLightOdd : hifi.colors.tableRowDarkOdd - padding.top: headerVisible ? hifi.dimensions.tableHeaderHeight: 0 - } - - rowDelegate: Rectangle { - height: (styleData.selected && expandSelectedRow ? 1.8 : 1) * hifi.dimensions.tableRowHeight - color: styleData.selected - ? hifi.colors.primaryHighlight - : tableView.isLightColorScheme - ? (styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd) - : (styleData.alternate ? hifi.colors.tableRowDarkEven : hifi.colors.tableRowDarkOdd) - } -} diff --git a/interface/resources/qml/controls-uit/Button.qml b/interface/resources/qml/controls-uit/Button.qml index 8d0f513021..1509abdae3 100644 --- a/interface/resources/qml/controls-uit/Button.qml +++ b/interface/resources/qml/controls-uit/Button.qml @@ -19,6 +19,8 @@ Original.Button { property int color: 0 property int colorScheme: hifi.colorSchemes.light + property int fontSize: hifi.fontSizes.buttonLabel + property alias implicitTextWidth: buttonText.implicitWidth property string buttonGlyph: ""; width: hifi.dimensions.buttonWidth @@ -108,7 +110,7 @@ Original.Button { font.capitalization: Font.AllUppercase color: enabled ? hifi.buttons.textColor[control.color] : hifi.buttons.disabledTextColor[control.colorScheme] - size: hifi.fontSizes.buttonLabel + size: control.fontSize verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter text: control.text diff --git a/interface/resources/qml/controls-uit/RadioButton.qml b/interface/resources/qml/controls-uit/RadioButton.qml index ebfe1ff9a9..56324c55d7 100644 --- a/interface/resources/qml/controls-uit/RadioButton.qml +++ b/interface/resources/qml/controls-uit/RadioButton.qml @@ -25,10 +25,16 @@ Original.RadioButton { property int colorScheme: hifi.colorSchemes.light readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light - readonly property int boxSize: 14 - readonly property int boxRadius: 3 - readonly property int checkSize: 10 - readonly property int checkRadius: 2 + property real letterSpacing: 1 + property int fontSize: hifi.fontSizes.inputLabel + property int boxSize: defaultBoxSize + property real scaleFactor: boxSize / defaultBoxSize + + readonly property int defaultBoxSize: 14 + readonly property int boxRadius: 3 * scaleFactor + readonly property int checkSize: 10 * scaleFactor + readonly property int checkRadius: 2 * scaleFactor + readonly property int indicatorRadius: 7 * scaleFactor onClicked: { Tablet.playSound(TabletEnums.ButtonClick); @@ -44,7 +50,7 @@ Original.RadioButton { id: box width: boxSize height: boxSize - radius: 7 + radius: indicatorRadius x: radioButton.leftPadding y: parent.height / 2 - height / 2 gradient: Gradient { @@ -66,7 +72,7 @@ Original.RadioButton { id: check width: checkSize height: checkSize - radius: 7 + radius: indicatorRadius anchors.centerIn: parent color: "#00B4EF" border.width: 1 @@ -77,7 +83,8 @@ Original.RadioButton { contentItem: RalewaySemiBold { text: radioButton.text - size: hifi.fontSizes.inputLabel + size: radioButton.fontSize + font.letterSpacing: letterSpacing color: isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter diff --git a/interface/resources/qml/controls-uit/SpinBox.qml b/interface/resources/qml/controls-uit/SpinBox.qml index 52d5a2eb99..5a4ba70080 100644 --- a/interface/resources/qml/controls-uit/SpinBox.qml +++ b/interface/resources/qml/controls-uit/SpinBox.qml @@ -27,6 +27,9 @@ SpinBox { property string suffix: "" property string labelInside: "" property color colorLabelInside: hifi.colors.white + property color backgroundColor: isLightColorScheme + ? (spinBox.activeFocus ? hifi.colors.white : hifi.colors.lightGray) + : (spinBox.activeFocus ? hifi.colors.black : hifi.colors.baseGrayShadow) property real controlHeight: height + (spinBoxLabel.visible ? spinBoxLabel.height + spinBoxLabel.anchors.bottomMargin : 0) property int decimals: 2; property real factor: Math.pow(10, decimals) @@ -55,10 +58,14 @@ SpinBox { onValueModified: realValue = value/factor onValueChanged: realValue = value/factor + onRealValueChanged: { + var newValue = Math.round(realValue*factor); + if(value != newValue) { + value = newValue; + } + } stepSize: realStepSize*factor - value: Math.round(realValue*factor) - to : realTo*factor from : realFrom*factor @@ -69,9 +76,7 @@ SpinBox { y: spinBoxLabel.visible ? spinBoxLabel.height + spinBoxLabel.anchors.bottomMargin : 0 background: Rectangle { - color: isLightColorScheme - ? (spinBox.activeFocus ? hifi.colors.white : hifi.colors.lightGray) - : (spinBox.activeFocus ? hifi.colors.black : hifi.colors.baseGrayShadow) + color: backgroundColor border.color: spinBoxLabelInside.visible ? spinBoxLabelInside.color : hifi.colors.primaryHighlight border.width: spinBox.activeFocus ? spinBoxLabelInside.visible ? 2 : 1 : 0 } diff --git a/interface/resources/qml/controls/+android/WebEntityView.qml b/interface/resources/qml/controls/+android/WebEntityView.qml new file mode 100644 index 0000000000..848077cea0 --- /dev/null +++ b/interface/resources/qml/controls/+android/WebEntityView.qml @@ -0,0 +1,76 @@ +// +// Web3DOverlay.qml +// +// Created by Gabriel Calero & Cristian Duarte on Jun 22, 2018 +// 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 + +Item { + + property string url + RadialGradient { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: "#262626" } + GradientStop { position: 1.0; color: "#000000" } + } + } + + function shortUrl(url) { + var hostBegin = url.indexOf("://"); + if (hostBegin > -1) { + url = url.substring(hostBegin + 3); + } + + var portBegin = url.indexOf(":"); + if (portBegin > -1) { + url = url.substring(0, portBegin); + } + + var pathBegin = url.indexOf("/"); + if (pathBegin > -1) { + url = url.substring(0, pathBegin); + } + + if (url.length > 45) { + url = url.substring(0, 45); + } + + return url; + } + + Text { + id: urlText + text: shortUrl(url) + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors.fill: parent + anchors.rightMargin: 10 + anchors.leftMargin: 10 + font.family: "Cairo" + font.weight: Font.DemiBold + font.pointSize: 48 + fontSizeMode: Text.Fit + color: "#FFFFFF" + minimumPixelSize: 5 + } + + Image { + id: hand + source: "../../../icons/hand.svg" + width: 300 + height: 300 + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.bottomMargin: 100 + anchors.rightMargin: 100 + } + + +} diff --git a/interface/resources/qml/desktop/+android/Desktop.qml b/interface/resources/qml/desktop/+android/Desktop.qml deleted file mode 100644 index 6a68f63d0a..0000000000 --- a/interface/resources/qml/desktop/+android/Desktop.qml +++ /dev/null @@ -1,575 +0,0 @@ -// -// Desktop.qml -// -// Created by Bradley Austin Davis on 15 Apr 2015 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.7 -import QtQuick.Controls 1.4 - -import "../dialogs" -import "../js/Utils.js" as Utils - -// This is our primary 'desktop' object to which all VR dialogs and windows are childed. -FocusScope { - id: desktop - objectName: "desktop" - anchors.fill: parent - - readonly property int invalid_position: -9999; - property rect recommendedRect: Qt.rect(0,0,0,0); - property var expectedChildren; - property bool repositionLocked: true - property bool hmdHandMouseActive: false - - onRepositionLockedChanged: { - if (!repositionLocked) { - d.handleSizeChanged(); - } - - } - - onHeightChanged: d.handleSizeChanged(); - - onWidthChanged: d.handleSizeChanged(); - - // Controls and windows can trigger this signal to ensure the desktop becomes visible - // when they're opened. - signal showDesktop(); - - // This is for JS/QML communication, which is unused in the Desktop, - // but not having this here results in spurious warnings about a - // missing signal - signal sendToScript(var message); - - // Allows QML/JS to find the desktop through the parent chain - property bool desktopRoot: true - - // The VR version of the primary menu - property var rootMenu: Menu { - id: rootMenuId - objectName: "rootMenu" - - property var exclusionGroups: ({}); - property Component exclusiveGroupMaker: Component { - ExclusiveGroup { - } - } - - function addExclusionGroup(qmlAction, exclusionGroup) { - - var exclusionGroupId = exclusionGroup.toString(); - if(!exclusionGroups[exclusionGroupId]) { - exclusionGroups[exclusionGroupId] = exclusiveGroupMaker.createObject(rootMenuId); - } - - qmlAction.exclusiveGroup = exclusionGroups[exclusionGroupId] - } - } - - // FIXME: Alpha gradients display as fuschia under QtQuick 2.5 on OSX/AMD - // because shaders are 4.2, and do not include #version declarations. - property bool gradientsSupported: Qt.platform.os != "osx" && !~GL.vendor.indexOf("ATI") - - readonly property alias zLevels: zLevels - QtObject { - id: zLevels; - readonly property real normal: 1 // make windows always appear higher than QML overlays and other non-window controls. - readonly property real top: 2000 - readonly property real modal: 4000 - readonly property real menu: 8000 - } - - QtObject { - id: d - - function handleSizeChanged() { - if (desktop.repositionLocked) { - return; - } - var oldRecommendedRect = recommendedRect; - var newRecommendedRectJS = (typeof Controller === "undefined") ? Qt.rect(0,0,0,0) : Controller.getRecommendedHUDRect(); - var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y, - newRecommendedRectJS.width, - newRecommendedRectJS.height); - - var oldChildren = expectedChildren; - var newChildren = d.getRepositionChildren(); - if (oldRecommendedRect != Qt.rect(0,0,0,0) && oldRecommendedRect != Qt.rect(0,0,1,1) - && (oldRecommendedRect != newRecommendedRect - || oldChildren != newChildren) - ) { - expectedChildren = newChildren; - d.repositionAll(); - } - recommendedRect = newRecommendedRect; - } - - function findChild(item, name) { - for (var i = 0; i < item.children.length; ++i) { - if (item.children[i].objectName === name) { - return item.children[i]; - } - } - return null; - } - - function findParentMatching(item, predicate) { - while (item) { - if (predicate(item)) { - break; - } - item = item.parent; - } - 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; - } - - function isAlwaysOnTopWindow(window) { - return window.alwaysOnTop; - } - - function isModalWindow(window) { - return window.modality !== Qt.NonModal; - } - - function getTopLevelWindows(predicate) { - return findMatchingChildren(desktop, function(child) { - return (isTopLevelWindow(child) && (!predicate || predicate(child))); - }); - } - - function getDesktopWindow(item) { - return findParentMatching(item, isTopLevelWindow) - } - - function fixupZOrder(windows, basis, topWindow) { - windows.sort(function(a, b){ return a.z - b.z; }); - - if ((topWindow.z >= basis) && (windows[windows.length - 1] === topWindow)) { - return; - } - - var lastZ = -1; - var lastTargetZ = basis - 1; - for (var i = 0; i < windows.length; ++i) { - var window = windows[i]; - if (!window.visible) { - continue - } - - if (topWindow && (topWindow === window)) { - continue - } - - if (window.z > lastZ) { - lastZ = window.z; - ++lastTargetZ; - } - if (DebugQML) { - console.log("Assigning z order " + lastTargetZ + " to " + window) - } - - window.z = lastTargetZ; - } - if (topWindow) { - ++lastTargetZ; - if (DebugQML) { - console.log("Assigning z order " + lastTargetZ + " to " + topWindow) - } - topWindow.z = lastTargetZ; - } - - return lastTargetZ; - } - - function raiseWindow(targetWindow) { - var predicate; - var zBasis; - if (isModalWindow(targetWindow)) { - predicate = isModalWindow; - zBasis = zLevels.modal - } else if (isAlwaysOnTopWindow(targetWindow)) { - predicate = function(window) { - return (isAlwaysOnTopWindow(window) && !isModalWindow(window)); - } - zBasis = zLevels.top - } else { - predicate = function(window) { - return (!isAlwaysOnTopWindow(window) && !isModalWindow(window)); - } - zBasis = zLevels.normal - } - - var windows = getTopLevelWindows(predicate); - fixupZOrder(windows, zBasis, targetWindow); - } - - Component.onCompleted: { - //offscreenWindow.activeFocusItemChanged.connect(onWindowFocusChanged); - focusHack.start(); - } - - function onWindowFocusChanged() { - //console.log("Focus item is " + offscreenWindow.activeFocusItem); - - // FIXME this needs more testing before it can go into production - // and I already cant produce any way to have a modal dialog lose focus - // to a non-modal one. - /* - var focusedWindow = getDesktopWindow(offscreenWindow.activeFocusItem); - - if (isModalWindow(focusedWindow)) { - return; - } - - // new focused window is not modal... check if there are any modal windows - var windows = getTopLevelWindows(isModalWindow); - if (0 === windows.length) { - return; - } - - // There are modal windows present, force focus back to the top-most modal window - windows.sort(function(a, b){ return a.z - b.z; }); - windows[windows.length - 1].focus = true; - */ - -// var focusedItem = offscreenWindow.activeFocusItem ; -// if (DebugQML && focusedItem) { -// var rect = desktop.mapFromItem(focusedItem, 0, 0, focusedItem.width, focusedItem.height); -// focusDebugger.x = rect.x; -// focusDebugger.y = rect.y; -// focusDebugger.width = rect.width -// focusDebugger.height = rect.height -// } - } - - function getRepositionChildren(predicate) { - return findMatchingChildren(desktop, function(child) { - return (child.shouldReposition === true && (!predicate || predicate(child))); - }); - } - - function repositionAll() { - if (desktop.repositionLocked) { - return; - } - - var oldRecommendedRect = recommendedRect; - var oldRecommendedDimmensions = { x: oldRecommendedRect.width, y: oldRecommendedRect.height }; - var newRecommendedRect = Controller.getRecommendedHUDRect(); - var newRecommendedDimmensions = { x: newRecommendedRect.width, y: newRecommendedRect.height }; - var windows = d.getTopLevelWindows(); - for (var i = 0; i < windows.length; ++i) { - var targetWindow = windows[i]; - if (targetWindow.visible) { - repositionWindow(targetWindow, true, oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions); - } - } - - // also reposition the other children that aren't top level windows but want to be repositioned - var otherChildren = d.getRepositionChildren(); - for (var i = 0; i < otherChildren.length; ++i) { - var child = otherChildren[i]; - repositionWindow(child, true, oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions); - } - - } - } - - property bool pinned: false - property var hiddenChildren: [] - - function togglePinned() { - pinned = !pinned - } - - function isPointOnWindow(point) { - for (var i = 0; i < desktop.visibleChildren.length; i++) { - var child = desktop.visibleChildren[i]; - if (child.hasOwnProperty("modality")) { - var mappedPoint = mapToItem(child, point.x, point.y); - if (child.hasOwnProperty("frame")) { - var outLine = child.frame.children[2]; - var framePoint = outLine.mapFromGlobal(point.x, point.y); - if (outLine.contains(framePoint)) { - return true; - } - } - - if (child.contains(mappedPoint)) { - return true; - } - } - } - return false; - } - - 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) { - d.raiseWindow(desktop); - desktop.focus = true; - desktop.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) { - console.warn("Could not find top level window for " + item); - return; - } - - // Fix up the Z-order (takes into account if this is a modal window) - d.raiseWindow(targetWindow); - var setFocus = true; - if (!d.isModalWindow(targetWindow)) { - var modalWindows = d.getTopLevelWindows(d.isModalWindow); - if (modalWindows.length) { - setFocus = false; - } - } - - if (setFocus) { - targetWindow.focus = true; - } - - showDesktop(); - } - - function ensureTitleBarVisible(targetWindow) { - // Reposition window to ensure that title bar is vertically inside window. - if (targetWindow.frame && targetWindow.frame.decoration) { - var topMargin = -targetWindow.frame.decoration.anchors.topMargin; // Frame's topMargin is a negative value. - targetWindow.y = Math.max(targetWindow.y, topMargin); - } - } - - function centerOnVisible(item) { - var targetWindow = d.getDesktopWindow(item); - if (!targetWindow) { - console.warn("Could not find top level window for " + item); - return; - } - - if (typeof Controller === "undefined") { - console.warn("Controller not yet available... can't center"); - return; - } - - var newRecommendedRectJS = (typeof Controller === "undefined") ? Qt.rect(0,0,0,0) : Controller.getRecommendedHUDRect(); - var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y, - newRecommendedRectJS.width, - newRecommendedRectJS.height); - var newRecommendedDimmensions = { x: newRecommendedRect.width, y: newRecommendedRect.height }; - var newX = newRecommendedRect.x + ((newRecommendedRect.width - targetWindow.width) / 2); - var newY = newRecommendedRect.y + ((newRecommendedRect.height - targetWindow.height) / 2); - targetWindow.x = newX; - targetWindow.y = newY; - - ensureTitleBarVisible(targetWindow); - - // If we've noticed that our recommended desktop rect has changed, record that change here. - if (recommendedRect != newRecommendedRect) { - recommendedRect = newRecommendedRect; - } - } - - function repositionOnVisible(item) { - var targetWindow = d.getDesktopWindow(item); - if (!targetWindow) { - console.warn("Could not find top level window for " + item); - return; - } - - if (typeof Controller === "undefined") { - console.warn("Controller not yet available... can't reposition targetWindow:" + targetWindow); - return; - } - - var oldRecommendedRect = recommendedRect; - var oldRecommendedDimmensions = { x: oldRecommendedRect.width, y: oldRecommendedRect.height }; - var newRecommendedRect = Controller.getRecommendedHUDRect(); - var newRecommendedDimmensions = { x: newRecommendedRect.width, y: newRecommendedRect.height }; - repositionWindow(targetWindow, false, oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions); - } - - function repositionWindow(targetWindow, forceReposition, - oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions) { - - if (desktop.width === 0 || desktop.height === 0) { - return; - } - - if (!targetWindow) { - console.warn("Could not find top level window for " + item); - return; - } - - var recommended = Controller.getRecommendedHUDRect(); - var maxX = recommended.x + recommended.width; - var maxY = recommended.y + recommended.height; - var newPosition = Qt.vector2d(targetWindow.x, targetWindow.y); - - // if we asked to force reposition, or if the window is completely outside of the recommended rectangle, reposition it - if (forceReposition || (targetWindow.x > maxX || (targetWindow.x + targetWindow.width) < recommended.x) || - (targetWindow.y > maxY || (targetWindow.y + targetWindow.height) < recommended.y)) { - newPosition.x = -1 - newPosition.y = -1 - } - - if (newPosition.x === -1 && newPosition.y === -1) { - var originRelativeX = (targetWindow.x - oldRecommendedRect.x); - var originRelativeY = (targetWindow.y - oldRecommendedRect.y); - if (isNaN(originRelativeX)) { - originRelativeX = 0; - } - if (isNaN(originRelativeY)) { - originRelativeY = 0; - } - var fractionX = Utils.clamp(originRelativeX / oldRecommendedDimmensions.x, 0, 1); - var fractionY = Utils.clamp(originRelativeY / oldRecommendedDimmensions.y, 0, 1); - var newX = (fractionX * newRecommendedDimmensions.x) + newRecommendedRect.x; - var newY = (fractionY * newRecommendedDimmensions.y) + newRecommendedRect.y; - newPosition = Qt.vector2d(newX, newY); - } - targetWindow.x = newPosition.x; - targetWindow.y = newPosition.y; - - ensureTitleBarVisible(targetWindow); - } - - Component { id: messageDialogBuilder; MessageDialog { } } - function messageBox(properties) { - return messageDialogBuilder.createObject(desktop, properties); - } - - Component { id: inputDialogBuilder; QueryDialog { } } - function inputDialog(properties) { - return inputDialogBuilder.createObject(desktop, properties); - } - - Component { id: customInputDialogBuilder; CustomQueryDialog { } } - function customInputDialog(properties) { - return customInputDialogBuilder.createObject(desktop, properties); - } - - Component { id: fileDialogBuilder; FileDialog { } } - function fileDialog(properties) { - return fileDialogBuilder.createObject(desktop, properties); - } - - Component { id: assetDialogBuilder; AssetDialog { } } - function assetDialog(properties) { - return assetDialogBuilder.createObject(desktop, properties); - } - - function unfocusWindows() { - // First find the active focus item, and unfocus it, all the way - // up the parent chain to the window - var currentFocus = offscreenWindow.activeFocusItem; - var targetWindow = d.getDesktopWindow(currentFocus); - while (currentFocus) { - if (currentFocus === targetWindow) { - break; - } - currentFocus.focus = false; - currentFocus = currentFocus.parent; - } - - // Unfocus all windows - var windows = d.getTopLevelWindows(); - for (var i = 0; i < windows.length; ++i) { - windows[i].focus = false; - } - - // For the desktop to have active focus - desktop.focus = true; - desktop.forceActiveFocus(); - } - - function openBrowserWindow(request, profile) { - var component = Qt.createComponent("../Browser.qml"); - var newWindow = component.createObject(desktop); - newWindow.webView.profile = profile; - request.openIn(newWindow.webView); - } - - FocusHack { id: focusHack; } - - Rectangle { - id: focusDebugger; - objectName: "focusDebugger" - z: 9999; visible: false; color: "red" - ColorAnimation on color { from: "#7fffff00"; to: "#7f0000ff"; duration: 1000; loops: 9999 } - } - - Action { - text: "Toggle Focus Debugger" - shortcut: "Ctrl+Shift+F" - enabled: DebugQML - onTriggered: focusDebugger.visible = !focusDebugger.visible - } - -} diff --git a/interface/resources/qml/dialogs/+android/CustomQueryDialog.qml b/interface/resources/qml/dialogs/+android/CustomQueryDialog.qml deleted file mode 100644 index aadd7c88ae..0000000000 --- a/interface/resources/qml/dialogs/+android/CustomQueryDialog.qml +++ /dev/null @@ -1,338 +0,0 @@ -// -// CustomQueryDialog.qml -// -// Created by Zander Otavka on 7/14/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 -// - -import QtQuick 2.7; -import QtQuick.Dialogs 1.2 as OriginalDialogs; -import QtQuick.Controls 1.4; - -import "../controls-uit"; -import "../styles-uit"; -import "../windows"; - -ModalWindow { - id: root; - HifiConstants { id: hifi; } - implicitWidth: 640; - implicitHeight: 320; - visible: true; - keyboardOverride: true // Disable ModalWindow's keyboard. - - signal selected(var result); - signal canceled(); - - property int icon: hifi.icons.none; - property string iconText: ""; - property int iconSize: 35; - onIconChanged: updateIcon(); - - property var textInput; - property var comboBox; - property var checkBox; - onTextInputChanged: { - if (textInput && textInput.text !== undefined) { - textField.text = textInput.text; - } - } - onComboBoxChanged: { - if (comboBox && comboBox.index !== undefined) { - comboBoxField.currentIndex = comboBox.index; - } - } - onCheckBoxChanged: { - if (checkBox && checkBox.checked !== undefined) { - checkBoxField.checked = checkBox.checked; - } - } - - property bool keyboardEnabled: false - property bool keyboardRaised: false - property bool punctuationMode: false - onKeyboardRaisedChanged: d.resize(); - - property var warning: ""; - property var result; - - property var implicitCheckState: null; - - property int titleWidth: 0; - onTitleWidthChanged: d.resize(); - - function updateIcon() { - if (!root) { - return; - } - iconText = hifi.glyphForIcon(root.icon); - } - - function updateCheckbox() { - if (checkBox.disableForItems) { - var currentItemInDisableList = false; - for (var i in checkBox.disableForItems) { - if (comboBoxField.currentIndex === checkBox.disableForItems[i]) { - currentItemInDisableList = true; - break; - } - } - - if (currentItemInDisableList) { - checkBoxField.enabled = false; - if (checkBox.checkStateOnDisable !== null && checkBox.checkStateOnDisable !== undefined) { - root.implicitCheckState = checkBoxField.checked; - checkBoxField.checked = checkBox.checkStateOnDisable; - } - root.warning = checkBox.warningOnDisable; - } else { - checkBoxField.enabled = true; - if (root.implicitCheckState !== null) { - checkBoxField.checked = root.implicitCheckState; - root.implicitCheckState = null; - } - root.warning = ""; - } - } - } - - Item { - clip: true; - width: pane.width; - height: pane.height; - anchors.margins: 0; - - QtObject { - id: d; - readonly property int minWidth: 480 - readonly property int maxWdith: 1280 - readonly property int minHeight: 120 - readonly property int maxHeight: 720 - - function resize() { - var targetWidth = Math.max(titleWidth, pane.width); - var targetHeight = (textField.visible ? textField.controlHeight + hifi.dimensions.contentSpacing.y : 0) + - (extraInputs.visible ? extraInputs.height + hifi.dimensions.contentSpacing.y : 0) + - (buttons.height + 3 * hifi.dimensions.contentSpacing.y) + - ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + hifi.dimensions.contentSpacing.y) : 0); - - root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth); - root.height = (targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? - d.maxHeight : targetHeight); - } - } - - Item { - anchors { - top: parent.top; - bottom: extraInputs.visible ? extraInputs.top : buttons.top; - left: parent.left; - right: parent.right; - margins: 0; - } - - // FIXME make a text field type that can be bound to a history for autocompletion - TextField { - id: textField; - label: root.textInput.label; - focus: root.textInput ? true : false; - visible: root.textInput ? true : false; - anchors { - left: parent.left; - right: parent.right; - bottom: keyboard.top; - bottomMargin: hifi.dimensions.contentSpacing.y; - } - } - - Keyboard { - id: keyboard - raised: keyboardEnabled && keyboardRaised - numeric: punctuationMode - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - bottomMargin: raised ? hifi.dimensions.contentSpacing.y : 0 - } - } - } - - Item { - id: extraInputs; - visible: Boolean(root.checkBox || root.comboBox); - anchors { - left: parent.left; - right: parent.right; - bottom: buttons.top; - bottomMargin: hifi.dimensions.contentSpacing.y; - } - height: comboBoxField.controlHeight; - onHeightChanged: d.resize(); - onWidthChanged: d.resize(); - - CheckBox { - id: checkBoxField; - text: root.checkBox.label; - focus: Boolean(root.checkBox); - visible: Boolean(root.checkBox); - anchors { - left: parent.left; - bottom: parent.bottom; - leftMargin: 6; // Magic number to align with warning icon - bottomMargin: 6; - } - } - - ComboBox { - id: comboBoxField; - label: root.comboBox.label; - focus: Boolean(root.comboBox); - visible: Boolean(root.comboBox); - Binding on x { - when: comboBoxField.visible - value: !checkBoxField.visible ? buttons.x : acceptButton.x - } - - Binding on width { - when: comboBoxField.visible - value: !checkBoxField.visible ? buttons.width : buttons.width - acceptButton.x - } - anchors { - right: parent.right; - bottom: parent.bottom; - } - model: root.comboBox ? root.comboBox.items : []; - onAccepted: { - updateCheckbox(); - focus = true; - } - } - } - - Row { - id: buttons; - focus: true; - spacing: hifi.dimensions.contentSpacing.x; - layoutDirection: Qt.RightToLeft; - onHeightChanged: d.resize(); - onWidthChanged: { - d.resize(); - resizeWarningText(); - } - - anchors { - bottom: parent.bottom; - left: parent.left; - right: parent.right; - bottomMargin: hifi.dimensions.contentSpacing.y; - } - - function resizeWarningText() { - var rowWidth = buttons.width; - var buttonsWidth = acceptButton.width + cancelButton.width + hifi.dimensions.contentSpacing.x * 2; - var warningIconWidth = warningIcon.width + hifi.dimensions.contentSpacing.x; - warningText.width = rowWidth - buttonsWidth - warningIconWidth; - } - - Button { - id: cancelButton; - action: cancelAction; - } - - Button { - id: acceptButton; - action: acceptAction; - } - - Text { - id: warningText; - visible: Boolean(root.warning); - text: root.warning; - wrapMode: Text.WordWrap; - font.italic: true; - maximumLineCount: 3; - } - - HiFiGlyphs { - id: warningIcon; - visible: Boolean(root.warning); - text: hifi.glyphs.alert; - size: hifi.dimensions.controlLineHeight; - width: 20 // Line up with checkbox. - } - } - - Action { - id: cancelAction; - text: qsTr("Cancel"); - shortcut: "Esc"; - onTriggered: { - root.result = null; - root.canceled(); - // FIXME we are leaking memory to avoid a crash - // root.destroy(); - - root.disableFade = true - visible = false; - } - } - - Action { - id: acceptAction; - text: qsTr("Add"); - shortcut: "Return" - onTriggered: { - var result = {}; - if (textInput) { - result.textInput = textField.text; - } - if (comboBox) { - result.comboBox = comboBoxField.currentIndex; - result.comboBoxText = comboBoxField.currentText; - } - if (checkBox) { - result.checkBox = checkBoxField.enabled ? checkBoxField.checked : null; - } - root.result = JSON.stringify(result); - root.selected(root.result); - // FIXME we are leaking memory to avoid a crash - // root.destroy(); - - root.disableFade = true - visible = false; - } - } - } - - Keys.onPressed: { - if (!visible) { - return; - } - - switch (event.key) { - case Qt.Key_Escape: - case Qt.Key_Back: - cancelAction.trigger(); - event.accepted = true; - break; - - case Qt.Key_Return: - case Qt.Key_Enter: - acceptAction.trigger(); - event.accepted = true; - break; - } - } - - Component.onCompleted: { - keyboardEnabled = HMD.active; - updateIcon(); - updateCheckbox(); - d.resize(); - textField.forceActiveFocus(); - } -} diff --git a/interface/resources/qml/dialogs/+android/FileDialog.qml b/interface/resources/qml/dialogs/+android/FileDialog.qml deleted file mode 100644 index be6524d2b8..0000000000 --- a/interface/resources/qml/dialogs/+android/FileDialog.qml +++ /dev/null @@ -1,840 +0,0 @@ -// -// FileDialog.qml -// -// Created by Bradley Austin Davis on 14 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.7 -import Qt.labs.folderlistmodel 2.1 -import Qt.labs.settings 1.0 -import QtQuick.Dialogs 1.2 as OriginalDialogs -import QtQuick.Controls 1.4 - -import ".." -import "../controls-uit" -import "../styles-uit" -import "../windows" - -import "fileDialog" - -//FIXME implement shortcuts for favorite location -ModalWindow { - id: root - resizable: true - implicitWidth: 480 - implicitHeight: 360 + (fileDialogItem.keyboardEnabled && fileDialogItem.keyboardRaised ? keyboard.raisedHeight + hifi.dimensions.contentSpacing.y : 0) - - minSize: Qt.vector2d(360, 240) - draggable: true - - HifiConstants { id: hifi } - - property var filesModel: ListModel { } - - Settings { - category: "FileDialog" - property alias width: root.width - property alias height: root.height - property alias x: root.x - property alias y: root.y - } - - - // Set from OffscreenUi::getOpenFile() - property alias caption: root.title; - // Set from OffscreenUi::getOpenFile() - property alias dir: fileTableModel.folder; - // Set from OffscreenUi::getOpenFile() - property alias filter: selectionType.filtersString; - // Set from OffscreenUi::getOpenFile() - property int options; // <-- FIXME unused - - property string iconText: root.title !== "" ? hifi.glyphs.scriptUpload : "" - property int iconSize: 40 - - property bool selectDirectory: false; - property bool showHidden: true; - // FIXME implement - property bool multiSelect: false; - property bool saveDialog: false; - property var helper: fileDialogHelper - property alias model: fileTableView.model - property var drives: helper.drives() - - property int titleWidth: 0 - - signal selectedFile(var file); - signal canceled(); - signal selected(int button); - function click(button) { - clickedButton = button; - selected(button); - destroy(); - } - - property int clickedButton: OriginalDialogs.StandardButton.NoButton; - - Component.onCompleted: { - console.log("Helper " + helper + " drives " + drives); - - fileDialogItem.keyboardEnabled = HMD.active; - - // HACK: The following lines force the model to initialize properly such that the go-up button - // works properly from the initial screen. - var initialFolder = folderListModel.folder; - fileTableModel.folder = helper.pathToUrl(drives[0]); - fileTableModel.folder = initialFolder; - - iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; - - // Clear selection when click on external frame. - frameClicked.connect(function() { d.clearSelection(); }); - - if (selectDirectory) { - currentSelection.text = d.capitalizeDrive(helper.urlToPath(initialFolder)); - d.currentSelectionIsFolder = true; - d.currentSelectionUrl = initialFolder; - } - - helper.contentsChanged.connect(function() { - if (folderListModel) { - // Make folderListModel refresh. - var save = folderListModel.folder; - folderListModel.folder = ""; - folderListModel.folder = save; - } - }); - - focusTimer.start(); - } - - Timer { - id: focusTimer - interval: 10 - running: false - repeat: false - onTriggered: { - fileTableView.contentItem.forceActiveFocus(); - } - } - - Item { - id: fileDialogItem - clip: true - width: pane.width - height: pane.height - anchors.margins: 0 - - property bool keyboardEnabled: false - property bool keyboardRaised: false - property bool punctuationMode: false - - MouseArea { - // Clear selection when click on internal unused area. - anchors.fill: parent - drag.target: root - onClicked: { - d.clearSelection(); - // Defocus text field so that the keyboard gets hidden. - // Clicking also breaks keyboard navigation apart from backtabbing to cancel - frame.forceActiveFocus(); - } - } - - Row { - id: navControls - anchors { - top: parent.top - topMargin: hifi.dimensions.contentMargin.y - left: parent.left - } - spacing: hifi.dimensions.contentSpacing.x - - GlyphButton { - id: upButton - glyph: hifi.glyphs.levelUp - width: height - size: 30 - enabled: fileTableModel.parentFolder && fileTableModel.parentFolder !== "" - onClicked: d.navigateUp(); - Keys.onReturnPressed: { d.navigateUp(); } - KeyNavigation.tab: homeButton - KeyNavigation.backtab: upButton - KeyNavigation.left: upButton - KeyNavigation.right: homeButton - } - - GlyphButton { - id: homeButton - property var destination: helper.home(); - glyph: hifi.glyphs.home - size: 28 - width: height - enabled: d.homeDestination ? true : false - onClicked: d.navigateHome(); - Keys.onReturnPressed: { d.navigateHome(); } - KeyNavigation.tab: fileTableView.contentItem - KeyNavigation.backtab: upButton - KeyNavigation.left: upButton - } - } - - ComboBox { - id: pathSelector - anchors { - top: parent.top - topMargin: hifi.dimensions.contentMargin.y - left: navControls.right - leftMargin: hifi.dimensions.contentSpacing.x - right: parent.right - } - - property var lastValidFolder: helper.urlToPath(fileTableModel.folder) - - function calculatePathChoices(folder) { - var folders = folder.split("/"), - choices = [], - i, length; - - if (folders[folders.length - 1] === "") { - folders.pop(); - } - - choices.push(folders[0]); - - for (i = 1, length = folders.length; i < length; i++) { - choices.push(choices[i - 1] + "/" + folders[i]); - } - - if (folders[0] === "") { - // Special handling for OSX root dir. - choices[0] = "/"; - } - - choices.reverse(); - - if (drives && drives.length > 1) { - choices.push("This PC"); - } - - if (choices.length > 0) { - pathSelector.model = choices; - } - } - - onLastValidFolderChanged: { - var folder = d.capitalizeDrive(lastValidFolder); - calculatePathChoices(folder); - } - - onCurrentTextChanged: { - var folder = currentText; - - if (/^[a-zA-z]:$/.test(folder)) { - folder = "file:///" + folder + "/"; - } else if (folder === "This PC") { - folder = "file:///"; - } else { - folder = helper.pathToUrl(folder); - } - - if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) { - if (root.selectDirectory) { - currentSelection.text = currentText !== "This PC" ? currentText : ""; - d.currentSelectionUrl = helper.pathToUrl(currentText); - } - fileTableModel.folder = folder; - } - } - - KeyNavigation.up: fileTableView.contentItem - KeyNavigation.down: fileTableView.contentItem - KeyNavigation.tab: fileTableView.contentItem - KeyNavigation.backtab: fileTableView.contentItem - KeyNavigation.left: fileTableView.contentItem - KeyNavigation.right: fileTableView.contentItem - } - - QtObject { - id: d - property var currentSelectionUrl; - readonly property string currentSelectionPath: helper.urlToPath(currentSelectionUrl); - property bool currentSelectionIsFolder; - property var backStack: [] - property var tableViewConnection: Connections { target: fileTableView; onCurrentRowChanged: d.update(); } - property var modelConnection: Connections { target: fileTableModel; onFolderChanged: d.update(); } - property var homeDestination: helper.home(); - - function capitalizeDrive(path) { - // Consistently capitalize drive letter for Windows. - if (/[a-zA-Z]:/.test(path)) { - return path.charAt(0).toUpperCase() + path.slice(1); - } - return path; - } - - function update() { - var row = fileTableView.currentRow; - - if (row === -1) { - if (!root.selectDirectory) { - currentSelection.text = ""; - currentSelectionIsFolder = false; - } - return; - } - - currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath); - currentSelectionIsFolder = fileTableView.model !== filesModel ? - fileTableView.model.isFolder(row) : - fileTableModel.isFolder(row); - if (root.selectDirectory || !currentSelectionIsFolder) { - currentSelection.text = capitalizeDrive(helper.urlToPath(currentSelectionUrl)); - } else { - currentSelection.text = ""; - } - } - - function navigateUp() { - if (fileTableModel.parentFolder && fileTableModel.parentFolder !== "") { - fileTableModel.folder = fileTableModel.parentFolder; - return true; - } - } - - function navigateHome() { - fileTableModel.folder = homeDestination; - return true; - } - - function clearSelection() { - fileTableView.selection.clear(); - fileTableView.currentRow = -1; - update(); - } - } - - FolderListModel { - id: folderListModel - nameFilters: selectionType.currentFilter - showDirsFirst: true - showDotAndDotDot: false - showFiles: !root.selectDirectory - showHidden: root.showHidden - Component.onCompleted: { - showFiles = !root.selectDirectory - showHidden = root.showHidden - } - - onFolderChanged: { - d.clearSelection(); - fileTableModel.update(); // Update once the data from the folder change is available. - } - - function getItem(index, field) { - return get(index, field); - } - } - - ListModel { - // Emulates FolderListModel but contains drive data. - id: driveListModel - - property int count: 1 - - Component.onCompleted: initialize(); - - function initialize() { - var drive, - i; - - count = drives.length; - - for (i = 0; i < count; i++) { - drive = drives[i].slice(0, -1); // Remove trailing "/". - append({ - fileName: drive, - fileModified: new Date(0), - fileSize: 0, - filePath: drive + "/", - fileIsDir: true, - fileNameSort: drive.toLowerCase() - }); - } - } - - function getItem(index, field) { - return get(index)[field]; - } - } - - Component { - id: filesModelBuilder - ListModel { } - } - - QtObject { - id: fileTableModel - - // FolderListModel has a couple of problems: - // 1) Files and directories sort case-sensitively: https://bugreports.qt.io/browse/QTBUG-48757 - // 2) Cannot browse up to the "computer" level to view Windows drives: https://bugreports.qt.io/browse/QTBUG-42901 - // - // To solve these problems an intermediary ListModel is used that implements proper sorting and can be populated with - // drive information when viewing at the computer level. - - property var folder - property int sortOrder: Qt.AscendingOrder - property int sortColumn: 0 - property var model: folderListModel - property string parentFolder: calculateParentFolder(); - - readonly property string rootFolder: "file:///" - - function calculateParentFolder() { - if (model === folderListModel) { - if (folderListModel.parentFolder.toString() === "" && driveListModel.count > 1) { - return rootFolder; - } else { - return folderListModel.parentFolder; - } - } else { - return ""; - } - } - - onFolderChanged: { - if (folder === rootFolder) { - model = driveListModel; - helper.monitorDirectory(""); - update(); - } else { - var needsUpdate = model === driveListModel && folder === folderListModel.folder; - - model = folderListModel; - folderListModel.folder = folder; - helper.monitorDirectory(helper.urlToPath(folder)); - - if (needsUpdate) { - update(); - } - } - } - - function isFolder(row) { - if (row === -1) { - return false; - } - return filesModel.get(row).fileIsDir; - } - - function get(row) { - return filesModel.get(row) - } - - function update() { - var dataFields = ["fileName", "fileModified", "fileSize"], - sortFields = ["fileNameSort", "fileModified", "fileSize"], - dataField = dataFields[sortColumn], - sortField = sortFields[sortColumn], - sortValue, - fileName, - fileIsDir, - comparisonFunction, - lower, - middle, - upper, - rows = 0, - i; - - filesModel = filesModelBuilder.createObject(root); - - comparisonFunction = sortOrder === Qt.AscendingOrder - ? function(a, b) { return a < b; } - : function(a, b) { return a > b; } - - for (i = 0; i < model.count; i++) { - fileName = model.getItem(i, "fileName"); - fileIsDir = model.getItem(i, "fileIsDir"); - - sortValue = model.getItem(i, dataField); - if (dataField === "fileName") { - // Directories first by prefixing a "*". - // Case-insensitive. - sortValue = (fileIsDir ? "*" : "") + sortValue.toLowerCase(); - } - - lower = 0; - upper = rows; - while (lower < upper) { - middle = Math.floor((lower + upper) / 2); - var lessThan; - if (comparisonFunction(sortValue, filesModel.get(middle)[sortField])) { - lessThan = true; - upper = middle; - } else { - lessThan = false; - lower = middle + 1; - } - } - - filesModel.insert(lower, { - fileName: fileName, - fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), - fileSize: model.getItem(i, "fileSize"), - filePath: model.getItem(i, "filePath"), - fileIsDir: fileIsDir, - fileNameSort: (fileIsDir ? "*" : "") + fileName.toLowerCase() - }); - - rows++; - } - } - } - - Table { - id: fileTableView - colorScheme: hifi.colorSchemes.light - anchors { - top: navControls.bottom - topMargin: hifi.dimensions.contentSpacing.y - left: parent.left - right: parent.right - bottom: currentSelection.top - bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height - } - headerVisible: !selectDirectory - onDoubleClicked: navigateToRow(row); - Keys.onReturnPressed: navigateToCurrentRow(); - Keys.onEnterPressed: navigateToCurrentRow(); - - sortIndicatorColumn: 0 - sortIndicatorOrder: Qt.AscendingOrder - sortIndicatorVisible: true - - model: filesModel - - function updateSort() { - fileTableModel.sortOrder = sortIndicatorOrder; - fileTableModel.sortColumn = sortIndicatorColumn; - fileTableModel.update(); - } - - onSortIndicatorColumnChanged: { updateSort(); } - - onSortIndicatorOrderChanged: { updateSort(); } - - itemDelegate: Item { - clip: true - - FiraSansSemiBold { - text: getText(); - elide: styleData.elideMode - anchors { - left: parent.left - leftMargin: hifi.dimensions.tablePadding - right: parent.right - rightMargin: hifi.dimensions.tablePadding - verticalCenter: parent.verticalCenter - } - size: hifi.fontSizes.tableText - color: hifi.colors.baseGrayHighlight - font.family: (styleData.row !== -1 && fileTableView.model.get(styleData.row).fileIsDir) - ? "Fira Sans SemiBold" : "Fira Sans" - - function getText() { - if (styleData.row === -1) { - return styleData.value; - } - - switch (styleData.column) { - case 1: return fileTableView.model.get(styleData.row).fileIsDir ? "" : styleData.value; - case 2: return fileTableView.model.get(styleData.row).fileIsDir ? "" : formatSize(styleData.value); - default: return styleData.value; - } - } - function formatSize(size) { - var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ]; - var suffixIndex = 0 - while ((size / 1024.0) > 1.1) { - size /= 1024.0; - ++suffixIndex; - } - - size = Math.round(size*1000)/1000; - size = size.toLocaleString() - - return size + " " + suffixes[suffixIndex]; - } - } - } - - TableViewColumn { - id: fileNameColumn - role: "fileName" - title: "Name" - width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width - movable: false - resizable: true - } - TableViewColumn { - id: fileModifiedColumn - role: "fileModified" - title: "Date" - width: 0.3 * fileTableView.width - movable: false - resizable: true - visible: !selectDirectory - } - TableViewColumn { - role: "fileSize" - title: "Size" - width: fileTableView.width - fileNameColumn.width - fileModifiedColumn.width - movable: false - resizable: true - visible: !selectDirectory - } - - function navigateToRow(row) { - currentRow = row; - navigateToCurrentRow(); - } - - function navigateToCurrentRow() { - var currentModel = fileTableView.model !== filesModel ? fileTableView.model : fileTableModel - var row = fileTableView.currentRow - var isFolder = currentModel.isFolder(row); - var file = currentModel.get(row).filePath; - if (isFolder) { - currentModel.folder = helper.pathToUrl(file); - } else { - okAction.trigger(); - } - } - - property string prefix: "" - - function addToPrefix(event) { - if (!event.text || event.text === "") { - return false; - } - var newPrefix = prefix + event.text.toLowerCase(); - var matchedIndex = -1; - for (var i = 0; i < model.count; ++i) { - var name = model !== filesModel ? model.get(i).fileName.toLowerCase() : - filesModel.get(i).fileName.toLowerCase(); - if (0 === name.indexOf(newPrefix)) { - matchedIndex = i; - break; - } - } - - if (matchedIndex !== -1) { - fileTableView.selection.clear(); - fileTableView.selection.select(matchedIndex); - fileTableView.currentRow = matchedIndex; - fileTableView.prefix = newPrefix; - } - prefixClearTimer.restart(); - return true; - } - - Timer { - id: prefixClearTimer - interval: 1000 - repeat: false - running: false - onTriggered: fileTableView.prefix = ""; - } - - Keys.onPressed: { - switch (event.key) { - case Qt.Key_Backspace: - case Qt.Key_Tab: - case Qt.Key_Backtab: - event.accepted = false; - break; - case Qt.Key_Escape: - event.accepted = true; - root.click(OriginalDialogs.StandardButton.Cancel); - break; - default: - if (addToPrefix(event)) { - event.accepted = true - } else { - event.accepted = false; - } - break; - } - } - - KeyNavigation.tab: root.saveDialog ? currentSelection : openButton - } - - TextField { - id: currentSelection - label: selectDirectory ? "Directory:" : "File name:" - anchors { - left: parent.left - right: selectionType.visible ? selectionType.left: parent.right - rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0 - bottom: keyboard.top - bottomMargin: hifi.dimensions.contentSpacing.y - } - readOnly: !root.saveDialog - activeFocusOnTab: !readOnly - onActiveFocusChanged: if (activeFocus) { selectAll(); } - onAccepted: okAction.trigger(); - KeyNavigation.up: fileTableView.contentItem - KeyNavigation.down: openButton - KeyNavigation.tab: openButton - KeyNavigation.backtab: fileTableView.contentItem - } - - FileTypeSelection { - id: selectionType - anchors { - top: currentSelection.top - left: buttonRow.left - right: parent.right - } - visible: !selectDirectory && filtersCount > 1 - } - - Keyboard { - id: keyboard - raised: parent.keyboardEnabled && parent.keyboardRaised - numeric: parent.punctuationMode - anchors { - left: parent.left - right: parent.right - bottom: buttonRow.top - bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0 - } - } - - Row { - id: buttonRow - anchors { - right: parent.right - bottom: parent.bottom - } - spacing: hifi.dimensions.contentSpacing.y - - Button { - id: openButton - color: hifi.buttons.blue - action: okAction - Keys.onReturnPressed: okAction.trigger() - KeyNavigation.right: cancelButton - KeyNavigation.up: root.saveDialog ? currentSelection : fileTableView.contentItem - KeyNavigation.tab: cancelButton - } - - Button { - id: cancelButton - action: cancelAction - Keys.onReturnPressed: { cancelAction.trigger() } - KeyNavigation.left: openButton - KeyNavigation.up: root.saveDialog ? currentSelection : fileTableView.contentItem - KeyNavigation.backtab: openButton - } - } - - Action { - id: okAction - text: currentSelection.text ? (root.selectDirectory && fileTableView.currentRow === -1 ? "Choose" : (root.saveDialog ? "Save" : "Open")) : "Open" - enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false - onTriggered: { - if (!root.selectDirectory && !d.currentSelectionIsFolder - || root.selectDirectory && fileTableView.currentRow === -1) { - okActionTimer.start(); - } else { - fileTableView.navigateToCurrentRow(); - } - } - } - - Timer { - id: okActionTimer - interval: 50 - running: false - repeat: false - onTriggered: { - if (!root.saveDialog) { - selectedFile(d.currentSelectionUrl); - root.destroy() - return; - } - - // Handle the ambiguity between different cases - // * typed name (with or without extension) - // * full path vs relative vs filename only - var selection = helper.saveHelper(currentSelection.text, root.dir, selectionType.currentFilter); - - if (!selection) { - desktop.messageBox({ icon: OriginalDialogs.StandardIcon.Warning, text: "Unable to parse selection" }) - return; - } - - if (helper.urlIsDir(selection)) { - root.dir = selection; - currentSelection.text = ""; - return; - } - - // Check if the file is a valid target - if (!helper.urlIsWritable(selection)) { - desktop.messageBox({ - icon: OriginalDialogs.StandardIcon.Warning, - text: "Unable to write to location " + selection - }) - return; - } - - if (helper.urlExists(selection)) { - var messageBox = desktop.messageBox({ - icon: OriginalDialogs.StandardIcon.Question, - buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, - text: "Do you wish to overwrite " + selection + "?", - }); - var result = messageBox.exec(); - if (OriginalDialogs.StandardButton.Yes !== result) { - return; - } - } - - console.log("Selecting " + selection) - selectedFile(selection); - root.destroy(); - } - } - - Action { - id: cancelAction - text: "Cancel" - onTriggered: { canceled(); root.shown = false; } - } - } - - Keys.onPressed: { - switch (event.key) { - case Qt.Key_Backspace: - event.accepted = d.navigateUp(); - break; - - case Qt.Key_Home: - event.accepted = d.navigateHome(); - break; - - case Qt.Key_Escape: - event.accepted = true; - root.click(OriginalDialogs.StandardButton.Cancel); - break; - } - } -} diff --git a/interface/resources/qml/dialogs/+android/QueryDialog.qml b/interface/resources/qml/dialogs/+android/QueryDialog.qml deleted file mode 100644 index aec6d8a286..0000000000 --- a/interface/resources/qml/dialogs/+android/QueryDialog.qml +++ /dev/null @@ -1,231 +0,0 @@ -// -// QueryDialog.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.7 -import QtQuick.Controls 1.4 - -import "../controls-uit" -import "../styles-uit" -import "../windows" - -ModalWindow { - id: root - HifiConstants { id: hifi } - implicitWidth: 640 - implicitHeight: 320 - visible: true - keyboardOverride: true // Disable ModalWindow's keyboard. - - signal selected(var result); - signal canceled(); - - property int icon: hifi.icons.none - property string iconText: "" - property int iconSize: 35 - onIconChanged: updateIcon(); - - property var items; - property string label - property var result; - property alias current: textResult.text - - // For text boxes - property alias placeholderText: textResult.placeholderText - - // For combo boxes - property bool editable: true; - - property int titleWidth: 0 - onTitleWidthChanged: d.resize(); - - property bool keyboardEnabled: false - property bool keyboardRaised: false - property bool punctuationMode: false - - onKeyboardRaisedChanged: d.resize(); - - function updateIcon() { - if (!root) { - return; - } - iconText = hifi.glyphForIcon(root.icon); - } - - Item { - id: modalWindowItem - clip: true - width: pane.width - height: pane.height - anchors.margins: 0 - - QtObject { - id: d - readonly property int minWidth: 480 - readonly property int maxWdith: 1280 - readonly property int minHeight: 120 - readonly property int maxHeight: 720 - - function resize() { - var targetWidth = Math.max(titleWidth, pane.width) - var targetHeight = (items ? comboBox.controlHeight : textResult.controlHeight) + 5 * hifi.dimensions.contentSpacing.y + buttons.height - root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth); - root.height = ((targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + 2 * hifi.dimensions.contentSpacing.y) : 0) - } - } - - Item { - anchors { - top: parent.top - bottom: keyboard.top; - left: parent.left; - right: parent.right; - margins: 0 - bottomMargin: 2 * hifi.dimensions.contentSpacing.y - } - - // FIXME make a text field type that can be bound to a history for autocompletion - TextField { - id: textResult - label: root.label - visible: items ? false : true - anchors { - left: parent.left; - right: parent.right; - bottom: parent.bottom - } - KeyNavigation.down: acceptButton - KeyNavigation.tab: acceptButton - } - - ComboBox { - id: comboBox - label: root.label - visible: items ? true : false - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } - model: items ? items : [] - KeyNavigation.down: acceptButton - KeyNavigation.tab: acceptButton - } - } - - property alias keyboardOverride: root.keyboardOverride - property alias keyboardRaised: root.keyboardRaised - property alias punctuationMode: root.punctuationMode - Keyboard { - id: keyboard - raised: keyboardEnabled && keyboardRaised - numeric: punctuationMode - anchors { - left: parent.left - right: parent.right - bottom: buttons.top - bottomMargin: raised ? 2 * hifi.dimensions.contentSpacing.y : 0 - } - } - - Flow { - id: buttons - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - layoutDirection: Qt.RightToLeft - anchors { - bottom: parent.bottom - right: parent.right - margins: 0 - bottomMargin: hifi.dimensions.contentSpacing.y - } - Button { - id: cancelButton - action: cancelAction - KeyNavigation.left: acceptButton - KeyNavigation.up: items ? comboBox : textResult - KeyNavigation.backtab: acceptButton - } - Button { - id: acceptButton - action: acceptAction - KeyNavigation.right: cancelButton - KeyNavigation.up: items ? comboBox : textResult - KeyNavigation.tab: cancelButton - KeyNavigation.backtab: items ? comboBox : textResult - } - } - - Action { - id: cancelAction - text: qsTr("Cancel"); - shortcut: "Esc" - onTriggered: { - root.canceled(); - // FIXME we are leaking memory to avoid a crash - // root.destroy(); - - root.disableFade = true - visible = false; - } - } - - Action { - id: acceptAction - text: qsTr("OK"); - shortcut: "Return" - onTriggered: { - root.result = items ? comboBox.currentText : textResult.text - root.selected(root.result); - // FIXME we are leaking memory to avoid a crash - // root.destroy(); - - root.disableFade = true - visible = false; - } - } - } - - Keys.onPressed: { - if (!visible) { - return - } - - switch (event.key) { - case Qt.Key_Escape: - case Qt.Key_Back: - cancelAction.trigger() - event.accepted = true; - break; - - case Qt.Key_Return: - case Qt.Key_Enter: - if (acceptButton.focus) { - acceptAction.trigger() - } else if (cancelButton.focus) { - cancelAction.trigger() - } else if (comboBox.focus || comboBox.popup.focus) { - comboBox.showList() - } - event.accepted = true; - break; - } - } - - Component.onCompleted: { - keyboardEnabled = HMD.active; - updateIcon(); - d.resize(); - if (items) { - comboBox.forceActiveFocus() - } else { - textResult.forceActiveFocus() - } - } -} diff --git a/interface/resources/qml/dialogs/assetDialog/+android/AssetDialogContent.qml b/interface/resources/qml/dialogs/assetDialog/+android/AssetDialogContent.qml deleted file mode 100644 index 54bdb0a888..0000000000 --- a/interface/resources/qml/dialogs/assetDialog/+android/AssetDialogContent.qml +++ /dev/null @@ -1,533 +0,0 @@ -// -// AssetDialogContent.qml -// -// Created by David Rowe on 19 Apr 2017 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.7 -import QtQuick.Controls 1.5 - -import "../../controls-uit" -import "../../styles-uit" - -import "../fileDialog" - -Item { - // Set from OffscreenUi::assetDialog() - property alias dir: assetTableModel.folder - property alias filter: selectionType.filtersString // FIXME: Currently only supports simple filters, "*.xxx". - property int options // Not used. - - property bool selectDirectory: false - - // Not implemented. - //property bool saveDialog: false; - //property bool multiSelect: false; - - property bool singleClickNavigate: false - - HifiConstants { id: hifi } - - Component.onCompleted: { - homeButton.destination = dir; - - if (selectDirectory) { - d.currentSelectionIsFolder = true; - d.currentSelectionPath = assetTableModel.folder; - } - - assetTableView.forceActiveFocus(); - } - - Item { - id: assetDialogItem - anchors.fill: parent - clip: true - - MouseArea { - // Clear selection when click on internal unused area. - anchors.fill: parent - drag.target: root - onClicked: { - d.clearSelection(); - frame.forceActiveFocus(); - assetTableView.forceActiveFocus(); - } - } - - Row { - id: navControls - anchors { - top: parent.top - topMargin: hifi.dimensions.contentMargin.y - left: parent.left - } - spacing: hifi.dimensions.contentSpacing.x - - GlyphButton { - id: upButton - glyph: hifi.glyphs.levelUp - width: height - size: 30 - enabled: assetTableModel.parentFolder !== "" - onClicked: d.navigateUp(); - } - - GlyphButton { - id: homeButton - property string destination: "" - glyph: hifi.glyphs.home - size: 28 - width: height - enabled: destination !== "" - //onClicked: d.navigateHome(); - onClicked: assetTableModel.folder = destination; - } - } - - ComboBox { - id: pathSelector - anchors { - top: parent.top - topMargin: hifi.dimensions.contentMargin.y - left: navControls.right - leftMargin: hifi.dimensions.contentSpacing.x - right: parent.right - } - z: 10 - - property string lastValidFolder: assetTableModel.folder - - function calculatePathChoices(folder) { - var folders = folder.split("/"), - choices = [], - i, length; - - if (folders[folders.length - 1] === "") { - folders.pop(); - } - - choices.push(folders[0]); - - for (i = 1, length = folders.length; i < length; i++) { - choices.push(choices[i - 1] + "/" + folders[i]); - } - - if (folders[0] === "") { - choices[0] = "/"; - } - - choices.reverse(); - - if (choices.length > 0) { - pathSelector.model = choices; - } - } - - onLastValidFolderChanged: { - var folder = lastValidFolder; - calculatePathChoices(folder); - } - - onCurrentTextChanged: { - var folder = currentText; - - if (folder !== "/") { - folder += "/"; - } - - if (folder !== assetTableModel.folder) { - if (root.selectDirectory) { - currentSelection.text = currentText; - d.currentSelectionPath = currentText; - } - assetTableModel.folder = folder; - assetTableView.forceActiveFocus(); - } - } - } - - QtObject { - id: d - - property string currentSelectionPath - property bool currentSelectionIsFolder - property var tableViewConnection: Connections { target: assetTableView; onCurrentRowChanged: d.update(); } - - function update() { - var row = assetTableView.currentRow; - - if (row === -1) { - if (!root.selectDirectory) { - currentSelection.text = ""; - currentSelectionIsFolder = false; - } - return; - } - - var rowInfo = assetTableModel.get(row); - currentSelectionPath = rowInfo.filePath; - currentSelectionIsFolder = rowInfo.fileIsDir; - if (root.selectDirectory || !currentSelectionIsFolder) { - currentSelection.text = currentSelectionPath; - } else { - currentSelection.text = ""; - } - } - - function navigateUp() { - if (assetTableModel.parentFolder !== "") { - assetTableModel.folder = assetTableModel.parentFolder; - return true; - } - return false; - } - - function navigateHome() { - assetTableModel.folder = homeButton.destination; - return true; - } - - function clearSelection() { - assetTableView.selection.clear(); - assetTableView.currentRow = -1; - update(); - } - } - - ListModel { - id: assetTableModel - - property string folder - property string parentFolder: "" - readonly property string rootFolder: "/" - - onFolderChanged: { - parentFolder = calculateParentFolder(); - update(); - } - - function calculateParentFolder() { - if (folder !== "/") { - return folder.slice(0, folder.slice(0, -1).lastIndexOf("/") + 1); - } - return ""; - } - - function isFolder(row) { - if (row === -1) { - return false; - } - return get(row).fileIsDir; - } - - function onGetAllMappings(error, map) { - var mappings, - fileTypeFilter, - index, - path, - fileName, - fileType, - fileIsDir, - isValid, - subDirectory, - subDirectories = [], - fileNameSort, - rows = 0, - lower, - middle, - upper, - i, - length; - - clear(); - - if (error === "") { - mappings = Object.keys(map); - fileTypeFilter = filter.replace("*", "").toLowerCase(); - - for (i = 0, length = mappings.length; i < length; i++) { - index = mappings[i].lastIndexOf("/"); - - path = mappings[i].slice(0, mappings[i].lastIndexOf("/") + 1); - fileName = mappings[i].slice(path.length); - fileType = fileName.slice(fileName.lastIndexOf(".")); - fileIsDir = false; - isValid = false; - - if (fileType.toLowerCase() === fileTypeFilter) { - if (path === folder) { - isValid = !selectDirectory; - } else if (path.length > folder.length) { - subDirectory = path.slice(folder.length); - index = subDirectory.indexOf("/"); - if (index === subDirectory.lastIndexOf("/")) { - fileName = subDirectory.slice(0, index); - if (subDirectories.indexOf(fileName) === -1) { - fileIsDir = true; - isValid = true; - subDirectories.push(fileName); - } - } - } - } - - if (isValid) { - fileNameSort = (fileIsDir ? "*" : "") + fileName.toLowerCase(); - - lower = 0; - upper = rows; - while (lower < upper) { - middle = Math.floor((lower + upper) / 2); - var lessThan; - if (fileNameSort < get(middle)["fileNameSort"]) { - lessThan = true; - upper = middle; - } else { - lessThan = false; - lower = middle + 1; - } - } - - insert(lower, { - fileName: fileName, - filePath: path + (fileIsDir ? "" : fileName), - fileIsDir: fileIsDir, - fileNameSort: fileNameSort - }); - - rows++; - } - } - - } else { - console.log("Error getting mappings from Asset Server"); - } - } - - function update() { - d.clearSelection(); - clear(); - Assets.getAllMappings(onGetAllMappings); - } - } - - Table { - id: assetTableView - colorScheme: hifi.colorSchemes.light - anchors { - top: navControls.bottom - topMargin: hifi.dimensions.contentSpacing.y - left: parent.left - right: parent.right - bottom: currentSelection.top - bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height - } - - model: assetTableModel - - focus: true - - onClicked: { - if (singleClickNavigate) { - navigateToRow(row); - } - } - - onDoubleClicked: navigateToRow(row); - Keys.onReturnPressed: navigateToCurrentRow(); - Keys.onEnterPressed: navigateToCurrentRow(); - - itemDelegate: Item { - clip: true - - FiraSansSemiBold { - text: styleData.value - elide: styleData.elideMode - anchors { - left: parent.left - leftMargin: hifi.dimensions.tablePadding - right: parent.right - rightMargin: hifi.dimensions.tablePadding - verticalCenter: parent.verticalCenter - } - size: hifi.fontSizes.tableText - color: hifi.colors.baseGrayHighlight - font.family: (styleData.row !== -1 && assetTableView.model.get(styleData.row).fileIsDir) - ? "Fira Sans SemiBold" : "Fira Sans" - } - } - - TableViewColumn { - id: fileNameColumn - role: "fileName" - title: "Name" - width: assetTableView.width - movable: false - resizable: false - } - - function navigateToRow(row) { - currentRow = row; - navigateToCurrentRow(); - } - - function navigateToCurrentRow() { - if (model.isFolder(currentRow)) { - model.folder = model.get(currentRow).filePath; - } else { - okAction.trigger(); - } - } - - Timer { - id: prefixClearTimer - interval: 1000 - repeat: false - running: false - onTriggered: assetTableView.prefix = ""; - } - - property string prefix: "" - - function addToPrefix(event) { - if (!event.text || event.text === "") { - return false; - } - var newPrefix = prefix + event.text.toLowerCase(); - var matchedIndex = -1; - for (var i = 0; i < model.count; ++i) { - var name = model.get(i).fileName.toLowerCase(); - if (0 === name.indexOf(newPrefix)) { - matchedIndex = i; - break; - } - } - - if (matchedIndex !== -1) { - assetTableView.selection.clear(); - assetTableView.selection.select(matchedIndex); - assetTableView.currentRow = matchedIndex; - assetTableView.prefix = newPrefix; - } - prefixClearTimer.restart(); - return true; - } - - Keys.onPressed: { - switch (event.key) { - case Qt.Key_Backspace: - case Qt.Key_Tab: - case Qt.Key_Backtab: - event.accepted = false; - break; - - default: - if (addToPrefix(event)) { - event.accepted = true - } else { - event.accepted = false; - } - break; - } - } - } - - TextField { - id: currentSelection - label: selectDirectory ? "Directory:" : "File name:" - anchors { - left: parent.left - right: selectionType.visible ? selectionType.left: parent.right - rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0 - bottom: buttonRow.top - bottomMargin: hifi.dimensions.contentSpacing.y - } - readOnly: true - activeFocusOnTab: !readOnly - onActiveFocusChanged: if (activeFocus) { selectAll(); } - onAccepted: okAction.trigger(); - } - - FileTypeSelection { - id: selectionType - anchors { - top: currentSelection.top - left: buttonRow.left - right: parent.right - } - visible: !selectDirectory && filtersCount > 1 - KeyNavigation.left: assetTableView - KeyNavigation.right: openButton - } - - Action { - id: okAction - text: currentSelection.text && root.selectDirectory && assetTableView.currentRow === -1 ? "Choose" : "Open" - enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false - onTriggered: { - if (!root.selectDirectory && !d.currentSelectionIsFolder - || root.selectDirectory && assetTableView.currentRow === -1) { - selectedAsset(d.currentSelectionPath); - root.destroy(); - } else { - assetTableView.navigateToCurrentRow(); - } - } - } - - Action { - id: cancelAction - text: "Cancel" - onTriggered: { - canceled(); - root.destroy(); - } - } - - Row { - id: buttonRow - anchors { - right: parent.right - bottom: parent.bottom - } - spacing: hifi.dimensions.contentSpacing.y - - Button { - id: openButton - color: hifi.buttons.blue - action: okAction - Keys.onReturnPressed: okAction.trigger() - KeyNavigation.up: selectionType - KeyNavigation.left: selectionType - KeyNavigation.right: cancelButton - } - - Button { - id: cancelButton - action: cancelAction - KeyNavigation.up: selectionType - KeyNavigation.left: openButton - KeyNavigation.right: assetTableView.contentItem - Keys.onReturnPressed: { canceled(); root.enabled = false } - } - } - } - - Keys.onPressed: { - switch (event.key) { - case Qt.Key_Backspace: - event.accepted = d.navigateUp(); - break; - - case Qt.Key_Home: - event.accepted = d.navigateHome(); - break; - - } - } -} diff --git a/interface/resources/qml/dialogs/preferences/SpinBoxPreference.qml b/interface/resources/qml/dialogs/preferences/SpinBoxPreference.qml index 89e1096a04..b2c334b674 100644 --- a/interface/resources/qml/dialogs/preferences/SpinBoxPreference.qml +++ b/interface/resources/qml/dialogs/preferences/SpinBoxPreference.qml @@ -26,11 +26,9 @@ Preference { preference.save(); } - Item { + Row { id: control anchors { - left: parent.left - right: parent.right bottom: parent.bottom } height: Math.max(spinnerLabel.height, spinner.controlHeight) @@ -40,15 +38,14 @@ Preference { text: root.label + ":" colorScheme: hifi.colorSchemes.dark anchors { - left: parent.left - right: spinner.left - rightMargin: hifi.dimensions.labelPadding verticalCenter: parent.verticalCenter } horizontalAlignment: Text.AlignRight wrapMode: Text.Wrap } + spacing: hifi.dimensions.labelPadding + SpinBox { id: spinner decimals: preference.decimals @@ -56,7 +53,6 @@ Preference { maximumValue: preference.max width: 100 anchors { - right: parent.right verticalCenter: parent.verticalCenter } colorScheme: hifi.colorSchemes.dark diff --git a/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml b/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml index 731acc7e5b..126e62fc30 100644 --- a/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml +++ b/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml @@ -56,8 +56,8 @@ Preference { id: slider value: preference.value width: 100 - minimumValue: MyAvatar.getDomainMinScale() - maximumValue: MyAvatar.getDomainMaxScale() + minimumValue: preference.min + maximumValue: preference.max stepSize: preference.step onValueChanged: { spinner.realValue = value @@ -74,8 +74,8 @@ Preference { id: spinner decimals: preference.decimals realValue: preference.value - minimumValue: MyAvatar.getDomainMinScale() - maximumValue: MyAvatar.getDomainMaxScale() + minimumValue: preference.min + maximumValue: preference.max width: 100 onValueChanged: { slider.value = realValue; diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml new file mode 100644 index 0000000000..0a2dcb951b --- /dev/null +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -0,0 +1,840 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtQml.Models 2.1 +import QtGraphicalEffects 1.0 +import "../controls-uit" as HifiControls +import "../styles-uit" +import "avatarapp" + +Rectangle { + id: root + width: 480 + height: 706 + + property bool keyboardEnabled: true + property bool keyboardRaised: false + property bool punctuationMode: false + + HifiControls.Keyboard { + id: keyboard + z: 1000 + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + + color: style.colors.white + property string getAvatarsMethod: 'getAvatars' + + signal sendToScript(var message); + function emitSendToScript(message) { + sendToScript(message); + } + + ListModel { // the only purpose of this model is to convert JS object to ListElement + id: currentAvatarModel + dynamicRoles: true; + function makeAvatarEntry(avatarObject) { + clear(); + append(avatarObject); + return get(count - 1); + } + } + + property var jointNames; + property var currentAvatarSettings; + + function fetchAvatarModelName(marketId, avatar) { + var xmlhttp = new XMLHttpRequest(); + var url = "https://highfidelity.com/api/v1/marketplace/items/" + marketId; + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState === XMLHttpRequest.DONE && xmlhttp.status === 200) { + try { + var marketResponse = JSON.parse(xmlhttp.responseText.trim()) + + if(marketResponse.status === 'success') { + avatar.modelName = marketResponse.data.title; + } + } + catch(err) { + console.error(err); + } + } + } + xmlhttp.open("GET", url, true); + xmlhttp.send(); + } + + function getAvatarModelName() { + + if(currentAvatar === null) { + return ''; + } + if(currentAvatar.modelName !== undefined) { + return currentAvatar.modelName; + } else { + var marketId = allAvatars.extractMarketId(currentAvatar.avatarUrl); + if(marketId !== '') { + fetchAvatarModelName(marketId, currentAvatar); + } + } + + var avatarUrl = currentAvatar.avatarUrl; + var splitted = avatarUrl.split('/'); + + return splitted[splitted.length - 1]; + } + + property string avatarName: currentAvatar ? currentAvatar.name : '' + property string avatarUrl: currentAvatar ? currentAvatar.thumbnailUrl : null + property bool isAvatarInFavorites: currentAvatar ? allAvatars.findAvatar(currentAvatar.name) !== undefined : false + property int avatarWearablesCount: currentAvatar ? currentAvatar.wearables.count : 0 + property var currentAvatar: null; + function setCurrentAvatar(avatar, bookmarkName) { + var currentAvatarObject = allAvatars.makeAvatarObject(avatar, bookmarkName); + currentAvatar = currentAvatarModel.makeAvatarEntry(currentAvatarObject); + } + + property url externalAvatarThumbnailUrl: '../../images/avatarapp/guy-in-circle.svg' + + function fromScript(message) { + if(message.method === 'initialize') { + jointNames = message.data.jointNames; + emitSendToScript({'method' : getAvatarsMethod}); + } else if(message.method === 'wearableUpdated') { + adjustWearables.refreshWearable(message.entityID, message.wearableIndex, message.properties, message.updateUI); + } else if(message.method === 'wearablesUpdated') { + var wearablesModel = currentAvatar.wearables; + wearablesModel.clear(); + message.wearables.forEach(function(wearable) { + wearablesModel.append(wearable); + }); + adjustWearables.refresh(currentAvatar); + } else if(message.method === 'scaleChanged') { + currentAvatar.avatarScale = message.value; + updateCurrentAvatarInBookmarks(currentAvatar); + } else if(message.method === 'externalAvatarApplied') { + currentAvatar.avatarUrl = message.avatarURL; + currentAvatar.thumbnailUrl = allAvatars.makeThumbnailUrl(message.avatarURL); + currentAvatar.entry.avatarUrl = currentAvatar.avatarUrl; + currentAvatar.modelName = undefined; + updateCurrentAvatarInBookmarks(currentAvatar); + } else if(message.method === 'settingChanged') { + currentAvatarSettings[message.name] = message.value; + } else if(message.method === 'changeSettings') { + currentAvatarSettings = message.settings; + } else if(message.method === 'bookmarkLoaded') { + setCurrentAvatar(message.data.currentAvatar, message.data.name); + var avatarIndex = allAvatars.findAvatarIndex(currentAvatar.name); + allAvatars.move(avatarIndex, 0, 1); + view.setPage(0); + } else if(message.method === 'bookmarkAdded') { + var avatar = allAvatars.findAvatar(message.bookmarkName); + if(avatar !== undefined) { + var avatarObject = allAvatars.makeAvatarObject(message.bookmark, message.bookmarkName); + for(var prop in avatarObject) { + avatar[prop] = avatarObject[prop]; + } + if(currentAvatar.name === message.bookmarkName) { + currentAvatar = currentAvatarModel.makeAvatarEntry(avatarObject); + } + } else { + allAvatars.addAvatarEntry(message.bookmark, message.bookmarkName); + } + updateCurrentAvatarInBookmarks(currentAvatar); + } else if(message.method === 'bookmarkDeleted') { + pageOfAvatars.isUpdating = true; + + var index = pageOfAvatars.findAvatarIndex(message.name); + var absoluteIndex = view.currentPage * view.itemsPerPage + index + + allAvatars.remove(absoluteIndex) + pageOfAvatars.remove(index); + + var itemsOnPage = pageOfAvatars.count; + var newItemIndex = view.currentPage * view.itemsPerPage + itemsOnPage; + + if(newItemIndex <= (allAvatars.count - 1)) { + pageOfAvatars.append(allAvatars.get(newItemIndex)); + } else { + if(!pageOfAvatars.hasGetAvatars()) + pageOfAvatars.appendGetAvatars(); + } + + pageOfAvatars.isUpdating = false; + } else if(message.method === getAvatarsMethod) { + var getAvatarsData = message.data; + allAvatars.populate(getAvatarsData.bookmarks); + setCurrentAvatar(getAvatarsData.currentAvatar, ''); + displayNameInput.text = getAvatarsData.displayName; + currentAvatarSettings = getAvatarsData.currentAvatarSettings; + + updateCurrentAvatarInBookmarks(currentAvatar); + } else if(message.method === 'updateAvatarInBookmarks') { + updateCurrentAvatarInBookmarks(currentAvatar); + } else if(message.method === 'selectAvatarEntity') { + adjustWearables.selectWearableByID(message.entityID); + } + } + + function updateCurrentAvatarInBookmarks(avatar) { + var bookmarkAvatarIndex = allAvatars.findAvatarIndexByValue(avatar); + if(bookmarkAvatarIndex === -1) { + avatar.name = ''; + view.setPage(0); + } else { + var bookmarkAvatar = allAvatars.get(bookmarkAvatarIndex); + avatar.name = bookmarkAvatar.name; + view.selectAvatar(bookmarkAvatar); + } + } + + property bool isInManageState: false + + Component.onCompleted: { + } + + AvatarAppStyle { + id: style + } + + AvatarAppHeader { + id: header + z: 100 + + property string currentPage: "Avatar" + property bool mainPageVisible: !settings.visible && !adjustWearables.visible + + Binding on currentPage { + when: settings.visible + value: "Avatar Settings" + } + Binding on currentPage { + when: adjustWearables.visible + value: "Adjust Wearables" + } + Binding on currentPage { + when: header.mainPageVisible + value: "Avatar" + } + + pageTitle: currentPage + avatarIconVisible: mainPageVisible + settingsButtonVisible: mainPageVisible + onSettingsClicked: { + settings.open(currentAvatarSettings, currentAvatar.avatarScale); + } + } + + Settings { + id: settings + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + anchors.bottom: parent.bottom + + z: 3 + + onSaveClicked: function() { + var avatarSettings = { + dominantHand : settings.dominantHandIsLeft ? 'left' : 'right', + collisionsEnabled : settings.avatarCollisionsOn, + animGraphUrl : settings.avatarAnimationJSON, + collisionSoundUrl : settings.avatarCollisionSoundUrl + }; + + emitSendToScript({'method' : 'saveSettings', 'settings' : avatarSettings, 'avatarScale': settings.scaleValue}) + + close(); + } + onCancelClicked: function() { + emitSendToScript({'method' : 'revertScale', 'avatarScale' : avatarScaleBackup}); + + close(); + } + + onScaleChanged: { + emitSendToScript({'method' : 'setScale', 'avatarScale' : scale}) + } + } + + AdjustWearables { + id: adjustWearables + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + anchors.bottom: parent.bottom + jointNames: root.jointNames + onWearableUpdated: { + emitSendToScript({'method' : 'adjustWearable', 'entityID' : id, 'wearableIndex' : index, 'properties' : properties}) + } + onWearableDeleted: { + emitSendToScript({'method' : 'deleteWearable', 'entityID' : id, 'avatarName' : avatarName}); + } + onAdjustWearablesOpened: { + emitSendToScript({'method' : 'adjustWearablesOpened', 'avatarName' : avatarName}); + } + onAdjustWearablesClosed: { + emitSendToScript({'method' : 'adjustWearablesClosed', 'save' : status, 'avatarName' : avatarName}); + } + onWearableSelected: { + emitSendToScript({'method' : 'selectWearable', 'entityID' : id}); + } + + z: 3 + } + + Rectangle { + id: mainBlock + anchors.left: parent.left + anchors.leftMargin: 30 + anchors.right: parent.right + anchors.rightMargin: 30 + anchors.top: header.bottom + anchors.bottom: favoritesBlock.top + + // TextStyle1 + RalewaySemiBold { + size: 24; + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 34 + } + + // TextStyle1 + RalewaySemiBold { + id: displayNameLabel + size: 24; + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 25 + text: 'Display Name' + } + + InputField { + id: displayNameInput + + font.family: "Fira Sans" + font.pixelSize: 15 + anchors.left: displayNameLabel.right + anchors.leftMargin: 30 + anchors.verticalCenter: displayNameLabel.verticalCenter + anchors.right: parent.right + width: 232 + + text: 'ThisIsDisplayName' + + onEditingFinished: { + emitSendToScript({'method' : 'changeDisplayName', 'displayName' : text}) + focus = false; + } + } + + ShadowImage { + id: avatarImage + width: 134 + height: 134 + anchors.top: displayNameLabel.bottom + anchors.topMargin: 31 + Binding on source { + when: avatarUrl !== '' + value: avatarUrl + } + + visible: avatarImage.status !== Image.Loading && avatarImage.status !== Image.Error + fillMode: Image.PreserveAspectCrop + } + + ShadowImage { + id: customAvatarImage + anchors.fill: avatarImage; + visible: avatarUrl === '' || avatarImage.status === Image.Error + source: externalAvatarThumbnailUrl + } + + ShadowRectangle { + anchors.fill: avatarImage; + color: 'white' + visible: avatarImage.status === Image.Loading + radius: avatarImage.radius + + dropShadowRadius: avatarImage.dropShadowRadius; + dropShadowHorizontalOffset: avatarImage.dropShadowHorizontalOffset + dropShadowVerticalOffset: avatarImage.dropShadowVerticalOffset + + Spinner { + id: spinner + visible: parent.visible + anchors.fill: parent; + } + } + + AvatarWearablesIndicator { + anchors.right: avatarImage.right + anchors.bottom: avatarImage.bottom + anchors.rightMargin: -radius + anchors.bottomMargin: 6.08 + wearablesCount: avatarWearablesCount + visible: avatarWearablesCount !== 0 + } + + RowLayout { + id: star + anchors.top: avatarImage.top + anchors.topMargin: 11 + anchors.left: avatarImage.right + anchors.leftMargin: 30.5 + anchors.right: parent.right + + spacing: 12.3 + + Image { + width: 21.2 + height: 19.3 + source: isAvatarInFavorites ? '../../images/FavoriteIconActive.svg' : '../../images/FavoriteIconInActive.svg' + anchors.verticalCenter: parent.verticalCenter + } + + // TextStyle5 + FiraSansSemiBold { + size: 22; + Layout.fillWidth: true + text: isAvatarInFavorites ? avatarName : "Add to Favorites" + elide: Qt.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + enabled: !isAvatarInFavorites + anchors.fill: star + onClicked: { + createFavorite.onSaveClicked = function() { + var entry = currentAvatar.entry; + + var wearables = []; + for(var i = 0; i < currentAvatar.wearables.count; ++i) { + wearables.push(currentAvatar.wearables.get(i)); + } + + entry.avatarEntites = wearables; + currentAvatar.name = createFavorite.favoriteNameText; + + emitSendToScript({'method': 'addAvatar', 'name' : currentAvatar.name}); + createFavorite.close(); + } + + var avatarThumbnail = (avatarUrl === '' || avatarImage.status === Image.Error) ? + externalAvatarThumbnailUrl : avatarUrl; + + createFavorite.open(root.currentAvatar.wearables.count, avatarThumbnail); + } + } + + // TextStyle3 + RalewayRegular { + id: avatarNameLabel + size: 22; + text: getAvatarModelName(); + elide: Qt.ElideRight + + anchors.right: linkLabel.left + anchors.left: avatarImage.right + anchors.leftMargin: 30 + anchors.top: star.bottom + anchors.topMargin: 11 + property bool hasMarketId: currentAvatar && allAvatars.extractMarketId(currentAvatar.avatarUrl) !== ''; + + MouseArea { + enabled: avatarNameLabel.hasMarketId + anchors.fill: parent; + onClicked: emitSendToScript({'method' : 'navigate', 'url' : allAvatars.makeMarketItemUrl(currentAvatar.avatarUrl)}) + } + color: hasMarketId ? style.colors.blueHighlight : 'black' + } + + // TextStyle3 + RalewayRegular { + id: wearablesLabel + size: 22; + anchors.left: avatarImage.right + anchors.leftMargin: 30 + anchors.top: avatarNameLabel.bottom + anchors.topMargin: 16 + color: 'black' + text: 'Wearables' + } + + SquareLabel { + id: linkLabel + anchors.right: parent.right + anchors.verticalCenter: avatarNameLabel.verticalCenter + glyphText: "." + glyphSize: 22 + + MouseArea { + anchors.fill: parent + onClicked: { + popup.showSpecifyAvatarUrl(function() { + var url = popup.inputText.text; + emitSendToScript({'method' : 'applyExternalAvatar', 'avatarURL' : url}) + }, function(link) { + Qt.openUrlExternally(link); + }); + } + } + } + + SquareLabel { + anchors.right: parent.right + anchors.verticalCenter: wearablesLabel.verticalCenter + glyphText: "\ue02e" + + visible: avatarWearablesCount !== 0 + + MouseArea { + anchors.fill: parent + onClicked: { + adjustWearables.open(currentAvatar); + } + } + } + + // TextStyle3 + RalewayRegular { + size: 22; + anchors.right: parent.right + anchors.verticalCenter: wearablesLabel.verticalCenter + font.underline: true + text: "Add" + color: 'black' + visible: avatarWearablesCount === 0 + + MouseArea { + anchors.fill: parent + onClicked: { + popup.showGetWearables(function() { + emitSendToScript({'method' : 'navigate', 'url' : 'hifi://AvatarIsland/11.5848,-8.10862,-2.80195'}) + }, function(link) { + emitSendToScript({'method' : 'navigate', 'url' : link}) + }); + } + } + } + } + + Rectangle { + id: favoritesBlock + height: 407 + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + color: style.colors.lightGrayBackground + + // TextStyle1 + RalewaySemiBold { + id: favoritesLabel + size: 24; + anchors.top: parent.top + anchors.topMargin: 15 + anchors.left: parent.left + anchors.leftMargin: 30 + text: "Favorites" + } + + // TextStyle8 + RalewaySemiBold { + id: manageLabel + color: style.colors.blueHighlight + size: 20; + anchors.top: parent.top + anchors.topMargin: 20 + anchors.right: parent.right + anchors.rightMargin: 30 + text: isInManageState ? "Back" : "Manage" + MouseArea { + anchors.fill: parent + onClicked: { + isInManageState = isInManageState ? false : true; + } + } + } + + Item { + anchors.left: parent.left + anchors.leftMargin: 30 + anchors.right: parent.right + anchors.rightMargin: 0 + + anchors.top: favoritesLabel.bottom + anchors.topMargin: 20 + anchors.bottom: parent.bottom + + GridView { + id: view + anchors.fill: parent + interactive: false; + currentIndex: currentAvatarIndexInBookmarksPage(); + + function currentAvatarIndexInBookmarksPage() { + return (currentAvatar && currentAvatar.name !== '' && !pageOfAvatars.isUpdating) ? pageOfAvatars.findAvatarIndex(currentAvatar.name) : -1; + } + + property int horizontalSpacing: 18 + property int verticalSpacing: 44 + property int thumbnailWidth: 92 + property int thumbnailHeight: 92 + + function selectAvatar(avatar) { + emitSendToScript({'method' : 'selectAvatar', 'name' : avatar.name}) + } + + function deleteAvatar(avatar) { + emitSendToScript({'method' : 'deleteAvatar', 'name' : avatar.name}) + } + + AvatarsModel { + id: allAvatars + } + + property int itemsPerPage: 8 + property int totalPages: Math.ceil((allAvatars.count + 1) / itemsPerPage) + property int currentPage: 0; + onCurrentPageChanged: { + currentIndex = Qt.binding(currentAvatarIndexInBookmarksPage); + } + + property bool hasNext: currentPage < (totalPages - 1) + property bool hasPrev: currentPage > 0 + + function setPage(pageIndex) { + pageOfAvatars.isUpdating = true; + pageOfAvatars.clear(); + var start = pageIndex * itemsPerPage; + var end = Math.min(start + itemsPerPage, allAvatars.count); + + for(var itemIndex = 0; start < end; ++start, ++itemIndex) { + var avatarItem = allAvatars.get(start) + pageOfAvatars.append(avatarItem); + } + + if(pageOfAvatars.count !== itemsPerPage) + pageOfAvatars.appendGetAvatars(); + + currentPage = pageIndex; + pageOfAvatars.isUpdating = false; + } + + model: AvatarsModel { + id: pageOfAvatars + + property bool isUpdating: false; + property var getMoreAvatarsEntry: {'thumbnailUrl' : '', 'name' : '', 'getMoreAvatars' : true} + + function appendGetAvatars() { + append(getMoreAvatarsEntry); + } + + function hasGetAvatars() { + return count != 0 && get(count - 1).getMoreAvatars + } + + function removeGetAvatars() { + if(hasGetAvatars()) { + remove(count - 1) + } + } + } + + flow: GridView.FlowLeftToRight + + cellHeight: thumbnailHeight + verticalSpacing + cellWidth: thumbnailWidth + horizontalSpacing + + delegate: Item { + id: delegateRoot + height: GridView.view.cellHeight + width: GridView.view.cellWidth + + Item { + id: container + width: 92 + height: 92 + + Behavior on y { + NumberAnimation { + duration: 100 + } + } + + states: [ + State { + name: "hovered" + when: favoriteAvatarMouseArea.containsMouse; + PropertyChanges { target: favoriteAvatarMouseArea; anchors.bottomMargin: -5 } + PropertyChanges { target: container; y: -5 } + PropertyChanges { target: favoriteAvatarImage; dropShadowRadius: 10 } + PropertyChanges { target: favoriteAvatarImage; dropShadowVerticalOffset: 6 } + } + ] + + property bool highlighted: delegateRoot.GridView.isCurrentItem + + AvatarThumbnail { + id: favoriteAvatarImage + externalAvatarThumbnailUrl: root.externalAvatarThumbnailUrl + avatarUrl: thumbnailUrl + border.color: container.highlighted ? style.colors.blueHighlight : 'transparent' + border.width: container.highlighted ? 4 : 0 + wearablesCount: { + return !getMoreAvatars ? wearables.count : 0 + } + + visible: !getMoreAvatars + + MouseArea { + id: favoriteAvatarMouseArea + anchors.fill: parent + anchors.margins: 0 + enabled: !container.highlighted + hoverEnabled: enabled + + onClicked: { + if(isInManageState) { + var currentItem = delegateRoot.GridView.view.model.get(index); + popup.showDeleteFavorite(currentItem.name, function() { + view.deleteAvatar(currentItem); + }); + } else { + if(delegateRoot.GridView.view.currentIndex !== index) { + var currentItem = delegateRoot.GridView.view.model.get(index); + popup.showLoadFavorite(currentItem.name, function() { + view.selectAvatar(currentItem); + }); + } + } + } + } + } + + Rectangle { + anchors.fill: favoriteAvatarImage + color: '#AFAFAF' + opacity: 0.4 + radius: 5 + visible: isInManageState && !container.highlighted && !getMoreAvatars + } + + HiFiGlyphs { + anchors.fill: parent + text: "{" + visible: isInManageState && !container.highlighted && !getMoreAvatars + horizontalAlignment: Text.AlignHCenter + size: 56 + } + + ShadowRectangle { + width: 92 + height: 92 + radius: 5 + color: style.colors.blueHighlight + visible: getMoreAvatars && !isInManageState + + HiFiGlyphs { + anchors.centerIn: parent + + color: 'white' + size: 60 + text: "K" + } + + MouseArea { + anchors.fill: parent + + onClicked: { + popup.showBuyAvatars(function() { + emitSendToScript({'method' : 'navigate', 'url' : 'hifi://BodyMart'}) + }, function(link) { + emitSendToScript({'method' : 'navigate', 'url' : link}) + }); + } + } + } + } + + // TextStyle7 + FiraSansRegular { + id: text + size: 18; + lineHeightMode: Text.FixedHeight + lineHeight: 16.9; + width: view.thumbnailWidth + height: view.verticalSpacing + elide: Qt.ElideRight + anchors.top: container.bottom + anchors.topMargin: 8 + anchors.horizontalCenter: container.horizontalCenter + verticalAlignment: Text.AlignTop + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + text: getMoreAvatars ? 'Get More Avatars' : name + visible: !getMoreAvatars || !isInManageState + } + } + } + + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + + Rectangle { + width: 40 + height: 40 + color: 'transparent' + + PageIndicator { + x: 1 + hasNext: view.hasNext + hasPrev: view.hasPrev + onClicked: view.setPage(view.currentPage - 1) + } + } + + spacing: 0 + + Rectangle { + width: 40 + height: 40 + color: 'transparent' + + PageIndicator { + x: -1 + isPrevious: false + hasNext: view.hasNext + hasPrev: view.hasPrev + onClicked: view.setPage(view.currentPage + 1) + } + } + + anchors.bottom: parent.bottom + anchors.bottomMargin: 20 + } + } + + MessageBoxes { + id: popup + } + + CreateFavoriteDialog { + avatars: allAvatars + id: createFavorite + } +} diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 5f2dfea8c7..785b586dd2 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -53,7 +53,7 @@ Column { 'protocol=' + encodeURIComponent(Window.protocolSignature()) ]; endpoint: '/api/v1/user_stories?' + options.join('&'); - itemsPerPage: 3; + itemsPerPage: 4; processPage: function (data) { return data.user_stories.map(makeModelData); }; @@ -106,7 +106,6 @@ Column { highlightMoveDuration: -1; highlightMoveVelocity: -1; currentIndex: -1; - onAtXEndChanged: { if (scroll.atXEnd && !scroll.atXBeginning) { suggestions.getNextPage(); } } spacing: 12; width: parent.width; diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 8dcb76442b..6884d2e1f6 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -61,7 +61,7 @@ Rectangle { 'username'; } sortAscending: connectionsTable.sortIndicatorOrder === Qt.AscendingOrder; - itemsPerPage: 9; + itemsPerPage: 10; listView: connectionsTable; processPage: function (data) { return data.users.map(function (user) { @@ -765,7 +765,7 @@ Rectangle { TableViewColumn { id: connectionsUserNameHeader; role: "userName"; - title: connectionsTable.rowCount + (connectionsTable.rowCount === 1 ? " NAME" : " NAMES"); + title: connectionsUserModel.totalEntries + (connectionsUserModel.totalEntries === 1 ? " NAME" : " NAMES"); width: connectionsNameCardWidth; movable: false; resizable: false; @@ -786,14 +786,6 @@ Rectangle { } model: connectionsUserModel; - Connections { - target: connectionsTable.flickableItem; - onAtYEndChanged: { - if (connectionsTable.flickableItem.atYEnd && !connectionsTable.flickableItem.atYBeginning) { - connectionsUserModel.getNextPage(); - } - } - } // This Rectangle refers to each Row in the connectionsTable. rowDelegate: Rectangle { @@ -1108,7 +1100,7 @@ Rectangle { function findNearbySessionIndex(sessionId, optionalData) { // no findIndex in .qml var data = optionalData || nearbyUserModelData, length = data.length; for (var i = 0; i < length; i++) { - if (data[i].sessionId === sessionId) { + if (data[i].sessionId === sessionId.toString()) { return i; } } @@ -1120,7 +1112,7 @@ Rectangle { var data = message.params; var index = -1; iAmAdmin = Users.canKick; - index = findNearbySessionIndex('', data); + index = findNearbySessionIndex("", data); if (index !== -1) { myData = data[index]; data.splice(index, 1); @@ -1197,8 +1189,8 @@ Rectangle { for (var userId in message.params) { var audioLevel = message.params[userId][0]; var avgAudioLevel = message.params[userId][1]; - // If the userId is 0, we're updating "myData". - if (userId == 0) { + // If the userId is "", we're updating "myData". + if (userId === "") { myData.audioLevel = audioLevel; myCard.audioLevel = audioLevel; // Defensive programming myData.avgAudioLevel = avgAudioLevel; diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index ba50b7f238..cc1ba49984 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -129,12 +129,10 @@ Rectangle { id: stereoMic spacing: muteMic.spacing; text: qsTr("Enable stereo input"); - checked: AudioScriptingInterface.isStereoInput(); + checked: AudioScriptingInterface.isStereoInput; onClicked: { - var success = AudioScriptingInterface.setStereoInput(checked); - if (!success) { - checked = !checked; - } + AudioScriptingInterface.isStereoInput = checked; + checked = Qt.binding(function() { return AudioScriptingInterface.isStereoInput; }); // restore binding } } } diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml new file mode 100644 index 0000000000..a501185853 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml @@ -0,0 +1,359 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Rectangle { + id: root; + visible: false; + width: 480 + height: 706 + color: 'white' + + signal wearableUpdated(var id, int index, var properties); + signal wearableSelected(var id); + signal wearableDeleted(string avatarName, var id); + + signal adjustWearablesOpened(var avatarName); + signal adjustWearablesClosed(bool status, var avatarName); + + property bool modified: false; + Component.onCompleted: { + modified = false; + } + + property var jointNames; + property string avatarName: '' + property var wearablesModel; + + function open(avatar) { + adjustWearablesOpened(avatar.name); + + visible = true; + avatarName = avatar.name; + wearablesModel = avatar.wearables; + refresh(avatar); + } + + function refresh(avatar) { + wearablesCombobox.model.clear(); + for(var i = 0; i < avatar.wearables.count; ++i) { + var wearable = avatar.wearables.get(i).properties; + for(var j = (wearable.modelURL.length - 1); j >= 0; --j) { + if(wearable.modelURL[j] === '/') { + wearable.text = wearable.modelURL.substring(j + 1) + ' [%jointIndex%]'.replace('%jointIndex%', jointNames[wearable.parentJointIndex]); + break; + } + } + wearablesCombobox.model.append(wearable); + } + + wearablesCombobox.currentIndex = 0; + } + + function refreshWearable(wearableID, wearableIndex, properties, updateUI) { + if(wearableIndex === -1) { + wearableIndex = wearablesCombobox.model.findIndexById(wearableID); + } + + var wearable = wearablesCombobox.model.get(wearableIndex); + + if(!wearable) { + return; + } + + var wearableModelItemProperties = wearablesModel.get(wearableIndex).properties; + + for(var prop in properties) { + wearable[prop] = properties[prop]; + wearableModelItemProperties[prop] = wearable[prop]; + + if(updateUI) { + if(prop === 'localPosition') { + position.set(wearable[prop]); + } else if(prop === 'localRotationAngles') { + rotation.set(wearable[prop]); + } else if(prop === 'dimensions') { + scalespinner.set(wearable[prop].x / wearable.naturalDimensions.x); + } + } + } + + wearablesModel.setProperty(wearableIndex, 'properties', wearableModelItemProperties); + } + + function getCurrentWearable() { + return wearablesCombobox.model.get(wearablesCombobox.currentIndex) + } + + function selectWearableByID(entityID) { + for(var i = 0; i < wearablesCombobox.model.count; ++i) { + var wearable = wearablesCombobox.model.get(i); + if(wearable.id === entityID) { + wearablesCombobox.currentIndex = i; + break; + } + } + } + + function close(status) { + visible = false; + adjustWearablesClosed(status, avatarName); + } + + HifiConstants { id: hifi } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + + Column { + anchors.top: parent.top + anchors.topMargin: 15 + anchors.horizontalCenter: parent.horizontalCenter + + spacing: 20 + width: parent.width - 30 * 2 + + HifiControlsUit.ComboBox { + id: wearablesCombobox + anchors.left: parent.left + anchors.right: parent.right + comboBox.textRole: "text" + + model: ListModel { + function findIndexById(id) { + + for(var i = 0; i < count; ++i) { + var wearable = get(i); + if(wearable.id === id) { + return i; + } + } + + return -1; + } + } + + comboBox.onCurrentIndexChanged: { + var currentWearable = getCurrentWearable(); + + if(currentWearable) { + position.set(currentWearable.localPosition); + rotation.set(currentWearable.localRotationAngles); + scalespinner.set(currentWearable.dimensions.x / currentWearable.naturalDimensions.x) + + wearableSelected(currentWearable.id); + } + } + } + + Column { + width: parent.width + spacing: 5 + + Row { + spacing: 20 + + // TextStyle5 + FiraSansSemiBold { + id: positionLabel + size: 22; + text: "Position" + } + + // TextStyle7 + FiraSansRegular { + size: 18; + lineHeightMode: Text.FixedHeight + lineHeight: 16.9; + text: "m" + anchors.verticalCenter: positionLabel.verticalCenter + } + } + + Vector3 { + id: position + backgroundColor: "lightgray" + + function set(localPosition) { + notify = false; + xvalue = localPosition.x + yvalue = localPosition.y + zvalue = localPosition.z + notify = true; + } + + function notifyPositionChanged() { + modified = true; + var properties = { + localPosition: { 'x' : xvalue, 'y' : yvalue, 'z' : zvalue } + }; + + wearableUpdated(getCurrentWearable().id, wearablesCombobox.currentIndex, properties); + } + + property bool notify: false; + + onXvalueChanged: if(notify) notifyPositionChanged(); + onYvalueChanged: if(notify) notifyPositionChanged(); + onZvalueChanged: if(notify) notifyPositionChanged(); + + decimals: 2 + realFrom: -10 + realTo: 10 + realStepSize: 0.01 + } + } + + Column { + width: parent.width + spacing: 5 + + Row { + spacing: 20 + + // TextStyle5 + FiraSansSemiBold { + id: rotationLabel + size: 22; + text: "Rotation" + } + + // TextStyle7 + FiraSansRegular { + size: 18; + lineHeightMode: Text.FixedHeight + lineHeight: 16.9; + text: "deg" + anchors.verticalCenter: rotationLabel.verticalCenter + } + } + + Vector3 { + id: rotation + backgroundColor: "lightgray" + + function set(localRotationAngles) { + notify = false; + xvalue = localRotationAngles.x + yvalue = localRotationAngles.y + zvalue = localRotationAngles.z + notify = true; + } + + function notifyRotationChanged() { + modified = true; + var properties = { + localRotationAngles: { 'x' : xvalue, 'y' : yvalue, 'z' : zvalue } + }; + + wearableUpdated(getCurrentWearable().id, wearablesCombobox.currentIndex, properties); + } + + property bool notify: false; + + onXvalueChanged: if(notify) notifyRotationChanged(); + onYvalueChanged: if(notify) notifyRotationChanged(); + onZvalueChanged: if(notify) notifyRotationChanged(); + + decimals: 0 + realFrom: -180 + realTo: 180 + realStepSize: 1 + } + } + + Column { + width: parent.width + spacing: 5 + + // TextStyle5 + FiraSansSemiBold { + size: 22; + text: "Scale" + } + + Item { + width: parent.width + height: childrenRect.height + + HifiControlsUit.SpinBox { + id: scalespinner + decimals: 2 + realStepSize: 0.1 + realFrom: 0.1 + realTo: 3.0 + realValue: 1.0 + backgroundColor: "lightgray" + width: position.spinboxWidth + colorScheme: hifi.colorSchemes.light + + property bool notify: false; + onValueChanged: if(notify) notifyScaleChanged(); + + function set(value) { + notify = false; + realValue = value + notify = true; + } + + function notifyScaleChanged() { + modified = true; + var currentWearable = getCurrentWearable(); + var naturalDimensions = currentWearable.naturalDimensions; + + var properties = { + dimensions: { + 'x' : realValue * naturalDimensions.x, + 'y' : realValue * naturalDimensions.y, + 'z' : realValue * naturalDimensions.z + } + }; + + wearableUpdated(currentWearable.id, wearablesCombobox.currentIndex, properties); + } + } + + HifiControlsUit.Button { + fontSize: 18 + height: 40 + anchors.right: parent.right + color: hifi.buttons.red; + colorScheme: hifi.colorSchemes.dark; + text: "TAKE IT OFF" + onClicked: wearableDeleted(root.avatarName, getCurrentWearable().id); + enabled: wearablesCombobox.model.count !== 0 + anchors.verticalCenter: scalespinner.verticalCenter + } + } + + } + } + + DialogButtons { + anchors.bottom: parent.bottom + anchors.bottomMargin: 30 + anchors.left: parent.left + anchors.leftMargin: 30 + anchors.right: parent.right + anchors.rightMargin: 30 + + yesText: "SAVE" + noText: "CANCEL" + + onYesClicked: function() { + root.close(true); + } + + onNoClicked: function() { + root.close(false); + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml b/interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml new file mode 100644 index 0000000000..9d9db010fb --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml @@ -0,0 +1,58 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" + +ShadowRectangle { + id: header + anchors.left: parent.left + anchors.right: parent.right + height: 60 + + property alias pageTitle: title.text + property alias avatarIconVisible: avatarIcon.visible + property alias settingsButtonVisible: settingsButton.visible + + signal settingsClicked; + + AvatarAppStyle { + id: style + } + + color: style.colors.lightGrayBackground + + HiFiGlyphs { + id: avatarIcon + anchors.left: parent.left + anchors.leftMargin: 23 + anchors.verticalCenter: header.verticalCenter + + size: 38 + text: "<" + } + + // TextStyle6 + RalewaySemiBold { + id: title + size: 22; + anchors.left: avatarIcon.visible ? avatarIcon.right : avatarIcon.left + anchors.leftMargin: 4 + anchors.verticalCenter: avatarIcon.verticalCenter + text: 'Avatar' + } + + HiFiGlyphs { + id: settingsButton + anchors.right: parent.right + anchors.rightMargin: 30 + anchors.verticalCenter: avatarIcon.verticalCenter + text: "&" + + MouseArea { + id: settingsMouseArea + anchors.fill: parent + onClicked: { + settingsClicked(); + } + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml b/interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml new file mode 100644 index 0000000000..f66c7121cb --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml @@ -0,0 +1,29 @@ +// +// HiFiConstants.qml +// +// Created by Alexander Ivash on 17 Apr 2018 +// Copyright 2018 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.Window 2.2 +import "../../styles-uit" + +QtObject { + readonly property QtObject colors: QtObject { + readonly property color lightGrayBackground: "#f2f2f2" + readonly property color black: "#000000" + readonly property color white: "#ffffff" + readonly property color blueHighlight: "#00b4ef" + readonly property color inputFieldBackground: "#d4d4d4" + readonly property color yellowishOrange: "#ffb017" + readonly property color blueAccent: "#0093c5" + readonly property color greenHighlight: "#1fc6a6" + readonly property color lightGray: "#afafaf" + readonly property color redHighlight: "#ea4c5f" + readonly property color orangeAccent: "#ff6309" + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml new file mode 100644 index 0000000000..970d132ba6 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml @@ -0,0 +1,73 @@ +import QtQuick 2.9 +import QtGraphicalEffects 1.0 + +Item { + width: 92 + height: 92 + property alias wearableIndicator: indicator + + property int wearablesCount: 0 + onWearablesCountChanged: { + console.debug('AvatarThumbnail: wearablesCount = ', wearablesCount) + } + + property alias dropShadowRadius: avatarImage.dropShadowRadius + property alias dropShadowHorizontalOffset: avatarImage.dropShadowHorizontalOffset + property alias dropShadowVerticalOffset: avatarImage.dropShadowVerticalOffset + + property url externalAvatarThumbnailUrl; + property var avatarUrl; + property alias border: avatarImage.border + + ShadowImage { + id: avatarImage + anchors.fill: parent + radius: 5 + fillMode: Image.PreserveAspectCrop + + Binding on source { + when: avatarUrl !== '' + value: avatarUrl + } + onSourceChanged: { + console.debug('avatarImage: source = ', source); + } + + visible: avatarImage.status !== Image.Loading && avatarImage.status !== Image.Error + } + + ShadowImage { + id: customAvatarImage + anchors.fill: avatarImage; + visible: avatarUrl === '' || avatarImage.status === Image.Error + source: externalAvatarThumbnailUrl + } + + ShadowRectangle { + anchors.fill: parent; + color: 'white' + visible: avatarImage.status === Image.Loading + radius: avatarImage.radius + border.width: avatarImage.border.width + border.color: avatarImage.border.color + + dropShadowRadius: avatarImage.dropShadowRadius; + dropShadowHorizontalOffset: avatarImage.dropShadowHorizontalOffset + dropShadowVerticalOffset: avatarImage.dropShadowVerticalOffset + + Spinner { + id: spinner + visible: parent.visible + anchors.fill: parent; + } + } + + AvatarWearablesIndicator { + id: indicator + anchors.left: avatarImage.left + anchors.bottom: avatarImage.bottom + anchors.leftMargin: 57 + wearablesCount: parent.wearablesCount + visible: parent.wearablesCount !== 0 + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml b/interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml new file mode 100644 index 0000000000..cb73e9fe71 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml @@ -0,0 +1,46 @@ +import QtQuick 2.9 +import "../../controls-uit" +import "../../styles-uit" + +ShadowRectangle { + property int wearablesCount: 0 + + dropShadowRadius: 4 + dropShadowHorizontalOffset: 0 + dropShadowVerticalOffset: 0 + + width: 46.5 + height: 46.5 + radius: width / 2 + + AvatarAppStyle { + id: style + } + + color: style.colors.greenHighlight + + HiFiGlyphs { + width: 26.5 + height: 13.8 + anchors.top: parent.top + anchors.topMargin: 10 + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + text: "\ue02e" + } + + Item { + width: 46.57 + height: 23 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 2.76 + + // TextStyle2 + RalewayBold { + size: 15; + anchors.horizontalCenter: parent.horizontalCenter + text: wearablesCount + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml b/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml new file mode 100644 index 0000000000..931a041747 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml @@ -0,0 +1,228 @@ +import QtQuick 2.9 + +ListModel { + id: model + function extractMarketId(avatarUrl) { + + var guidRegexp = '([A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12})'; + + var regexp = new RegExp(guidRegexp,["i"]); + var match = regexp.exec(avatarUrl); + if (match !== null) { + return match[1]; + } + + return ''; + } + + function makeMarketItemUrl(avatarUrl) { + var marketItemUrl = "https://highfidelity.com/marketplace/items/%marketId%" + .split('%marketId%').join(extractMarketId(avatarUrl)); + + return marketItemUrl; + } + + function makeThumbnailUrl(avatarUrl) { + var marketId = extractMarketId(avatarUrl); + if(marketId === '') + return ''; + + var avatarThumbnailUrl = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/%marketId%/large/hifi-mp-%marketId%.jpg" + .split('%marketId%').join(marketId); + + return avatarThumbnailUrl; + } + + function makeAvatarObject(avatar, avatarName) { + var avatarThumbnailUrl = makeThumbnailUrl(avatar.avatarUrl); + + return { + 'name' : avatarName, + 'avatarScale' : avatar.avatarScale, + 'thumbnailUrl' : avatarThumbnailUrl, + 'avatarUrl' : avatar.avatarUrl, + 'wearables' : avatar.avatarEntites ? avatar.avatarEntites : [], + 'attachments' : avatar.attachments ? avatar.attachments : [], + 'entry' : avatar, + 'getMoreAvatars' : false + }; + + } + + function addAvatarEntry(avatar, avatarName) { + var avatarEntry = makeAvatarObject(avatar, avatarName); + append(avatarEntry); + + return allAvatars.count - 1; + } + + function populate(bookmarks) { + clear(); + for(var avatarName in bookmarks) { + var avatar = bookmarks[avatarName]; + var avatarEntry = makeAvatarObject(avatar, avatarName); + + append(avatarEntry); + } + } + + function arraysAreEqual(a1, a2, comparer) { + if(Array.isArray(a1) && Array.isArray(a2)) { + if(a1.length !== a2.length) { + return false; + } + + for(var i = 0; i < a1.length; ++i) { + if(!comparer(a1[i], a2[i])) { + return false; + } + } + } else if(Array.isArray(a1)) { + return a1.length === 0; + } else if(Array.isArray(a2)) { + return a2.length === 0; + } + + return true; + } + + function modelsAreEqual(m1, m2, comparer) { + if(m1.count !== m2.count) { + return false; + } + + for(var i = 0; i < m1.count; ++i) { + var e1 = m1.get(i); + + var allDifferent = true; + + // it turns out order of wearables can randomly change so make position-independent comparison here + for(var j = 0; j < m2.count; ++j) { + var e2 = m2.get(j); + + if(comparer(e1, e2)) { + allDifferent = false; + break; + } + } + + if(allDifferent) { + return false; + } + } + + return true; + } + + function compareNumericObjects(o1, o2) { + if(o1 === undefined && o2 !== undefined) + return false; + if(o1 !== undefined && o2 === undefined) + return false; + + for(var prop in o1) { + if(o1.hasOwnProperty(prop) && o2.hasOwnProperty(prop)) { + var v1 = o1[prop]; + var v2 = o2[prop]; + + + if(v1 !== v2 && Math.round(v1 * 500) != Math.round(v2 * 500)) { + return false; + } + } + } + + return true; + } + + function compareObjects(o1, o2, props, arrayProp) { + for(var i = 0; i < props.length; ++i) { + var prop = props[i]; + var propertyName = prop.propertyName; + var comparer = prop.comparer; + + var o1Value = arrayProp ? o1[arrayProp][propertyName] : o1[propertyName]; + var o2Value = arrayProp ? o2[arrayProp][propertyName] : o2[propertyName]; + + if(comparer) { + if(comparer(o1Value, o2Value) === false) { + return false; + } + } else { + if(JSON.stringify(o1Value) !== JSON.stringify(o2Value)) { + return false; + } + } + } + + return true; + } + + function compareWearables(w1, w2) { + return compareObjects(w1, w2, [{'propertyName' : 'modelURL'}, + {'propertyName' : 'parentJointIndex'}, + {'propertyName' : 'marketplaceID'}, + {'propertyName' : 'itemName'}, + {'propertyName' : 'script'}, + {'propertyName' : 'localPosition', 'comparer' : compareNumericObjects}, + {'propertyName' : 'localRotationAngles', 'comparer' : compareNumericObjects}, + {'propertyName' : 'dimensions', 'comparer' : compareNumericObjects}], 'properties') + } + + function compareAttachments(a1, a2) { + return compareObjects(a1, a2, [{'propertyName' : 'position', 'comparer' : compareNumericObjects}, + {'propertyName' : 'orientation'}, + {'propertyName' : 'parentJointIndex'}, + {'propertyName' : 'modelurl'}]) + } + + function findAvatarIndexByValue(avatar) { + + var index = -1; + + // 2DO: find better way of determining selected avatar in bookmarks + for(var i = 0; i < allAvatars.count; ++i) { + var thesame = true; + var bookmarkedAvatar = allAvatars.get(i); + + if(bookmarkedAvatar.avatarUrl !== avatar.avatarUrl) + continue; + + if(bookmarkedAvatar.avatarScale !== avatar.avatarScale) + continue; + + if(!modelsAreEqual(bookmarkedAvatar.attachments, avatar.attachments, compareAttachments)) { + continue; + } + + if(!modelsAreEqual(bookmarkedAvatar.wearables, avatar.wearables, compareWearables)) { + continue; + } + + if(thesame) { + index = i; + break; + } + } + + return index; + } + + function findAvatarIndex(avatarName) { + for(var i = 0; i < count; ++i) { + if(get(i).name === avatarName) { + return i; + } + } + return -1; + } + + function findAvatar(avatarName) { + var avatarIndex = findAvatarIndex(avatarName); + if(avatarIndex === -1) + return undefined; + + return get(avatarIndex); + } + +} diff --git a/interface/resources/qml/hifi/avatarapp/BlueButton.qml b/interface/resources/qml/hifi/avatarapp/BlueButton.qml new file mode 100644 index 0000000000..e668951517 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/BlueButton.qml @@ -0,0 +1,15 @@ +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit + +HifiControlsUit.Button { + HifiConstants { + id: hifi + } + + width: Math.max(hifi.dimensions.buttonWidth, implicitTextWidth + 20) + fontSize: 18 + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.light; + height: 40 +} diff --git a/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml b/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml new file mode 100644 index 0000000000..ab995e79e8 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml @@ -0,0 +1,184 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Rectangle { + id: root; + visible: false; + anchors.fill: parent; + color: Qt.rgba(0, 0, 0, 0.5); + z: 999; + + property string titleText: 'Create Favorite' + property string favoriteNameText: favoriteName.text + property string avatarImageUrl: null + property int wearablesCount: 0 + + property string button1color: hifi.buttons.noneBorderlessGray; + property string button1text: 'CANCEL' + property string button2color: hifi.buttons.blue; + property string button2text: 'CONFIRM' + + property var avatars; + property var onSaveClicked; + property var onCancelClicked; + + function open(wearables, thumbnail) { + favoriteName.text = ''; + favoriteName.forceActiveFocus(); + + avatarImageUrl = thumbnail; + wearablesCount = wearables; + + visible = true; + } + + function close() { + console.debug('closing'); + visible = false; + } + + HifiConstants { + id: hifi + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + + Rectangle { + id: mainContainer; + width: Math.max(parent.width * 0.8, 400) + property int margin: 30; + + height: childrenRect.height + margin * 2 + onHeightChanged: { + console.debug('mainContainer: height = ', height) + } + + anchors.centerIn: parent + + color: "white" + + // TextStyle1 + RalewaySemiBold { + id: title + size: 24; + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 30 + anchors.leftMargin: 30 + anchors.rightMargin: 30 + + text: root.titleText + } + + Item { + id: contentContainer + width: parent.width - 50 + height: childrenRect.height + + anchors.top: title.bottom + anchors.topMargin: 20 + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + + AvatarThumbnail { + id: avatarThumbnail + avatarUrl: avatarImageUrl + onAvatarUrlChanged: { + console.debug('CreateFavoritesDialog: onAvatarUrlChanged: ', avatarUrl); + } + + wearablesCount: avatarWearablesCount + } + + InputTextStyle4 { + id: favoriteName + anchors.right: parent.right + height: 40 + anchors.left: avatarThumbnail.right + anchors.leftMargin: 44 + anchors.verticalCenter: avatarThumbnail.verticalCenter + placeholderText: "Enter Favorite Name" + + RalewayRegular { + id: wrongName + anchors.top: parent.bottom; + anchors.topMargin: 2 + + anchors.left: parent.left + anchors.right: parent.right; + anchors.rightMargin: -contentContainer.anchors.rightMargin // allow text to render beyond favorite input text + wrapMode: Text.WordWrap + text: 'Favorite name exists. Overwrite existing favorite?' + size: 15 + color: 'red' + visible: { + for(var i = 0; i < avatars.count; ++i) { + var avatarName = avatars.get(i).name; + if(avatarName === favoriteName.text) { + return true; + } + } + + return false; + } + } + } + } + + DialogButtons { + anchors.top: contentContainer.bottom + anchors.topMargin: 20 + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 30 + + yesButton.enabled: favoriteNameText !== '' + yesText: root.button2text + noText: root.button1text + + Binding on yesButton.text { + when: wrongName.visible + value: "OVERWRITE"; + } + + Binding on yesButton.color { + when: wrongName.visible + value: hifi.buttons.red; + } + + Binding on yesButton.colorScheme { + when: wrongName.visible + value: hifi.colorSchemes.dark; + } + + onYesClicked: function() { + if(onSaveClicked) { + onSaveClicked(); + } else { + root.close(); + } + } + + onNoClicked: function() { + if(onCancelClicked) { + onCancelClicked(); + } else { + root.close(); + } + } + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/DialogButtons.qml b/interface/resources/qml/hifi/avatarapp/DialogButtons.qml new file mode 100644 index 0000000000..46c17bb4dc --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/DialogButtons.qml @@ -0,0 +1,41 @@ +import QtQuick 2.9 + +Row { + id: root + property string yesText; + property string noText; + property var onYesClicked; + property var onNoClicked; + + property alias yesButton: yesButton + property alias noButton: noButton + + height: childrenRect.height + layoutDirection: Qt.RightToLeft + + spacing: 30 + + BlueButton { + id: yesButton; + text: yesText; + onClicked: { + console.debug('bluebutton.clicked', onYesClicked); + + if(onYesClicked) { + onYesClicked(); + } + } + } + + WhiteButton { + id: noButton + text: noText; + onClicked: { + console.debug('whitebutton.clicked', onNoClicked); + + if(onNoClicked) { + onNoClicked(); + } + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/InputField.qml b/interface/resources/qml/hifi/avatarapp/InputField.qml new file mode 100644 index 0000000000..905518ef0f --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/InputField.qml @@ -0,0 +1,48 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit + +TextField { + id: textField + + property bool error: false; + text: 'ThisIsDisplayName' + + states: [ + State { + name: "hovered" + when: textField.hovered && !textField.focus && !textField.error; + PropertyChanges { target: background; color: '#afafaf' } + }, + State { + name: "focused" + when: textField.focus && !textField.error + PropertyChanges { target: background; color: '#f2f2f2' } + PropertyChanges { target: background; border.color: '#00b4ef' } + }, + State { + name: "error" + when: textField.error + PropertyChanges { target: background; color: '#f2f2f2' } + PropertyChanges { target: background; border.color: '#e84e62' } + } + ] + + background: Rectangle { + id: background + implicitWidth: 200 + implicitHeight: 40 + color: '#d4d4d4' + border.color: '#afafaf' + border.width: 1 + radius: 2 + } + + HiFiGlyphs { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + size: 36 + text: "\ue00d" + } +} diff --git a/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml b/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml new file mode 100644 index 0000000000..4b868b47ce --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml @@ -0,0 +1,16 @@ +import "../../controls-uit" as HifiControlsUit +import "../../styles-uit" + +import QtQuick 2.0 +import QtQuick.Controls 2.2 + +HifiControlsUit.TextField { + id: control + font.family: "Fira Sans" + font.pixelSize: 15; + implicitHeight: 40 + + AvatarAppStyle { + id: style + } +} diff --git a/interface/resources/qml/hifi/avatarapp/MessageBox.qml b/interface/resources/qml/hifi/avatarapp/MessageBox.qml new file mode 100644 index 0000000000..f2df0b5199 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/MessageBox.qml @@ -0,0 +1,184 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Rectangle { + id: root; + visible: false; + anchors.fill: parent; + color: Qt.rgba(0, 0, 0, 0.5); + z: 999; + + property string titleText: '' + property string bodyText: '' + property alias inputText: input; + + property string imageSource: null + onImageSourceChanged: { + console.debug('imageSource = ', imageSource) + } + + property string button1color: hifi.buttons.noneBorderlessGray; + property string button1text: '' + property string button2color: hifi.buttons.blue; + property string button2text: '' + + property var onButton2Clicked; + property var onButton1Clicked; + property var onLinkClicked; + + function open() { + visible = true; + } + + function close() { + visible = false; + + onButton1Clicked = null; + onButton2Clicked = null; + button1text = ''; + button2text = ''; + imageSource = null; + inputText.visible = false; + inputText.placeholderText = ''; + inputText.text = ''; + } + + HifiConstants { + id: hifi + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + + Rectangle { + id: mainContainer; + width: Math.max(parent.width * 0.8, 400) + property int margin: 30; + + height: childrenRect.height + margin * 2 + onHeightChanged: { + console.debug('mainContainer: height = ', height) + } + + anchors.centerIn: parent + + color: "white" + + // TextStyle1 + RalewaySemiBold { + id: title + size: 24; + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 30 + anchors.leftMargin: 30 + anchors.rightMargin: 30 + + text: root.titleText + elide: Qt.ElideRight + } + + Column { + id: contentContainer + spacing: 15 + + anchors.top: title.bottom + anchors.topMargin: 10 + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + + InputTextStyle4 { + id: input + visible: false + height: visible ? implicitHeight : 0 + + anchors.left: parent.left; + anchors.right: parent.right; + } + + // TextStyle3 + RalewayRegular { + id: body + + AvatarAppStyle { + id: style + } + + size: 18 + text: root.bodyText; + linkColor: style.colors.blueHighlight + anchors.left: parent.left; + anchors.right: parent.right; + height: paintedHeight; + verticalAlignment: Text.AlignTop; + wrapMode: Text.WordWrap; + + onLinkActivated: { + if(onLinkClicked) + onLinkClicked(link); + } + } + + Image { + id: image + Binding on height { + when: imageSource === null + value: 0 + } + + anchors.left: parent.left; + anchors.right: parent.right; + + Binding on source { + when: imageSource !== null + value: imageSource + } + + visible: imageSource !== null ? true : false + } + } + + DialogButtons { + id: buttons + + anchors.top: contentContainer.bottom + anchors.topMargin: 30 + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 30 + + yesButton.enabled: !input.visible || input.text.length !== 0 + yesText: root.button2text + noText: root.button1text + + onYesClicked: function() { + if(onButton2Clicked) { + onButton2Clicked(); + } else { + root.close(); + } + } + + onNoClicked: function() { + if(onButton1Clicked) { + onButton1Clicked(); + } else { + root.close(); + } + } + } + + } +} diff --git a/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml b/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml new file mode 100644 index 0000000000..125b30fa95 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml @@ -0,0 +1,122 @@ +import QtQuick 2.5 + +MessageBox { + id: popup + + function showSpecifyAvatarUrl(callback, linkCallback) { + popup.onButton2Clicked = callback; + popup.titleText = 'Specify Avatar URL' + popup.bodyText = 'This will not overwrite your existing favorite if you are wearing one.
' + + '' + + 'Learn to make a custom avatar by opening this link on your desktop.' + + '' + popup.inputText.visible = true; + popup.inputText.placeholderText = 'Enter Avatar Url'; + popup.button1text = 'CANCEL'; + popup.button2text = 'CONFIRM'; + + popup.onButton2Clicked = function() { + if(callback) + callback(); + + popup.close(); + } + + popup.onLinkClicked = function(link) { + if(linkCallback) + linkCallback(link); + } + + popup.open(); + popup.inputText.forceActiveFocus(); + } + + property url getWearablesUrl: '../../../images/avatarapp/AvatarIsland.jpg' + + function showGetWearables(callback, linkCallback) { + popup.button2text = 'AvatarIsland' + popup.button1text = 'CANCEL' + popup.titleText = 'Get Wearables' + popup.bodyText = 'Buy wearables from Marketplace' + '
' + + 'Wear wearables from My Purchases' + '
' + + 'You can visit the domain “AvatarIsland” to get wearables' + + popup.imageSource = getWearablesUrl; + popup.onButton2Clicked = function() { + popup.close(); + + if(callback) + callback(); + } + + popup.onLinkClicked = function(link) { + popup.close(); + + if(linkCallback) + linkCallback(link); + } + + popup.open(); + } + + function showDeleteFavorite(favoriteName, callback) { + popup.titleText = 'Delete Favorite: {AvatarName}'.replace('{AvatarName}', favoriteName) + popup.bodyText = 'This will delete your favorite. You will retain access to the wearables and avatar that made up the favorite from My Purchases.' + popup.imageSource = null; + popup.button1text = 'CANCEL' + popup.button2text = 'DELETE' + + popup.onButton2Clicked = function() { + popup.close(); + + if(callback) + callback(); + } + popup.open(); + } + + function showLoadFavorite(favoriteName, callback) { + popup.button2text = 'CONFIRM' + popup.button1text = 'CANCEL' + popup.titleText = 'Load Favorite: {AvatarName}'.replace('{AvatarName}', favoriteName) + popup.bodyText = 'This will switch your current avatar and wearables that you are wearing with a new avatar and wearables.' + popup.imageSource = null; + popup.onButton2Clicked = function() { + popup.close(); + + if(callback) + callback(); + } + popup.open(); + } + + property url getAvatarsUrl: '../../../images/avatarapp/BodyMart.PNG' + + function showBuyAvatars(callback, linkCallback) { + popup.button2text = 'BodyMart' + popup.button1text = 'CANCEL' + popup.titleText = 'Get Avatars' + + popup.bodyText = 'Buy avatars from Marketplace' + '
' + + 'Wear avatars from My Purchases' + '
' + + 'You can visit the domain “BodyMart” to get avatars' + + popup.imageSource = getAvatarsUrl; + popup.onButton2Clicked = function() { + popup.close(); + + if(callback) + callback(); + } + + popup.onLinkClicked = function(link) { + popup.close(); + + if(linkCallback) + linkCallback(link); + } + + popup.open(); + } +} + diff --git a/interface/resources/qml/hifi/avatarapp/PageIndicator.qml b/interface/resources/qml/hifi/avatarapp/PageIndicator.qml new file mode 100644 index 0000000000..28575e4496 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/PageIndicator.qml @@ -0,0 +1,44 @@ +import QtQuick 2.9 + +ShadowGlyph { + id: indicator + property bool isPrevious: true; + property bool hasNext: false + property bool hasPrev: false + property bool isEnabled: isPrevious ? hasPrev : hasNext + signal clicked; + + states: [ + State { + name: "hovered" + when: pageIndicatorMouseArea.containsMouse; + PropertyChanges { target: pageIndicatorMouseArea; anchors.bottomMargin: -5 } + PropertyChanges { target: indicator; y: -5 } + PropertyChanges { target: indicator; dropShadowVerticalOffset: 9 } + } + ] + + Behavior on y { + NumberAnimation { + duration: 100 + } + } + + text: isPrevious ? "E" : "D"; + width: 40 + height: 40 + font.pixelSize: 100 + color: isEnabled ? 'black' : 'gray' + visible: hasNext || hasPrev + horizontalAlignment: Text.AlignHCenter + + MouseArea { + id: pageIndicatorMouseArea + anchors.fill: parent + enabled: isEnabled + hoverEnabled: enabled + onClicked: { + parent.clicked(); + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/RoundImage.qml b/interface/resources/qml/hifi/avatarapp/RoundImage.qml new file mode 100644 index 0000000000..f941cd0f1d --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/RoundImage.qml @@ -0,0 +1,36 @@ +import QtQuick 2.0 + +Item { + + property alias border: borderRectangle.border + property alias source: image.source + property alias fillMode: image.fillMode + property alias radius: mask.radius + property alias status: image.status + property alias progress: image.progress + + Image { + id: image + anchors.fill: parent + anchors.margins: borderRectangle.border.width + } + + Rectangle { + id: mask + anchors.fill: image + } + + TransparencyMask { + anchors.fill: image + source: image + maskSource: mask + } + + Rectangle { + id: borderRectangle + anchors.fill: parent + + radius: mask.radius + color: "transparent" + } +} diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml new file mode 100644 index 0000000000..f996bdfd03 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -0,0 +1,353 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Rectangle { + id: root + + color: 'white' + visible: false; + + signal scaleChanged(real scale); + + property alias onSaveClicked: dialogButtons.onYesClicked + property alias onCancelClicked: dialogButtons.onNoClicked + + property real scaleValue: scaleSlider.value / 10 + property alias dominantHandIsLeft: leftHandRadioButton.checked + property alias avatarCollisionsOn: collisionsEnabledRadiobutton.checked + property alias avatarAnimationJSON: avatarAnimationUrlInputText.text + property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text + + property real avatarScaleBackup; + function open(settings, avatarScale) { + console.debug('Settings.qml: open: ', JSON.stringify(settings, 0, 4)); + avatarScaleBackup = avatarScale; + + scaleSlider.notify = false; + scaleSlider.value = Math.round(avatarScale * 10); + scaleSlider.notify = true;; + + if(settings.dominantHand === 'left') { + leftHandRadioButton.checked = true; + } else { + rightHandRadioButton.checked = true; + } + + if(settings.collisionsEnabled) { + collisionsEnabledRadiobutton.checked = true; + } else { + collisionsDisabledRadioButton.checked = true; + } + + avatarAnimationJSON = settings.animGraphUrl; + avatarCollisionSoundUrl = settings.collisionSoundUrl; + + visible = true; + } + + function close() { + visible = false + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + + Item { + anchors.left: parent.left + anchors.leftMargin: 27 + anchors.top: parent.top + anchors.topMargin: 25 + anchors.right: parent.right + anchors.rightMargin: 32 + anchors.bottom: parent.bottom + anchors.bottomMargin: 57 + + RowLayout { + id: avatarScaleRow + anchors.left: parent.left + anchors.right: parent.right + + spacing: 17 + + // TextStyle9 + RalewaySemiBold { + size: 17; + text: "Avatar Scale" + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + } + + RowLayout { + anchors.verticalCenter: parent.verticalCenter + Layout.fillWidth: true + + spacing: 0 + + HiFiGlyphs { + size: 30 + text: 'T' + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors.verticalCenter: parent.verticalCenter + } + + HifiControlsUit.Slider { + id: scaleSlider + property bool notify: false; + + from: 1 + to: 40 + + onValueChanged: { + console.debug('value changed: ', value); + if(notify) { + console.debug('notifying.. '); + root.scaleChanged(value / 10); + } + } + + anchors.verticalCenter: parent.verticalCenter + Layout.fillWidth: true + + // TextStyle9 + RalewaySemiBold { + size: 17; + anchors.left: scaleSlider.left + anchors.leftMargin: 5 + anchors.top: scaleSlider.bottom + anchors.topMargin: 2 + text: String(scaleSlider.from / 10) + 'x' + } + + // TextStyle9 + RalewaySemiBold { + size: 17; + anchors.right: scaleSlider.right + anchors.rightMargin: 5 + anchors.top: scaleSlider.bottom + anchors.topMargin: 2 + text: String(scaleSlider.to / 10) + 'x' + } + } + + HiFiGlyphs { + size: 40 + text: 'T' + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + ShadowRectangle { + width: 37 + height: 28 + AvatarAppStyle { + id: style + } + + gradient: Gradient { + GradientStop { position: 0.0; color: style.colors.blueHighlight } + GradientStop { position: 1.0; color: style.colors.blueAccent } + } + + radius: 3 + + RalewaySemiBold { + color: 'white' + anchors.centerIn: parent + text: "1x" + size: 18 + } + + MouseArea { + anchors.fill: parent + onClicked: { + scaleSlider.value = 10 + } + } + } + } + + GridLayout { + id: handAndCollisions + anchors.top: avatarScaleRow.bottom + anchors.topMargin: 39 + anchors.left: parent.left + anchors.right: parent.right + + rows: 2 + rowSpacing: 25 + + columns: 3 + + // TextStyle9 + RalewaySemiBold { + size: 17; + Layout.row: 0 + Layout.column: 0 + + text: "Dominant Hand" + } + + ButtonGroup { + id: leftRight + } + + HifiControlsUit.RadioButton { + id: leftHandRadioButton + + Layout.row: 0 + Layout.column: 1 + Layout.leftMargin: -40 + + ButtonGroup.group: leftRight + checked: true + + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + text: "Left" + boxSize: 20 + } + + HifiControlsUit.RadioButton { + id: rightHandRadioButton + + Layout.row: 0 + Layout.column: 2 + Layout.rightMargin: 20 + + ButtonGroup.group: leftRight + + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + text: "Right" + boxSize: 20 + } + + // TextStyle9 + RalewaySemiBold { + size: 17; + Layout.row: 1 + Layout.column: 0 + + text: "Avatar Collisions" + } + + ButtonGroup { + id: onOff + } + + HifiControlsUit.RadioButton { + id: collisionsEnabledRadiobutton + + Layout.row: 1 + Layout.column: 1 + Layout.leftMargin: -40 + ButtonGroup.group: onOff + + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + checked: true + + text: "ON" + boxSize: 20 + } + + HifiConstants { + id: hifi + } + + HifiControlsUit.RadioButton { + id: collisionsDisabledRadioButton + + Layout.row: 1 + Layout.column: 2 + Layout.rightMargin: 20 + + ButtonGroup.group: onOff + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + + text: "OFF" + boxSize: 20 + } + } + + ColumnLayout { + id: avatarAnimationLayout + anchors.top: handAndCollisions.bottom + anchors.topMargin: 25 + anchors.left: parent.left + anchors.right: parent.right + + spacing: 4 + + // TextStyle9 + RalewaySemiBold { + size: 17; + text: "Avatar Animation JSON" + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + } + + InputTextStyle4 { + id: avatarAnimationUrlInputText + font.pixelSize: 17 + anchors.left: parent.left + anchors.right: parent.right + placeholderText: 'user\\file\\dir' + } + } + + ColumnLayout { + id: avatarCollisionLayout + anchors.top: avatarAnimationLayout.bottom + anchors.topMargin: 25 + anchors.left: parent.left + anchors.right: parent.right + + spacing: 4 + + // TextStyle9 + RalewaySemiBold { + size: 17; + text: "Avatar collision sound URL (optional)" + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + } + + InputTextStyle4 { + id: avatarCollisionSoundUrlInputText + font.pixelSize: 17 + anchors.left: parent.left + anchors.right: parent.right + placeholderText: 'https://hifi-public.s3.amazonaws.com/sounds/Collisions-' + } + } + + DialogButtons { + id: dialogButtons + anchors.right: parent.right + anchors.bottom: parent.bottom + + yesText: "SAVE" + noText: "CANCEL" + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml b/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml new file mode 100644 index 0000000000..c2d84bb371 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml @@ -0,0 +1,29 @@ +import "../../styles-uit" +import QtQuick 2.9 +import QtGraphicalEffects 1.0 + +Item { + property alias text: glyph.text + property alias font: glyph.font + property alias color: glyph.color + property alias horizontalAlignment: glyph.horizontalAlignment + property alias dropShadowRadius: shadow.radius + property alias dropShadowHorizontalOffset: shadow.horizontalOffset + property alias dropShadowVerticalOffset: shadow.verticalOffset + + HiFiGlyphs { + id: glyph + width: parent.width + height: parent.height + } + + DropShadow { + id: shadow + anchors.fill: glyph + radius: 4 + horizontalOffset: 0 + verticalOffset: 4 + color: Qt.rgba(0, 0, 0, 0.25) + source: glyph + } +} diff --git a/interface/resources/qml/hifi/avatarapp/ShadowImage.qml b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml new file mode 100644 index 0000000000..3995446e49 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml @@ -0,0 +1,32 @@ +import "../../styles-uit" +import QtQuick 2.9 +import QtGraphicalEffects 1.0 + +Item { + property alias source: image.source + property alias dropShadowRadius: shadow.radius + property alias dropShadowHorizontalOffset: shadow.horizontalOffset + property alias dropShadowVerticalOffset: shadow.verticalOffset + property alias radius: image.radius + property alias border: image.border + property alias status: image.status + property alias progress: image.progress + property alias fillMode: image.fillMode + + RoundImage { + id: image + width: parent.width + height: parent.height + radius: 6 + } + + DropShadow { + id: shadow + anchors.fill: image + radius: 6 + horizontalOffset: 0 + verticalOffset: 3 + color: Qt.rgba(0, 0, 0, 0.25) + source: image + } +} diff --git a/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml b/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml new file mode 100644 index 0000000000..741fce3d8d --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml @@ -0,0 +1,30 @@ +import "../../styles-uit" +import QtQuick 2.9 +import QtGraphicalEffects 1.0 + +Item { + property alias color: rectangle.color + property alias gradient: rectangle.gradient + property alias border: rectangle.border + property alias radius: rectangle.radius + property alias dropShadowRadius: shadow.radius + property alias dropShadowHorizontalOffset: shadow.horizontalOffset + property alias dropShadowVerticalOffset: shadow.verticalOffset + property alias dropShadowOpacity: shadow.opacity + + Rectangle { + id: rectangle + width: parent.width + height: parent.height + } + + DropShadow { + id: shadow + anchors.fill: rectangle + radius: 6 + horizontalOffset: 0 + verticalOffset: 3 + color: Qt.rgba(0, 0, 0, 0.25) + source: rectangle + } +} diff --git a/interface/resources/qml/hifi/avatarapp/Spinner.qml b/interface/resources/qml/hifi/avatarapp/Spinner.qml new file mode 100644 index 0000000000..3fc331346d --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/Spinner.qml @@ -0,0 +1,7 @@ +import QtQuick 2.5 +import QtWebEngine 1.5 + +AnimatedImage { + source: "../../../icons/loader-snake-64-w.gif" + playing: visible +} diff --git a/interface/resources/qml/hifi/avatarapp/SquareLabel.qml b/interface/resources/qml/hifi/avatarapp/SquareLabel.qml new file mode 100644 index 0000000000..3c5463e1dd --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/SquareLabel.qml @@ -0,0 +1,29 @@ +import "../../styles-uit" +import QtQuick 2.9 +import QtGraphicalEffects 1.0 + +ShadowRectangle { + width: 44 + height: 28 + AvatarAppStyle { + id: style + } + + gradient: Gradient { + GradientStop { position: 0.0; color: style.colors.blueHighlight } + GradientStop { position: 1.0; color: style.colors.blueAccent } + } + + property alias glyphText: glyph.text + property alias glyphRotation: glyph.rotation + property alias glyphSize: glyph.size + + radius: 3 + + HiFiGlyphs { + id: glyph + color: 'white' + anchors.centerIn: parent + size: 30 + } +} diff --git a/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml b/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml new file mode 100644 index 0000000000..4884d1e1ad --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml @@ -0,0 +1,43 @@ +import QtQuick 2.0 + +Item { + property alias source: sourceImage.sourceItem + property alias maskSource: sourceMask.sourceItem + + anchors.fill: parent + ShaderEffectSource { + id: sourceMask + smooth: true + hideSource: true + } + ShaderEffectSource { + id: sourceImage + hideSource: true + } + + ShaderEffect { + id: maskEffect + anchors.fill: parent + + property variant source: sourceImage + property variant mask: sourceMask + + fragmentShader: { +" + varying highp vec2 qt_TexCoord0; + uniform lowp sampler2D source; + uniform lowp sampler2D mask; + void main() { + + highp vec4 maskColor = texture2D(mask, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); + highp vec4 sourceColor = texture2D(source, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); + + if(maskColor.a > 0.0) + gl_FragColor = sourceColor; + else + gl_FragColor = maskColor; + } +" + } + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarapp/Vector3.qml b/interface/resources/qml/hifi/avatarapp/Vector3.qml new file mode 100644 index 0000000000..33e82fe0ee --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/Vector3.qml @@ -0,0 +1,65 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Row { + id: root + width: parent.width + height: xspinner.controlHeight + + property int spinboxSpace: 10 + property int spinboxWidth: (parent.width - 2 * spinboxSpace) / 3 + property color backgroundColor: "darkgray" + + property int decimals: 4 + property real realFrom: 0 + property real realTo: 100 + property real realStepSize: 0.0001 + + spacing: spinboxSpace + + property alias xvalue: xspinner.realValue + property alias yvalue: yspinner.realValue + property alias zvalue: zspinner.realValue + + HifiControlsUit.SpinBox { + id: xspinner + width: parent.spinboxWidth + labelInside: "X:" + backgroundColor: parent.backgroundColor + colorLabelInside: hifi.colors.redHighlight + colorScheme: hifi.colorSchemes.light + decimals: root.decimals; + realFrom: root.realFrom + realTo: root.realTo + realStepSize: root.realStepSize + } + + HifiControlsUit.SpinBox { + id: yspinner + width: parent.spinboxWidth + labelInside: "Y:" + backgroundColor: parent.backgroundColor + colorLabelInside: hifi.colors.greenHighlight + colorScheme: hifi.colorSchemes.light + decimals: root.decimals; + realFrom: root.realFrom + realTo: root.realTo + realStepSize: root.realStepSize + } + + HifiControlsUit.SpinBox { + id: zspinner + width: parent.spinboxWidth + labelInside: "Z:" + backgroundColor: parent.backgroundColor + colorLabelInside: hifi.colors.primaryHighlight + colorScheme: hifi.colorSchemes.light + decimals: root.decimals; + realFrom: root.realFrom + realTo: root.realTo + realStepSize: root.realStepSize + } +} diff --git a/interface/resources/qml/hifi/avatarapp/WhiteButton.qml b/interface/resources/qml/hifi/avatarapp/WhiteButton.qml new file mode 100644 index 0000000000..dc729ae097 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/WhiteButton.qml @@ -0,0 +1,15 @@ +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit + +HifiControlsUit.Button { + HifiConstants { + id: hifi + } + + width: Math.max(hifi.dimensions.buttonWidth, implicitTextWidth + 20) + fontSize: 18 + color: hifi.buttons.noneBorderlessGray; + colorScheme: hifi.colorSchemes.light; + height: 40 +} diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 16c1b55930..cac62d3976 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -92,9 +92,9 @@ Rectangle { onBuyResult: { if (result.status !== 'success') { - failureErrorText.text = result.message; + failureErrorText.text = result.data.message; root.activeView = "checkoutFailure"; - UserActivityLogger.commercePurchaseFailure(root.itemId, root.itemAuthor, root.itemPrice, !root.alreadyOwned, result.message); + UserActivityLogger.commercePurchaseFailure(root.itemId, root.itemAuthor, root.itemPrice, !root.alreadyOwned, result.data.message); } else { root.certificateId = result.data.certificate_id; root.itemHref = result.data.download_url; @@ -129,7 +129,7 @@ Rectangle { } onAppInstalled: { - if (appHref === root.itemHref) { + if (appID === root.itemId) { root.isInstalled = true; } } diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index 3e4bae4780..a515c8031f 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -398,7 +398,7 @@ Item { http: root.http; listModelName: root.listModelName; endpoint: "/api/v1/users?filter=connections"; - itemsPerPage: 8; + itemsPerPage: 9; listView: connectionsList; processPage: function (data) { return data.users; @@ -520,7 +520,6 @@ Item { visible: !connectionsLoading.visible; clip: true; model: connectionsModel; - onAtYEndChanged: if (connectionsList.atYEnd && !connectionsList.atYBeginning) { connectionsModel.getNextPage(); } snapMode: ListView.SnapToItem; // Anchors anchors.fill: parent; diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index 9f1d307f0e..b2338d08de 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -67,13 +67,13 @@ Item { } onAppInstalled: { - if (appHref === root.itemHref) { + if (appID === root.itemId) { root.isInstalled = true; } } onAppUninstalled: { - if (appHref === root.itemHref) { + if (appID === root.itemId) { root.isInstalled = false; } } @@ -328,7 +328,15 @@ Item { item.buttonColor = "#E2334D"; item.buttonClicked = function() { sendToPurchases({ method: 'flipCard', closeAll: true }); - sendToPurchases({method: 'updateItemClicked', itemId: root.itemId, itemEdition: root.itemEdition, upgradeUrl: root.upgradeUrl}); + sendToPurchases({ + method: 'updateItemClicked', + itemId: root.itemId, + itemEdition: root.itemEdition, + upgradeUrl: root.upgradeUrl, + itemHref: root.itemHref, + itemType: root.itemType, + isInstalled: root.isInstalled + }); } } } @@ -723,7 +731,7 @@ Item { } HiFiGlyphs { id: rezIcon; - text: (root.buttonGlyph)[itemTypesArray.indexOf(root.itemType)]; + text: root.isInstalled ? "" : (root.buttonGlyph)[itemTypesArray.indexOf(root.itemType)]; anchors.right: rezIconLabel.left; anchors.rightMargin: 2; anchors.verticalCenter: parent.verticalCenter; diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 8a5b1fb0e7..3569ce6767 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -98,7 +98,7 @@ Rectangle { } onAppInstalled: { - root.installedApps = Commerce.getInstalledApps(); + root.installedApps = Commerce.getInstalledApps(appID); } onAppUninstalled: { @@ -551,8 +551,9 @@ Rectangle { HifiModels.PSFListModel { id: purchasesModel; - itemsPerPage: 6; + itemsPerPage: 7; listModelName: 'purchases'; + listView: purchasesContentsList; getPage: function () { console.debug('getPage', purchasesModel.listModelName, root.isShowingMyItems, filterBar.primaryFilter_filterName, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage); Commerce.inventory( @@ -706,7 +707,23 @@ Rectangle { } } } else if (msg.method === "updateItemClicked") { - sendToScript(msg); + if (msg.itemType === "app" && msg.isInstalled) { + lightboxPopup.titleText = "Uninstall App"; + lightboxPopup.bodyText = "The app that you are trying to update is installed.

" + + "If you proceed, the current version of the app will be uninstalled."; + lightboxPopup.button1text = "CANCEL"; + lightboxPopup.button1method = function() { + lightboxPopup.visible = false; + } + lightboxPopup.button2text = "CONFIRM"; + lightboxPopup.button2method = function() { + Commerce.uninstallApp(msg.itemHref); + sendToScript(msg); + }; + lightboxPopup.visible = true; + } else { + sendToScript(msg); + } } else if (msg.method === "giftAsset") { sendAsset.assetName = msg.itemName; sendAsset.assetCertID = msg.certId; @@ -765,14 +782,6 @@ Rectangle { } } } - - - onAtYEndChanged: { - if (purchasesContentsList.atYEnd && !purchasesContentsList.atYBeginning) { - console.log("User scrolled to the bottom of 'Purchases'."); - purchasesModel.getNextPage(); - } - } } Rectangle { diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 3e0a56b4c5..a0c6057b3b 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -212,6 +212,7 @@ Item { HifiModels.PSFListModel { id: transactionHistoryModel; listModelName: "transaction history"; // For debugging. Alternatively, we could specify endpoint for that purpose, even though it's not used directly. + listView: transactionHistory; itemsPerPage: 6; getPage: function () { console.debug('getPage', transactionHistoryModel.listModelName, transactionHistoryModel.currentPageToRetrieve); @@ -346,12 +347,6 @@ Item { } } } - onAtYEndChanged: { - if (transactionHistory.atYEnd && !transactionHistory.atYBeginning) { - console.log("User scrolled to the bottom of 'Recent Activity'."); - transactionHistoryModel.getNextPage(); - } - } } Item { diff --git a/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml b/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml index cb4913f999..861de001d8 100644 --- a/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml +++ b/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml @@ -17,7 +17,7 @@ PreferencesDialog { id: root objectName: "GeneralPreferencesDialog" title: "General Settings" - showCategories: ["User Interface", "HMD", "Snapshots", "Privacy"] + showCategories: ["User Interface", "Mouse Sensitivity", "HMD", "Snapshots", "Privacy"] property var settings: Settings { category: root.objectName property alias x: root.x diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index 1bfa2f6ae0..988502dd91 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -33,7 +33,6 @@ ListModel { // QML fires the following changed handlers even when first instantiating the Item. So we need a guard against firing them too early. property bool initialized: false; - Component.onCompleted: initialized = true; onEndpointChanged: if (initialized) { getFirstPage('delayClear'); } onSortKeyChanged: if (initialized) { getFirstPage('delayClear'); } onSearchFilterChanged: if (initialized) { getFirstPage('delayClear'); } @@ -51,6 +50,8 @@ ListModel { if (!delayedClear) { root.clear(); } currentPageToRetrieve = 1; retrievedAtLeastOnePage = false; + totalPages = 0; + totalEntries = 0; } // Page processing. @@ -58,7 +59,39 @@ ListModel { // Override to return one property of data, and/or to transform the elements. Must return an array of model elements. property var processPage: function (data) { return data; } - property var listView; // Optional. For debugging. + property var listView; // Optional. For debugging, or for having the scroll handler automatically call getNextPage. + property var flickable: listView && (listView.flickableItem || listView); + // 2: get two pages before you need it (i.e. one full page before you reach the end). + // 1: equivalent to paging when reaching end (and not before). + // 0: don't getNextPage on scroll at all here. The application code will do it. + property real pageAhead: 2.0; + function needsEarlyYFetch() { + return flickable + && !flickable.atYBeginning + && (flickable.contentY - flickable.originY) >= (flickable.contentHeight - (pageAhead * flickable.height)); + } + function needsEarlyXFetch() { + return flickable + && !flickable.atXBeginning + && (flickable.contentX - flickable.originX) >= (flickable.contentWidth - (pageAhead * flickable.width)); + } + function getNextPageIfHorizontalScroll() { + if (needsEarlyXFetch()) { getNextPage(); } + } + function getNextPageIfVerticalScroll() { + if (needsEarlyYFetch()) { getNextPage(); } + } + Component.onCompleted: { + initialized = true; + if (flickable && pageAhead > 0.0) { + // Pun: Scrollers are usually one direction or another, such that only one of the following will actually fire. + flickable.contentXChanged.connect(getNextPageIfHorizontalScroll); + flickable.contentYChanged.connect(getNextPageIfVerticalScroll); + } + } + + property int totalPages: 0; + property int totalEntries: 0; // Check consistency and call processPage. function handlePage(error, response) { var processed; @@ -79,8 +112,10 @@ ListModel { if (response.current_page && response.current_page !== currentPageToRetrieve) { // Not all endpoints specify this property. return fail("Mismatched page, expected:" + currentPageToRetrieve); } + totalPages = response.total_pages || 0; + totalEntries = response.total_entries || 0; processed = processPage(response.data || response); - if (response.total_pages && (response.total_pages === currentPageToRetrieve)) { + if (totalPages && (totalPages === currentPageToRetrieve)) { currentPageToRetrieve = -1; } diff --git a/interface/resources/qml/hifi/tablet/ControllerSettings.qml b/interface/resources/qml/hifi/tablet/ControllerSettings.qml index 4ad37b7bc8..135c1379e2 100644 --- a/interface/resources/qml/hifi/tablet/ControllerSettings.qml +++ b/interface/resources/qml/hifi/tablet/ControllerSettings.qml @@ -69,6 +69,7 @@ Item { id: stack initialItem: inputConfiguration property alias messageVisible: imageMessageBox.visible + property string selectedPlugin: "" Rectangle { id: inputConfiguration anchors { @@ -274,6 +275,8 @@ Item { } else { box.label = ""; } + + stack.selectedPlugin = selectedDevice; } Timer { @@ -297,6 +300,12 @@ Item { id: controllerPrefereneces objectName: "TabletControllerPreferences" showCategories: ["VR Movement", "Game Controller", "Sixense Controllers", "Perception Neuron", "Leap Motion"] + categoryProperties: { + "VR Movement" : { + "User real-world height (meters)" : { "anchors.right" : "undefined" }, + "RESET SENSORS" : { "width" : "180", "anchors.left" : "undefined" } + } + } } } } diff --git a/interface/resources/qml/hifi/tablet/EditEntityList.qml b/interface/resources/qml/hifi/tablet/EditEntityList.qml new file mode 100644 index 0000000000..d484885103 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/EditEntityList.qml @@ -0,0 +1,15 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtWebChannel 1.0 +import "../../controls" +import "../toolbars" +import QtGraphicalEffects 1.0 +import "../../controls-uit" as HifiControls +import "../../styles-uit" + + +WebView { + id: entityListToolWebView + url: Paths.defaultScripts + "/system/html/entityList.html" + enabled: true +} diff --git a/interface/resources/qml/hifi/tablet/EditTabView.qml b/interface/resources/qml/hifi/tablet/EditTabView.qml index 9a7958f95c..4ac8755570 100644 --- a/interface/resources/qml/hifi/tablet/EditTabView.qml +++ b/interface/resources/qml/hifi/tablet/EditTabView.qml @@ -9,7 +9,6 @@ import "../../styles-uit" TabBar { id: editTabView - // anchors.fill: parent width: parent.width contentWidth: parent.width padding: 0 @@ -34,7 +33,7 @@ TabBar { width: parent.width clip: true - contentHeight: createEntitiesFlow.height + importButton.height + assetServerButton.height + + contentHeight: createEntitiesFlow.height + importButton.height + assetServerButton.height + header.anchors.topMargin + createEntitiesFlow.anchors.topMargin + assetServerButton.anchors.topMargin + importButton.anchors.topMargin + header.paintedHeight @@ -77,8 +76,9 @@ TabBar { text: "MODEL" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newModelButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newModelButton" } + }); editTabView.currentIndex = 2 } } @@ -88,8 +88,9 @@ TabBar { text: "CUBE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newCubeButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newCubeButton" } + }); editTabView.currentIndex = 2 } } @@ -99,8 +100,9 @@ TabBar { text: "SPHERE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newSphereButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newSphereButton" } + }); editTabView.currentIndex = 2 } } @@ -110,8 +112,9 @@ TabBar { text: "LIGHT" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newLightButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newLightButton" } + }); editTabView.currentIndex = 2 } } @@ -121,8 +124,9 @@ TabBar { text: "TEXT" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newTextButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newTextButton" } + }); editTabView.currentIndex = 2 } } @@ -132,8 +136,9 @@ TabBar { text: "IMAGE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newImageButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newImageButton" } + }); editTabView.currentIndex = 2 } } @@ -143,8 +148,9 @@ TabBar { text: "WEB" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newWebButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newWebButton" } + }); editTabView.currentIndex = 2 } } @@ -154,8 +160,9 @@ TabBar { text: "ZONE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newZoneButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newZoneButton" } + }); editTabView.currentIndex = 2 } } @@ -165,8 +172,9 @@ TabBar { text: "PARTICLE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newParticleButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newParticleButton" } + }); editTabView.currentIndex = 4 } } @@ -176,8 +184,9 @@ TabBar { text: "MATERIAL" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newMaterialButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newMaterialButton" } + }); editTabView.currentIndex = 2 } } @@ -196,8 +205,9 @@ TabBar { anchors.topMargin: 35 onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "openAssetBrowserButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "openAssetBrowserButton" } + }); } } @@ -214,8 +224,9 @@ TabBar { anchors.topMargin: 20 onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "importEntitiesButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "importEntitiesButton" } + }); } } } diff --git a/interface/resources/qml/hifi/tablet/EditTools.qml b/interface/resources/qml/hifi/tablet/EditTools.qml new file mode 100644 index 0000000000..f989038c16 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/EditTools.qml @@ -0,0 +1,58 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.3 + +// FIXME pretty non-DRY code, should figure out a way to optionally hide one tab from the tab view, keep in sync with Edit.qml +StackView { + id: editRoot + objectName: "stack" + + signal sendToScript(var message); + + topPadding: 40 + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + + anchors.fill: parent + + property var itemProperties: {"y": editRoot.topPadding, + "width": editRoot.availableWidth, + "height": editRoot.availableHeight } + Component.onCompleted: { + tab.currentIndex = 0 + } + + background: Rectangle { + color: "#404040" //default background color + EditToolsTabView { + id: tab + anchors.fill: parent + currentIndex: -1 + onCurrentIndexChanged: { + editRoot.replace(null, tab.itemAt(currentIndex).visualItem, + itemProperties, + StackView.Immediate) + } + } + } + + function pushSource(path) { + editRoot.push(Qt.resolvedUrl("../../" + path), itemProperties, + StackView.Immediate); + editRoot.currentItem.sendToScript.connect(editRoot.sendToScript); + } + + function popSource() { + editRoot.pop(StackView.Immediate); + } + + // Passes script messages to the item on the top of the stack + function fromScript(message) { + var currentItem = editRoot.currentItem; + if (currentItem && currentItem.fromScript) { + currentItem.fromScript(message); + } else if (tab.fromScript) { + tab.fromScript(message); + } + } +} diff --git a/interface/resources/qml/hifi/tablet/EditToolsTabView.qml b/interface/resources/qml/hifi/tablet/EditToolsTabView.qml new file mode 100644 index 0000000000..00084b8ca9 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/EditToolsTabView.qml @@ -0,0 +1,328 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtWebChannel 1.0 +import "../../controls" +import "../toolbars" +import QtGraphicalEffects 1.0 +import "../../controls-uit" as HifiControls +import "../../styles-uit" + +TabBar { + id: editTabView + width: parent.width + contentWidth: parent.width + padding: 0 + spacing: 0 + + readonly property QtObject tabIndex: QtObject { + readonly property int create: 0 + readonly property int properties: 1 + readonly property int grid: 2 + readonly property int particle: 3 + } + + readonly property HifiConstants hifi: HifiConstants {} + + EditTabButton { + title: "CREATE" + active: true + enabled: true + property string originalUrl: "" + + property Component visualItem: Component { + + Rectangle { + color: "#404040" + id: container + + Flickable { + height: parent.height + width: parent.width + clip: true + + contentHeight: createEntitiesFlow.height + importButton.height + assetServerButton.height + + header.anchors.topMargin + createEntitiesFlow.anchors.topMargin + + assetServerButton.anchors.topMargin + importButton.anchors.topMargin + + header.paintedHeight + + contentWidth: width + + ScrollBar.vertical : ScrollBar { + visible: parent.contentHeight > parent.height + width: 20 + background: Rectangle { + color: hifi.colors.tableScrollBackgroundDark + } + } + + Text { + id: header + color: "#ffffff" + text: "Choose an Entity Type to Create:" + font.pixelSize: 14 + font.bold: true + anchors.top: parent.top + anchors.topMargin: 28 + anchors.left: parent.left + anchors.leftMargin: 28 + } + + Flow { + id: createEntitiesFlow + spacing: 35 + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: parent.top + anchors.topMargin: 70 + + + NewEntityButton { + icon: "icons/create-icons/94-model-01.svg" + text: "MODEL" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newModelButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/21-cube-01.svg" + text: "CUBE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newCubeButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/22-sphere-01.svg" + text: "SPHERE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newSphereButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/24-light-01.svg" + text: "LIGHT" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newLightButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/20-text-01.svg" + text: "TEXT" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newTextButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/image.svg" + text: "IMAGE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newImageButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/25-web-1-01.svg" + text: "WEB" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newWebButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/23-zone-01.svg" + text: "ZONE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newZoneButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/90-particles-01.svg" + text: "PARTICLE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newParticleButton" } + }); + editTabView.currentIndex = tabIndex.particle + } + } + + NewEntityButton { + icon: "icons/create-icons/126-material-01.svg" + text: "MATERIAL" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newMaterialButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + } + + HifiControls.Button { + id: assetServerButton + text: "Open This Domain's Asset Server" + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: createEntitiesFlow.bottom + anchors.topMargin: 35 + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "openAssetBrowserButton" } + }); + } + } + + HifiControls.Button { + id: importButton + text: "Import Entities (.json)" + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: assetServerButton.bottom + anchors.topMargin: 20 + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "importEntitiesButton" } + }); + } + } + } + } // Flickable + } + } + + EditTabButton { + title: "PROPERTIES" + active: true + enabled: true + property string originalUrl: "" + + property Component visualItem: Component { + WebView { + id: entityPropertiesWebView + url: Paths.defaultScripts + "/system/html/entityProperties.html" + enabled: true + } + } + } + + EditTabButton { + title: "GRID" + active: true + enabled: true + property string originalUrl: "" + + property Component visualItem: Component { + WebView { + id: gridControlsWebView + url: Paths.defaultScripts + "/system/html/gridControls.html" + enabled: true + } + } + } + + EditTabButton { + title: "P" + active: true + enabled: true + property string originalUrl: "" + + property Component visualItem: Component { + WebView { + id: particleExplorerWebView + url: Paths.defaultScripts + "/system/particle_explorer/particleExplorer.html" + enabled: true + } + } + } + + function fromScript(message) { + switch (message.method) { + case 'selectTab': + selectTab(message.params.id); + break; + default: + console.warn('Unrecognized message:', JSON.stringify(message)); + } + } + + // Changes the current tab based on tab index or title as input + function selectTab(id) { + if (typeof id === 'number') { + if (id >= tabIndex.create && id <= tabIndex.particle) { + editTabView.currentIndex = id; + } else { + console.warn('Attempt to switch to invalid tab:', id); + } + } else if (typeof id === 'string'){ + switch (id.toLowerCase()) { + case 'create': + editTabView.currentIndex = tabIndex.create; + break; + case 'properties': + editTabView.currentIndex = tabIndex.properties; + break; + case 'grid': + editTabView.currentIndex = tabIndex.grid; + break; + case 'particle': + editTabView.currentIndex = tabIndex.particle; + break; + default: + console.warn('Attempt to switch to invalid tab:', id); + } + } else { + console.warn('Attempt to switch tabs with invalid input:', JSON.stringify(id)); + } + } +} diff --git a/interface/resources/qml/hifi/tablet/EntityList.qml b/interface/resources/qml/hifi/tablet/EntityList.qml new file mode 100644 index 0000000000..f4b47c19bb --- /dev/null +++ b/interface/resources/qml/hifi/tablet/EntityList.qml @@ -0,0 +1,5 @@ +WebView { + id: entityListToolWebView + url: Paths.defaultScripts + "/system/html/entityList.html" + enabled: true +} diff --git a/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml b/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml index 6df97e67b0..526a42f8e2 100644 --- a/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml +++ b/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml @@ -29,12 +29,16 @@ Rectangle { property bool keyboardRasied: false function errorMessageBox(message) { - return desktop.messageBox({ - icon: hifi.icons.warning, - defaultButton: OriginalDialogs.StandardButton.Ok, - title: "Error", - text: message - }); + try { + return desktop.messageBox({ + icon: hifi.icons.warning, + defaultButton: OriginalDialogs.StandardButton.Ok, + title: "Error", + text: message + }); + } catch(e) { + Window.alert(message); + } } Item { diff --git a/interface/resources/qml/hifi/tablet/NewMaterialWindow.qml b/interface/resources/qml/hifi/tablet/NewMaterialWindow.qml new file mode 100644 index 0000000000..def816c36e --- /dev/null +++ b/interface/resources/qml/hifi/tablet/NewMaterialWindow.qml @@ -0,0 +1,20 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 + +StackView { + id: stackView + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + anchors.topMargin: 40 + + signal sendToScript(var message); + + NewMaterialDialog { + id: dialog + anchors.fill: parent + Component.onCompleted:{ + dialog.sendToScript.connect(stackView.sendToScript); + } + } +} diff --git a/interface/resources/qml/hifi/tablet/NewModelDialog.qml b/interface/resources/qml/hifi/tablet/NewModelDialog.qml index 8f6718e1f3..10b844c987 100644 --- a/interface/resources/qml/hifi/tablet/NewModelDialog.qml +++ b/interface/resources/qml/hifi/tablet/NewModelDialog.qml @@ -29,12 +29,16 @@ Rectangle { property bool keyboardRasied: false function errorMessageBox(message) { - return desktop.messageBox({ - icon: hifi.icons.warning, - defaultButton: OriginalDialogs.StandardButton.Ok, - title: "Error", - text: message - }); + try { + return desktop.messageBox({ + icon: hifi.icons.warning, + defaultButton: OriginalDialogs.StandardButton.Ok, + title: "Error", + text: message + }); + } catch(e) { + Window.alert(message); + } } Item { diff --git a/interface/resources/qml/hifi/tablet/NewModelWindow.qml b/interface/resources/qml/hifi/tablet/NewModelWindow.qml new file mode 100644 index 0000000000..616a44ab7a --- /dev/null +++ b/interface/resources/qml/hifi/tablet/NewModelWindow.qml @@ -0,0 +1,20 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 + +StackView { + id: stackView + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + anchors.topMargin: 40 + + signal sendToScript(var message); + + NewModelDialog { + id: dialog + anchors.fill: parent + Component.onCompleted:{ + dialog.sendToScript.connect(stackView.sendToScript); + } + } +} diff --git a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml index 20682372c5..c2aff08e35 100644 --- a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml +++ b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml @@ -28,6 +28,7 @@ Flickable { onPluginNameChanged: { if (page !== null) { page.pluginName = flick.pluginName; + page.displayConfiguration(); } } diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 08f86770e6..9a472de046 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -89,6 +89,7 @@ StackView { property bool keyboardEnabled: false property bool punctuationMode: false + property bool keyboardRaised: false width: parent.width height: parent.height @@ -210,6 +211,8 @@ StackView { QQC2.TextField { id: addressLine + + focus: true width: addressLineContainer.width - addressLineContainer.anchors.leftMargin - addressLineContainer.anchors.rightMargin; anchors { left: addressLineContainer.left; @@ -236,24 +239,20 @@ StackView { color: hifi.colors.text background: Item {} - QQC2.Label { - T.TextField { - id: control + } - padding: 6 // numbers taken from Qt\5.9.2\Src\qtquickcontrols2\src\imports\controls\TextField.qml - leftPadding: padding + 4 - } + QQC2.Label { + font: addressLine.font - font: parent.font + x: addressLine.x + y: addressLine.y + leftPadding: addressLine.leftPadding + topPadding: addressLine.topPadding - x: control.leftPadding - y: control.topPadding - - text: parent.placeholderText2 - verticalAlignment: "AlignVCenter" - color: 'gray' - visible: parent.text === '' - } + text: addressLine.placeholderText2 + verticalAlignment: "AlignVCenter" + color: 'gray' + visible: addressLine.text === '' } Rectangle { diff --git a/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml index 63801019b9..4f1100f20b 100644 --- a/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml +++ b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml @@ -32,6 +32,6 @@ StackView { TabletPreferencesDialog { id: root objectName: "TabletGeneralPreferences" - showCategories: ["User Interface", "HMD", "Snapshots", "Privacy"] + showCategories: ["User Interface", "Mouse Sensitivity", "HMD", "Snapshots", "Privacy"] } } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml index 646a0d2c77..05f45dc61e 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml @@ -24,6 +24,7 @@ Item { HifiConstants { id: hifi } property var sections: [] property var showCategories: [] + property var categoryProperties: ({}) property bool keyboardEnabled: false property bool keyboardRaised: false @@ -100,7 +101,8 @@ Item { // NOTE: the sort order of items in the showCategories array is the same order in the dialog. for (i = 0; i < showCategories.length; i++) { if (categoryMap[showCategories[i]]) { - sections.push(sectionBuilder.createObject(prefControls, {name: showCategories[i]})); + var properties = categoryProperties.hasOwnProperty(showCategories[i]) ? categoryProperties[showCategories[i]] : {}; + sections.push(sectionBuilder.createObject(prefControls, {name: showCategories[i], sectionProperties: properties})); } } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml index 15db58decc..833175f311 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -24,6 +24,7 @@ Preference { property bool isLast: false property string name: "Header" property real spacing: 8 + property var sectionProperties: ({}) default property alias preferences: contentContainer.children HifiConstants { id: hifi } @@ -163,6 +164,28 @@ Preference { if (builder) { preferences.push(builder.createObject(contentContainer, { preference: preference, isFirstCheckBox: (checkBoxCount === 1) , z: zpos})); + + var preferenceObject = preferences[preferences.length - 1]; + var props = sectionProperties.hasOwnProperty(preference.name) ? sectionProperties[preference.name] : {}; + + for(var prop in props) { + var value = props[prop]; + if(value.indexOf('.') !== -1) { + var splittedValues = value.split('.'); + if(splittedValues[0] === 'parent') { + value = preferenceObject.parent[splittedValues[1]]; + } + } else if(value === 'undefined') { + value = undefined; + } + + if(prop.indexOf('.') !== -1) { + var splittedProps = prop.split('.'); + preferenceObject[splittedProps[0]][splittedProps[1]] = value; + } else { + preferenceObject[prop] = value; + } + } } } } diff --git a/interface/src/AndroidHelper.cpp b/interface/src/AndroidHelper.cpp index ef7319e63f..419382f2cb 100644 --- a/interface/src/AndroidHelper.cpp +++ b/interface/src/AndroidHelper.cpp @@ -10,35 +10,21 @@ // #include "AndroidHelper.h" #include -#include +#include "Application.h" + +#if defined(qApp) +#undef qApp +#endif +#define qApp (static_cast(QCoreApplication::instance())) AndroidHelper::AndroidHelper() { } AndroidHelper::~AndroidHelper() { - workerThread.quit(); - workerThread.wait(); } -void AndroidHelper::init() { - workerThread.start(); - _accountManager = QSharedPointer(new AccountManager, &QObject::deleteLater); - _accountManager->setIsAgent(true); - _accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL()); - _accountManager->setSessionID(DependencyManager::get()->getSessionID()); - connect(_accountManager.data(), &AccountManager::loginComplete, [](const QUrl& authURL) { - DependencyManager::get()->setAccountInfo(AndroidHelper::instance().getAccountManager()->getAccountInfo()); - DependencyManager::get()->setAuthURL(authURL); - }); - - connect(_accountManager.data(), &AccountManager::logoutComplete, [] () { - DependencyManager::get()->logout(); - }); - _accountManager->moveToThread(&workerThread); -} - -void AndroidHelper::requestActivity(const QString &activityName, const bool backToScene) { - emit androidActivityRequested(activityName, backToScene); +void AndroidHelper::requestActivity(const QString &activityName, const bool backToScene, QList args) { + emit androidActivityRequested(activityName, backToScene, args); } void AndroidHelper::notifyLoadComplete() { @@ -49,6 +35,10 @@ void AndroidHelper::notifyEnterForeground() { emit enterForeground(); } +void AndroidHelper::notifyBeforeEnterBackground() { + emit beforeEnterBackground(); +} + void AndroidHelper::notifyEnterBackground() { emit enterBackground(); } @@ -60,3 +50,9 @@ void AndroidHelper::performHapticFeedback(int duration) { void AndroidHelper::showLoginDialog() { emit androidActivityRequested("Login", true); } + +void AndroidHelper::processURL(const QString &url) { + if (qApp->canAcceptURL(url)) { + qApp->acceptURL(url); + } +} diff --git a/interface/src/AndroidHelper.h b/interface/src/AndroidHelper.h index 007c0db4a5..03d92f91d9 100644 --- a/interface/src/AndroidHelper.h +++ b/interface/src/AndroidHelper.h @@ -13,8 +13,6 @@ #define hifi_Android_Helper_h #include -#include -#include class AndroidHelper : public QObject { Q_OBJECT @@ -23,15 +21,15 @@ public: static AndroidHelper instance; return instance; } - void init(); - void requestActivity(const QString &activityName, const bool backToScene); + void requestActivity(const QString &activityName, const bool backToScene, QList args = QList()); void notifyLoadComplete(); void notifyEnterForeground(); + void notifyBeforeEnterBackground(); void notifyEnterBackground(); void performHapticFeedback(int duration); + void processURL(const QString &url); - QSharedPointer getAccountManager() { return _accountManager; } AndroidHelper(AndroidHelper const&) = delete; void operator=(AndroidHelper const&) = delete; @@ -39,9 +37,10 @@ public slots: void showLoginDialog(); signals: - void androidActivityRequested(const QString &activityName, const bool backToScene); + void androidActivityRequested(const QString &activityName, const bool backToScene, QList args = QList()); void qtAppLoadComplete(); void enterForeground(); + void beforeEnterBackground(); void enterBackground(); void hapticFeedbackRequested(int duration); @@ -49,8 +48,6 @@ signals: private: AndroidHelper(); ~AndroidHelper(); - QSharedPointer _accountManager; - QThread workerThread; }; #endif diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index cffed61b37..b322b823cd 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -129,6 +130,7 @@ #include #include #include +#include #include #include #include @@ -269,9 +271,6 @@ public: } _renderContext->doneCurrent(); - // Deleting the object with automatically shutdown the thread - connect(qApp, &QCoreApplication::aboutToQuit, this, &QObject::deleteLater); - // Transfer to a new thread moveToNewNamedThread(this, "RenderThread", [this](QThread* renderThread) { hifi::qt::addBlockingForbiddenThread("Render", renderThread); @@ -814,6 +813,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { } // Tell the plugin manager about our statically linked plugins + DependencyManager::set(); auto pluginManager = PluginManager::getInstance(); pluginManager->setInputPluginProvider([] { return getInputPlugins(); }); pluginManager->setDisplayPluginProvider([] { return getDisplayPlugins(); }); @@ -1074,6 +1074,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Regular.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Bold.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-SemiBold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Cairo-SemiBold.ttf"); _window->setWindowTitle("High Fidelity Interface"); Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us @@ -1081,6 +1082,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo auto nodeList = DependencyManager::get(); nodeList->startThread(); + // move the AddressManager to the NodeList thread so that domain resets due to domain changes always occur + // before we tell MyAvatar to go to a new location in the new domain + auto addressManager = DependencyManager::get(); + addressManager->moveToThread(nodeList->thread()); + const char** constArgv = const_cast(argv); if (cmdOptionExists(argc, constArgv, "--disableWatchdog")) { DISABLE_WATCHDOG = true; @@ -1231,8 +1237,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo accountManager->setIsAgent(true); accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL()); - auto addressManager = DependencyManager::get(); - // use our MyAvatar position and quat for address manager path addressManager->setPositionGetter([this]{ return getMyAvatar()->getWorldPosition(); }); addressManager->setOrientationGetter([this]{ return getMyAvatar()->getWorldOrientation(); }); @@ -1375,6 +1379,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo initializeRenderEngine(); qCDebug(interfaceapp, "Initialized Render Engine."); + // Overlays need to exist before we set the ContextOverlayInterface dependency + _overlays.init(); // do this before scripts load + DependencyManager::set(); + // Initialize the user interface and menu system // Needs to happen AFTER the render engine initialization to access its configuration initializeUi(); @@ -1440,8 +1448,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // add firstRun flag from settings to launch event Setting::Handle firstRun { Settings::firstRun, true }; - QString machineFingerPrint = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); - auto& userActivityLogger = UserActivityLogger::getInstance(); if (userActivityLogger.isEnabled()) { // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. @@ -1493,13 +1499,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["first_run"] = firstRun.get(); // add the user's machine ID to the launch event + QString machineFingerPrint = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); properties["machine_fingerprint"] = machineFingerPrint; userActivityLogger.logAction("launch", properties); } - setCrashAnnotation("machine_fingerprint", machineFingerPrint.toStdString()); - _entityEditSender.setMyAvatar(myAvatar.get()); // The entity octree will have to know about MyAvatar for the parentJointName import @@ -1511,10 +1516,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // allow you to move an entity around in your hand _entityEditSender.setPacketsPerSecond(3000); // super high!! - // Overlays need to exist before we set the ContextOverlayInterface dependency - _overlays.init(); // do this before scripts load - DependencyManager::set(); - // Make sure we don't time out during slow operations at startup updateHeartbeat(); @@ -2119,6 +2120,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo || ((rightHandPose.valid || lastRightHandPose.valid) && (rightHandPose != lastRightHandPose)); lastLeftHandPose = leftHandPose; lastRightHandPose = rightHandPose; + properties["avatar_identity_requests_sent"] = DependencyManager::get()->getIdentityRequestsSent(); UserActivityLogger::getInstance().logAction("stats", properties); }); @@ -2256,7 +2258,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo qCDebug(interfaceapp) << "Metaverse session ID is" << uuidStringWithoutCurlyBraces(accountManager->getSessionID()); #if defined(Q_OS_ANDROID) - AndroidHelper::instance().init(); + connect(&AndroidHelper::instance(), &AndroidHelper::beforeEnterBackground, this, &Application::beforeEnterBackground); connect(&AndroidHelper::instance(), &AndroidHelper::enterBackground, this, &Application::enterBackground); connect(&AndroidHelper::instance(), &AndroidHelper::enterForeground, this, &Application::enterForeground); AndroidHelper::instance().notifyLoadComplete(); @@ -2553,25 +2555,28 @@ Application::~Application() { _octreeProcessor.terminate(); _entityEditSender.terminate(); + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + steamClient->shutdown(); + } + DependencyManager::destroy(); + + DependencyManager::destroy(); // must be destroyed before the FramebufferCache + DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); - DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); + DependencyManager::destroy(); DependencyManager::get()->cleanup(); // remove the NodeList from the DependencyManager DependencyManager::destroy(); - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - steamClient->shutdown(); - } - #if 0 ConnexionClient::getInstance().destroy(); #endif @@ -2591,6 +2596,8 @@ Application::~Application() { // Can't log to file passed this point, FileLogger about to be deleted qInstallMessageHandler(LogHandler::verboseMessageHandler); + + _renderEventHandler->deleteLater(); } void Application::initializeGL() { @@ -2717,7 +2724,7 @@ void Application::initializeDisplayPlugins() { setDisplayPlugin(defaultDisplayPlugin); // Now set the desired plugin if it's not the same as the default plugin - if (targetDisplayPlugin != defaultDisplayPlugin) { + if (targetDisplayPlugin && (targetDisplayPlugin != defaultDisplayPlugin)) { setDisplayPlugin(targetDisplayPlugin); } @@ -2891,6 +2898,7 @@ void Application::initializeUi() { auto compositorHelper = DependencyManager::get(); connect(compositorHelper.data(), &CompositorHelper::allowMouseCaptureChanged, this, [=] { if (isHMDMode()) { + auto compositorHelper = DependencyManager::get(); // don't capture outer smartpointer showCursor(compositorHelper->getAllowMouseCapture() ? Cursor::Manager::lookupIcon(_preferredCursor.get()) : Cursor::Icon::SYSTEM); @@ -2973,6 +2981,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("Overlays", &_overlays); surfaceContext->setContextProperty("Window", DependencyManager::get().data()); + surfaceContext->setContextProperty("Desktop", DependencyManager::get().data()); surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); @@ -3212,6 +3221,7 @@ void Application::setSettingConstrainToolbarPosition(bool setting) { void Application::showHelp() { static const QString HAND_CONTROLLER_NAME_VIVE = "vive"; static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; + static const QString HAND_CONTROLLER_NAME_WINDOWS_MR = "windowsMR"; static const QString TAB_KEYBOARD_MOUSE = "kbm"; static const QString TAB_GAMEPAD = "gamepad"; @@ -3226,9 +3236,13 @@ void Application::showHelp() { } else if (PluginUtils::isOculusTouchControllerAvailable()) { defaultTab = TAB_HAND_CONTROLLERS; handControllerName = HAND_CONTROLLER_NAME_OCULUS_TOUCH; + } else if (qApp->getActiveDisplayPlugin()->getName() == "WindowMS") { + defaultTab = TAB_HAND_CONTROLLERS; + handControllerName = HAND_CONTROLLER_NAME_WINDOWS_MR; } else if (PluginUtils::isXboxControllerAvailable()) { defaultTab = TAB_GAMEPAD; } + // TODO need some way to detect windowsMR to load controls reference default tab in Help > Controls Reference menu. QUrlQuery queryString; queryString.addQueryItem("handControllerName", handControllerName); @@ -3254,13 +3268,22 @@ void Application::resizeGL() { // Set the desired FBO texture size. If it hasn't changed, this does nothing. // Otherwise, it must rebuild the FBOs uvec2 framebufferSize = displayPlugin->getRecommendedRenderSize(); - float renderResolutionScale = getRenderResolutionScale(); - uvec2 renderSize = uvec2(vec2(framebufferSize) * renderResolutionScale); + uvec2 renderSize = uvec2(framebufferSize); if (_renderResolution != renderSize) { _renderResolution = renderSize; DependencyManager::get()->setFrameBufferSize(fromGlm(renderSize)); } + auto renderResolutionScale = getRenderResolutionScale(); + if (displayPlugin->getRenderResolutionScale() != renderResolutionScale) { + auto renderConfig = _renderEngine->getConfiguration(); + assert(renderConfig); + auto mainView = renderConfig->getConfig("RenderMainView.RenderDeferredTask"); + assert(mainView); + mainView->setProperty("resolutionScale", renderResolutionScale); + displayPlugin->setRenderResolutionScale(renderResolutionScale); + } + // FIXME the aspect ratio for stereo displays is incorrect based on this. float aspectRatio = displayPlugin->getRecommendedAspectRatio(); _myCamera.setProjection(glm::perspective(glm::radians(_fieldOfView.get()), aspectRatio, @@ -3272,7 +3295,6 @@ void Application::resizeGL() { } DependencyManager::get()->resize(fromGlm(displayPlugin->getRecommendedUiSize())); - displayPlugin->setRenderResolutionScale(renderResolutionScale); } void Application::handleSandboxStatus(QNetworkReply* reply) { @@ -4013,7 +4035,18 @@ void Application::mousePressEvent(QMouseEvent* event) { return; } +#if defined(Q_OS_MAC) + // Fix for OSX right click dragging on window when coming from a native window + bool isFocussed = hasFocus(); + if (!isFocussed && event->button() == Qt::MouseButton::RightButton) { + setFocus(); + isFocussed = true; + } + + if (isFocussed) { +#else if (hasFocus()) { +#endif if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->mousePressEvent(event); } @@ -6630,6 +6663,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe qScriptRegisterMetaType(scriptEngine.data(), OverlayIDtoScriptValue, OverlayIDfromScriptValue); + registerInteractiveWindowMetaType(scriptEngine.data()); + DependencyManager::get()->registerMetaTypes(scriptEngine.data()); // connect this script engines printedMessage signal to the global ScriptEngines these various messages @@ -6977,7 +7012,9 @@ void Application::showAssetServerWidget(QString filePath) { DependencyManager::get()->show(url, "AssetServer", startUpload); } else { static const QUrl url("hifi/dialogs/TabletAssetServer.qml"); - tablet->pushOntoStack(url); + if (!tablet->isPathLoaded(url)) { + tablet->pushOntoStack(url); + } } } @@ -7614,7 +7651,6 @@ void Application::toggleEntityScriptServerLogDialog() { void Application::loadAddAvatarBookmarkDialog() const { auto avatarBookmarks = DependencyManager::get(); - avatarBookmarks->addBookmark(); } void Application::loadAvatarBrowser() const { @@ -8296,7 +8332,24 @@ void Application::saveNextPhysicsStats(QString filename) { _physicsEngine->saveNextPhysicsStats(filename); } +void Application::copyToClipboard(const QString& text) { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "copyToClipboard"); + return; + } + + // assume that the address is being copied because the user wants a shareable address + QApplication::clipboard()->setText(text); +} + #if defined(Q_OS_ANDROID) +void Application::beforeEnterBackground() { + auto nodeList = DependencyManager::get(); + nodeList->setSendDomainServerCheckInEnabled(false); + nodeList->reset(true); + clearDomainOctreeDetails(); +} + void Application::enterBackground() { QMetaObject::invokeMethod(DependencyManager::get().data(), "stop", Qt::BlockingQueuedConnection); @@ -8311,6 +8364,8 @@ void Application::enterForeground() { if (!getActiveDisplayPlugin() || getActiveDisplayPlugin()->isActive() || !getActiveDisplayPlugin()->activate()) { qWarning() << "Could not re-activate display plugin"; } + auto nodeList = DependencyManager::get(); + nodeList->setSendDomainServerCheckInEnabled(true); } #endif diff --git a/interface/src/Application.h b/interface/src/Application.h index 346ea258da..94e561e550 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -310,7 +310,10 @@ public: void loadAvatarScripts(const QVector& urls); void unloadAvatarScripts(); + Q_INVOKABLE void copyToClipboard(const QString& text); + #if defined(Q_OS_ANDROID) + void beforeEnterBackground(); void enterBackground(); void enterForeground(); #endif diff --git a/interface/src/Application_render.cpp b/interface/src/Application_render.cpp index 2daa49dcf7..6648fa2eb7 100644 --- a/interface/src/Application_render.cpp +++ b/interface/src/Application_render.cpp @@ -102,7 +102,7 @@ void Application::paintGL() { PerformanceTimer perfTimer("renderOverlay"); // NOTE: There is no batch associated with this renderArgs // the ApplicationOverlay class assumes it's viewport is setup to be the device size - renderArgs._viewport = glm::ivec4(0, 0, getDeviceSize() * getRenderResolutionScale()); + renderArgs._viewport = glm::ivec4(0, 0, getDeviceSize()); _applicationOverlay.renderOverlay(&renderArgs); } @@ -118,7 +118,7 @@ void Application::paintGL() { // Primary rendering pass auto framebufferCache = DependencyManager::get(); finalFramebufferSize = framebufferCache->getFrameBufferSize(); - // Final framebuffer that will be handled to the display-plugin + // Final framebuffer that will be handed to the display-plugin finalFramebuffer = framebufferCache->getFramebuffer(); } @@ -139,7 +139,10 @@ void Application::paintGL() { frame->frameIndex = _renderFrameCount; frame->framebuffer = finalFramebuffer; frame->framebufferRecycler = [](const gpu::FramebufferPointer& framebuffer) { - DependencyManager::get()->releaseFramebuffer(framebuffer); + auto frameBufferCache = DependencyManager::get(); + if (frameBufferCache) { + frameBufferCache->releaseFramebuffer(framebuffer); + } }; // deliver final scene rendering commands to the display plugin { diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index f97c02bca3..4e3e539dea 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -17,7 +17,9 @@ #include #include #include +#include +#include #include #include #include @@ -28,7 +30,6 @@ #include #include "MainWindow.h" -#include "Menu.h" #include "InterfaceLogging.h" #include "QVariantGLM.h" @@ -92,10 +93,96 @@ void addAvatarEntities(const QVariantList& avatarEntities) { } AvatarBookmarks::AvatarBookmarks() { + QDir directory(PathUtils::getAppDataPath()); + if (!directory.exists()) { + directory.mkpath("."); + } + _bookmarksFilename = PathUtils::getAppDataPath() + "/" + AVATARBOOKMARKS_FILENAME; + if(!QFile::exists(_bookmarksFilename)) { + auto defaultBookmarksFilename = PathUtils::resourcesPath() + QString("avatar/bookmarks") + "/" + AVATARBOOKMARKS_FILENAME; + if (!QFile::exists(defaultBookmarksFilename)) { + qDebug() << defaultBookmarksFilename << "doesn't exist!"; + } + + if (!QFile::copy(defaultBookmarksFilename, _bookmarksFilename)) { + qDebug() << "failed to copy" << defaultBookmarksFilename << "to" << _bookmarksFilename; + } + } readFromFile(); } +void AvatarBookmarks::addBookmark(const QString& bookmarkName) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "addBookmark", Q_ARG(QString, bookmarkName)); + return; + } + QVariantMap bookmark = getAvatarDataToBookmark(); + insert(bookmarkName, bookmark); + + emit bookmarkAdded(bookmarkName); +} + +void AvatarBookmarks::saveBookmark(const QString& bookmarkName) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "saveBookmark", Q_ARG(QString, bookmarkName)); + return; + } + if (contains(bookmarkName)) { + QVariantMap bookmark = getAvatarDataToBookmark(); + insert(bookmarkName, bookmark); + } +} + +void AvatarBookmarks::removeBookmark(const QString& bookmarkName) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "removeBookmark", Q_ARG(QString, bookmarkName)); + return; + } + + remove(bookmarkName); + emit bookmarkDeleted(bookmarkName); +} + +void AvatarBookmarks::updateAvatarEntities(const QVariantList &avatarEntities) { + auto myAvatar = DependencyManager::get()->getMyAvatar(); + myAvatar->removeAvatarEntities(); + + addAvatarEntities(avatarEntities); +} + +void AvatarBookmarks::loadBookmark(const QString& bookmarkName) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "loadBookmark", Q_ARG(QString, bookmarkName)); + return; + } + + auto bookmarkEntry = _bookmarks.find(bookmarkName); + + if (bookmarkEntry != _bookmarks.end()) { + QVariantMap bookmark = bookmarkEntry.value().toMap(); + if (!bookmark.empty()) { + auto myAvatar = DependencyManager::get()->getMyAvatar(); + myAvatar->removeAvatarEntities(); + const QString& avatarUrl = bookmark.value(ENTRY_AVATAR_URL, "").toString(); + myAvatar->useFullAvatarURL(avatarUrl); + qCDebug(interfaceapp) << "Avatar On " << avatarUrl; + const QList& attachments = bookmark.value(ENTRY_AVATAR_ATTACHMENTS, QList()).toList(); + + qCDebug(interfaceapp) << "Attach " << attachments; + myAvatar->setAttachmentsVariant(attachments); + + const float& qScale = bookmark.value(ENTRY_AVATAR_SCALE, 1.0f).toFloat(); + myAvatar->setAvatarScale(qScale); + + const QVariantList& avatarEntities = bookmark.value(ENTRY_AVATAR_ENTITIES, QVariantList()).toList(); + addAvatarEntities(avatarEntities); + + emit bookmarkLoaded(bookmarkName); + } + } +} + void AvatarBookmarks::readFromFile() { // migrate old avatarbookmarks.json, used to be in 'local' folder on windows QString oldConfigPath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + AVATARBOOKMARKS_FILENAME; @@ -115,99 +202,37 @@ void AvatarBookmarks::readFromFile() { Bookmarks::readFromFile(); } -void AvatarBookmarks::setupMenus(Menu* menubar, MenuWrapper* menu) { - // Add menus/actions - auto bookmarkAction = menubar->addActionToQMenuAndActionHash(menu, MenuOption::BookmarkAvatar); - QObject::connect(bookmarkAction, SIGNAL(triggered()), this, SLOT(addBookmark()), Qt::QueuedConnection); - _bookmarksMenu = menu->addMenu(MenuOption::AvatarBookmarks); - _deleteBookmarksAction = menubar->addActionToQMenuAndActionHash(menu, MenuOption::DeleteAvatarBookmark); - QObject::connect(_deleteBookmarksAction, SIGNAL(triggered()), this, SLOT(deleteBookmark()), Qt::QueuedConnection); - - for (auto it = _bookmarks.begin(); it != _bookmarks.end(); ++it) { - addBookmarkToMenu(menubar, it.key(), it.value()); +QVariantMap AvatarBookmarks::getBookmark(const QString &bookmarkName) +{ + if (QThread::currentThread() != thread()) { + QVariantMap result; + BLOCKING_INVOKE_METHOD(this, "getBookmark", Q_RETURN_ARG(QVariantMap, result), Q_ARG(QString, bookmarkName)); + return result; } - Bookmarks::sortActions(menubar, _bookmarksMenu); + QVariantMap bookmark; + + auto bookmarkEntry = _bookmarks.find(bookmarkName); + + if (bookmarkEntry != _bookmarks.end()) { + bookmark = bookmarkEntry.value().toMap(); + } + + return bookmark; } -void AvatarBookmarks::changeToBookmarkedAvatar() { - QAction* action = qobject_cast(sender()); +QVariantMap AvatarBookmarks::getAvatarDataToBookmark() { auto myAvatar = DependencyManager::get()->getMyAvatar(); + const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString(); + const QVariant& avatarScale = myAvatar->getAvatarScale(); - - if (action->data().type() == QVariant::String) { - // TODO: Phase this out eventually. - // Legacy avatar bookmark. - - myAvatar->useFullAvatarURL(action->data().toString()); - qCDebug(interfaceapp) << " Using Legacy V1 Avatar Bookmark "; - } else { - - const QMap bookmark = action->data().toMap(); - // Not magic value. This is the current made version, and if it changes this interpreter should be updated to - // handle the new one separately. - // This is where the avatar bookmark entry is parsed. If adding new Value, make sure to have backward compatability with previous - if (bookmark.value(ENTRY_VERSION) == 3) { - myAvatar->removeAvatarEntities(); - const QString& avatarUrl = bookmark.value(ENTRY_AVATAR_URL, "").toString(); - myAvatar->useFullAvatarURL(avatarUrl); - qCDebug(interfaceapp) << "Avatar On " << avatarUrl; - const QList& attachments = bookmark.value(ENTRY_AVATAR_ATTACHMENTS, QList()).toList(); - - qCDebug(interfaceapp) << "Attach " << attachments; - myAvatar->setAttachmentsVariant(attachments); - - const float& qScale = bookmark.value(ENTRY_AVATAR_SCALE, 1.0f).toFloat(); - myAvatar->setAvatarScale(qScale); - - const QVariantList& avatarEntities = bookmark.value(ENTRY_AVATAR_ENTITIES, QVariantList()).toList(); - addAvatarEntities(avatarEntities); - - } else { - qCDebug(interfaceapp) << " Bookmark entry does not match client version, make sure client has a handler for the new AvatarBookmark"; - } - } - -} - -void AvatarBookmarks::addBookmark() { - ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_PLACEMARK, "Bookmark Avatar", "Name", QString()); - connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { - disconnect(dlg, &ModalDialogListener::response, this, nullptr); - auto bookmarkName = response.toString(); - bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " "); - if (bookmarkName.length() == 0) { - return; - } - - auto myAvatar = DependencyManager::get()->getMyAvatar(); - - const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString(); - const QVariant& avatarScale = myAvatar->getAvatarScale(); - - // If Avatar attachments ever change, this is where to update them, when saving remember to also append to AVATAR_BOOKMARK_VERSION - QVariantMap bookmark; - bookmark.insert(ENTRY_VERSION, AVATAR_BOOKMARK_VERSION); - bookmark.insert(ENTRY_AVATAR_URL, avatarUrl); - bookmark.insert(ENTRY_AVATAR_SCALE, avatarScale); - bookmark.insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant()); - bookmark.insert(ENTRY_AVATAR_ENTITIES, myAvatar->getAvatarEntitiesVariant()); - - Bookmarks::addBookmarkToFile(bookmarkName, bookmark); - }); - -} - -void AvatarBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) { - QAction* changeAction = _bookmarksMenu->newAction(); - changeAction->setData(bookmark); - connect(changeAction, SIGNAL(triggered()), this, SLOT(changeToBookmarkedAvatar())); - if (!_isMenuSorted) { - menubar->addActionToQMenuAndActionHash(_bookmarksMenu, changeAction, name, 0, QAction::NoRole); - } else { - // TODO: this is aggressive but other alternatives have proved less fruitful so far. - menubar->addActionToQMenuAndActionHash(_bookmarksMenu, changeAction, name, 0, QAction::NoRole); - Bookmarks::sortActions(menubar, _bookmarksMenu); - } + // If Avatar attachments ever change, this is where to update them, when saving remember to also append to AVATAR_BOOKMARK_VERSION + QVariantMap bookmark; + bookmark.insert(ENTRY_VERSION, AVATAR_BOOKMARK_VERSION); + bookmark.insert(ENTRY_AVATAR_URL, avatarUrl); + bookmark.insert(ENTRY_AVATAR_SCALE, avatarScale); + bookmark.insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant()); + bookmark.insert(ENTRY_AVATAR_ENTITIES, myAvatar->getAvatarEntitiesVariant()); + return bookmark; } diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h index 7b47ea8af7..f1bc6820eb 100644 --- a/interface/src/AvatarBookmarks.h +++ b/interface/src/AvatarBookmarks.h @@ -30,19 +30,50 @@ class AvatarBookmarks: public Bookmarks, public Dependency { public: AvatarBookmarks(); - void setupMenus(Menu* menubar, MenuWrapper* menu) override; - + void setupMenus(Menu* menubar, MenuWrapper* menu) override {}; + Q_INVOKABLE QVariantMap getBookmark(const QString& bookmarkName); public slots: /**jsdoc * Add the current Avatar to your avatar bookmarks. * @function AvatarBookmarks.addBookMark */ - void addBookmark(); + void addBookmark(const QString& bookmarkName); + void saveBookmark(const QString& bookmarkName); + void loadBookmark(const QString& bookmarkName); + void removeBookmark(const QString& bookmarkName); + void updateAvatarEntities(const QVariantList& avatarEntities); + QVariantMap getBookmarks() { return _bookmarks; } + +signals: + /**jsdoc + * This function gets triggered after avatar loaded from bookmark + * @function AvatarBookmarks.bookmarkLoaded + * @param {string} bookmarkName + * @returns {Signal} + */ + void bookmarkLoaded(const QString& bookmarkName); + + /**jsdoc + * This function gets triggered after avatar bookmark deleted + * @function AvatarBookmarks.bookmarkDeleted + * @param {string} bookmarkName + * @returns {Signal} + */ + void bookmarkDeleted(const QString& bookmarkName); + + /**jsdoc + * This function gets triggered after avatar bookmark added + * @function AvatarBookmarks.bookmarkAdded + * @param {string} bookmarkName + * @returns {Signal} + */ + void bookmarkAdded(const QString& bookmarkName); protected: - void addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) override; + void addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) override {}; void readFromFile() override; + QVariantMap getAvatarDataToBookmark(); private: const QString AVATARBOOKMARKS_FILENAME = "avatarbookmarks.json"; @@ -53,9 +84,6 @@ private: const QString ENTRY_VERSION = "version"; const int AVATAR_BOOKMARK_VERSION = 3; - -private slots: - void changeToBookmarkedAvatar(); }; #endif // hifi_AvatarBookmarks_h diff --git a/interface/src/Bookmarks.cpp b/interface/src/Bookmarks.cpp index 6e99b81e50..9a8d8eb279 100644 --- a/interface/src/Bookmarks.cpp +++ b/interface/src/Bookmarks.cpp @@ -46,6 +46,10 @@ void Bookmarks::deleteBookmark() { return; } + deleteBookmark(bookmarkName); +} + +void Bookmarks::deleteBookmark(const QString& bookmarkName) { removeBookmarkFromMenu(Menu::getInstance(), bookmarkName); remove(bookmarkName); diff --git a/interface/src/Bookmarks.h b/interface/src/Bookmarks.h index dc08d4b279..88510e4eda 100644 --- a/interface/src/Bookmarks.h +++ b/interface/src/Bookmarks.h @@ -31,6 +31,8 @@ public: QString addressForBookmark(const QString& name) const; protected: + void deleteBookmark(const QString& bookmarkName); + void addBookmarkToFile(const QString& bookmarkName, const QVariant& bookmark); virtual void addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) = 0; void enableMenuItems(bool enabled); @@ -38,9 +40,10 @@ protected: void insert(const QString& name, const QVariant& address); // Overwrites any existing entry with same name. void sortActions(Menu* menubar, MenuWrapper* menu); int getMenuItemLocation(QList actions, const QString& name) const; - + void removeBookmarkFromMenu(Menu* menubar, const QString& name); bool contains(const QString& name) const; - + void remove(const QString& name); + QVariantMap _bookmarks; // { name: url, ... } QPointer _bookmarksMenu; QPointer _deleteBookmarksAction; @@ -57,12 +60,9 @@ protected slots: void deleteBookmark(); private: - void remove(const QString& name); static bool sortOrder(QAction* a, QAction* b); void persistToFile(); - - void removeBookmarkFromMenu(Menu* menubar, const QString& name); }; #endif // hifi_Bookmarks_h diff --git a/interface/src/CrashHandler.h b/interface/src/CrashHandler.h index 4a6483c700..6f8e9c3bf6 100644 --- a/interface/src/CrashHandler.h +++ b/interface/src/CrashHandler.h @@ -14,8 +14,7 @@ #include -bool startCrashHandler(); +bool startCrashHandler(std::string appPath); void setCrashAnnotation(std::string name, std::string value); - -#endif \ No newline at end of file +#endif // hifi_CrashHandler_h diff --git a/interface/src/CrashHandler_Breakpad.cpp b/interface/src/CrashHandler_Breakpad.cpp index f2a174b6ea..c21bfa95e0 100644 --- a/interface/src/CrashHandler_Breakpad.cpp +++ b/interface/src/CrashHandler_Breakpad.cpp @@ -9,10 +9,10 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "CrashHandler.h" - #if HAS_BREAKPAD +#include "CrashHandler.h" + #include #include @@ -23,8 +23,10 @@ #include #include -#include #include +#include +#include +#include google_breakpad::ExceptionHandler* gBreakpadHandler; @@ -55,11 +57,14 @@ void flushAnnotations() { settings.sync(); } -bool startCrashHandler() { +bool startCrashHandler(std::string appPath) { annotations["version"] = BuildInfo::VERSION; annotations["build_number"] = BuildInfo::BUILD_NUMBER; annotations["build_type"] = BuildInfo::BUILD_TYPE_STRING; + auto machineFingerPrint = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); + annotations["machine_fingerprint"] = machineFingerPrint; + flushAnnotations(); gBreakpadHandler = new google_breakpad::ExceptionHandler( diff --git a/interface/src/CrashHandler_Crashpad.cpp b/interface/src/CrashHandler_Crashpad.cpp index 76d4a8e2e1..d1b5103990 100644 --- a/interface/src/CrashHandler_Crashpad.cpp +++ b/interface/src/CrashHandler_Crashpad.cpp @@ -9,21 +9,24 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#if HAS_CRASHPAD + #include "CrashHandler.h" #include -#include - -#if HAS_CRASHPAD - #include #include -#include +#include #include +#include -#include + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++14-extensions" +#endif #include #include @@ -31,19 +34,32 @@ #include #include +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + #include +#include +#include using namespace crashpad; static const std::string BACKTRACE_URL { CMAKE_BACKTRACE_URL }; static const std::string BACKTRACE_TOKEN { CMAKE_BACKTRACE_TOKEN }; -extern QString qAppFileName(); - CrashpadClient* client { nullptr }; std::mutex annotationMutex; crashpad::SimpleStringDictionary* crashpadAnnotations { nullptr }; +#if defined(Q_OS_WIN) +static const QString CRASHPAD_HANDLER_NAME { "crashpad_handler.exe" }; +#else +static const QString CRASHPAD_HANDLER_NAME { "crashpad_handler" }; +#endif + +#ifdef Q_OS_WIN +#include + LONG WINAPI vectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo) { if (!client) { return EXCEPTION_CONTINUE_SEARCH; @@ -56,8 +72,9 @@ LONG WINAPI vectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo) { return EXCEPTION_CONTINUE_SEARCH; } +#endif -bool startCrashHandler() { +bool startCrashHandler(std::string appPath) { if (BACKTRACE_URL.empty() || BACKTRACE_TOKEN.empty()) { return false; } @@ -73,6 +90,10 @@ bool startCrashHandler() { annotations["build_number"] = BuildInfo::BUILD_NUMBER.toStdString(); annotations["build_type"] = BuildInfo::BUILD_TYPE_STRING.toStdString(); + auto machineFingerPrint = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); + annotations["machine_fingerprint"] = machineFingerPrint.toStdString(); + + arguments.push_back("--no-rate-limit"); // Setup Crashpad DB directory @@ -82,7 +103,10 @@ bool startCrashHandler() { const auto crashpadDbPath = crashpadDbDir.toStdString() + "/" + crashpadDbName; // Locate Crashpad handler - const std::string CRASHPAD_HANDLER_PATH = QFileInfo(qAppFileName()).absolutePath().toStdString() + "/crashpad_handler.exe"; + const QFileInfo interfaceBinary { QString::fromStdString(appPath) }; + const QDir interfaceDir = interfaceBinary.dir(); + assert(interfaceDir.exists(CRASHPAD_HANDLER_NAME)); + const std::string CRASHPAD_HANDLER_PATH = interfaceDir.filePath(CRASHPAD_HANDLER_NAME).toStdString(); // Setup different file paths base::FilePath::StringType dbPath; @@ -101,20 +125,24 @@ bool startCrashHandler() { // Enable automated uploads. database->GetSettings()->SetUploadsEnabled(true); +#ifdef Q_OS_WIN AddVectoredExceptionHandler(0, vectoredExceptionHandler); +#endif return client->StartHandler(handler, db, db, BACKTRACE_URL, annotations, arguments, true, true); } void setCrashAnnotation(std::string name, std::string value) { - std::lock_guard guard(annotationMutex); - if (!crashpadAnnotations) { - crashpadAnnotations = new crashpad::SimpleStringDictionary(); // don't free this, let it leak - crashpad::CrashpadInfo* crashpad_info = crashpad::CrashpadInfo::GetCrashpadInfo(); - crashpad_info->set_simple_annotations(crashpadAnnotations); + if (client) { + std::lock_guard guard(annotationMutex); + if (!crashpadAnnotations) { + crashpadAnnotations = new crashpad::SimpleStringDictionary(); // don't free this, let it leak + crashpad::CrashpadInfo* crashpad_info = crashpad::CrashpadInfo::GetCrashpadInfo(); + crashpad_info->set_simple_annotations(crashpadAnnotations); + } + std::replace(value.begin(), value.end(), ',', ';'); + crashpadAnnotations->SetKeyValue(name, value); } - std::replace(value.begin(), value.end(), ',', ';'); - crashpadAnnotations->SetKeyValue(name, value); } #endif diff --git a/interface/src/CrashHandler_None.cpp b/interface/src/CrashHandler_None.cpp index cba585f7b7..77b8ab332e 100644 --- a/interface/src/CrashHandler_None.cpp +++ b/interface/src/CrashHandler_None.cpp @@ -9,14 +9,15 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#if !defined(HAS_CRASHPAD) && !defined(HAS_BREAKPAD) + #include "CrashHandler.h" #include + #include -#if !defined(HAS_CRASHPAD) && !defined(HAS_BREAKPAD) - -bool startCrashHandler() { +bool startCrashHandler(std::string appPath) { qDebug() << "No crash handler available."; return false; } diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index d39aaa4f1a..e3a99475ef 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -97,7 +97,7 @@ void DiscoverabilityManager::updateLocation() { locationObject.insert(AVAILABILITY_KEY_IN_LOCATION, findableByString(static_cast(_mode.get()))); JSONCallbackParameters callbackParameters; - callbackParameters.jsonCallbackReceiver = this; + callbackParameters.callbackReceiver = this; callbackParameters.jsonCallbackMethod = "handleHeartbeatResponse"; // figure out if we'll send a fresh location or just a simple heartbeat @@ -121,7 +121,7 @@ void DiscoverabilityManager::updateLocation() { // we still send a heartbeat to the metaverse server for stats collection JSONCallbackParameters callbackParameters; - callbackParameters.jsonCallbackReceiver = this; + callbackParameters.callbackReceiver = this; callbackParameters.jsonCallbackMethod = "handleHeartbeatResponse"; accountManager->sendRequest(API_USER_HEARTBEAT_PATH, AccountManagerAuth::Optional, @@ -136,7 +136,7 @@ void DiscoverabilityManager::updateLocation() { setCrashAnnotation("address", currentAddress.toString().toStdString()); } -void DiscoverabilityManager::handleHeartbeatResponse(QNetworkReply& requestReply) { +void DiscoverabilityManager::handleHeartbeatResponse(QNetworkReply* requestReply) { auto dataObject = AccountManager::dataObjectFromResponse(requestReply); if (!dataObject.isEmpty()) { diff --git a/interface/src/DiscoverabilityManager.h b/interface/src/DiscoverabilityManager.h index 0c62ad5663..fc0b1fa759 100644 --- a/interface/src/DiscoverabilityManager.h +++ b/interface/src/DiscoverabilityManager.h @@ -51,7 +51,7 @@ public: static QString findableByString(Discoverability::Mode discoverabilityMode); private slots: - void handleHeartbeatResponse(QNetworkReply& requestReply); + void handleHeartbeatResponse(QNetworkReply* requestReply); private: DiscoverabilityManager(); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index a102f2a42e..8524c40262 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -158,46 +158,6 @@ Menu::Menu() { // Edit > Reload All Content addActionToQMenuAndActionHash(editMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); - - MenuWrapper* avatarMenu = addMenu("Avatar"); - auto avatarManager = DependencyManager::get(); - auto avatar = avatarManager->getMyAvatar(); - - // Avatar > Size - MenuWrapper* avatarSizeMenu = avatarMenu->addMenu("Size"); - // Avatar > Size > Increase - addActionToQMenuAndActionHash(avatarSizeMenu, - MenuOption::IncreaseAvatarSize, - 0, // QML Qt::Key_Plus, - avatar.get(), SLOT(increaseSize())); - - // Avatar > Size > Decrease - addActionToQMenuAndActionHash(avatarSizeMenu, - MenuOption::DecreaseAvatarSize, - 0, // QML Qt::Key_Minus, - avatar.get(), SLOT(decreaseSize())); - - // Avatar > Size > Reset - addActionToQMenuAndActionHash(avatarSizeMenu, - MenuOption::ResetAvatarSize, - 0, // QML Qt::Key_Equal, - avatar.get(), SLOT(resetSize())); - - // Avatar > Reset Sensors - addActionToQMenuAndActionHash(avatarMenu, - MenuOption::ResetSensors, - 0, // QML Qt::Key_Apostrophe, - qApp, SLOT(resetSensors())); - - // Avatar > Attachments... - action = addActionToQMenuAndActionHash(avatarMenu, MenuOption::Attachments); - connect(action, &QAction::triggered, [] { - qApp->showDialog(QString("hifi/dialogs/AttachmentsDialog.qml"), - QString("hifi/tablet/TabletAttachmentsDialog.qml"), "AttachmentsDialog"); - }); - - auto avatarBookmarks = DependencyManager::get(); - avatarBookmarks->setupMenus(this, avatarMenu); // Display menu ---------------------------------- // FIXME - this is not yet matching Alan's spec because it doesn't have // menus for "2D"/"3D" - we need to add support for detecting the appropriate @@ -256,8 +216,6 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::CenterPlayerInView, 0, true, qApp, SLOT(rotationModeChanged())); - addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Overlays, 0, true); - // View > Enter First Person Mode in HMD addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::FirstPersonHMD, 0, true); @@ -283,7 +241,7 @@ Menu::Menu() { MenuWrapper* settingsMenu = addMenu("Settings"); // Settings > General... - action = addActionToQMenuAndActionHash(settingsMenu, MenuOption::Preferences, Qt::CTRL | Qt::Key_G, nullptr, nullptr, QAction::PreferencesRole); + action = addActionToQMenuAndActionHash(settingsMenu, MenuOption::Preferences, Qt::CTRL | Qt::Key_G, nullptr, nullptr); connect(action, &QAction::triggered, [] { qApp->showDialog(QString("hifi/dialogs/GeneralPreferencesDialog.qml"), QString("hifi/tablet/TabletGeneralPreferences.qml"), "GeneralPreferencesDialog"); @@ -317,13 +275,6 @@ Menu::Menu() { QString("hifi/tablet/TabletGraphicsPreferences.qml"), "GraphicsPreferencesDialog"); }); - // Settings > Avatar... - action = addActionToQMenuAndActionHash(settingsMenu, "Avatar..."); - connect(action, &QAction::triggered, [] { - qApp->showDialog(QString("hifi/dialogs/AvatarPreferencesDialog.qml"), - QString("hifi/tablet/TabletAvatarPreferences.qml"), "AvatarPreferencesDialog"); - }); - // Settings > Developer Menu addCheckableActionToQMenuAndActionHash(settingsMenu, "Developer Menu", 0, false, this, SLOT(toggleDeveloperMenus())); @@ -582,6 +533,9 @@ Menu::Menu() { action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowOtherLookAtVectors, 0, false); connect(action, &QAction::triggered, [this]{ Avatar::setShowOtherLookAtVectors(isOptionChecked(MenuOption::ShowOtherLookAtVectors)); }); + auto avatarManager = DependencyManager::get(); + auto avatar = avatarManager->getMyAvatar(); + action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableLookAtSnapping, 0, true); connect(action, &QAction::triggered, [this, avatar]{ avatar->setProperty("lookAtSnappingEnabled", isOptionChecked(MenuOption::EnableLookAtSnapping)); @@ -818,6 +772,9 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::VerboseLogging, 0, false, qApp, SLOT(updateVerboseLogging())); + // Developer > Show Overlays + addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Overlays, 0, true); + #if 0 /// -------------- REMOVED FOR NOW -------------- addDisabledActionAndSeparator(navigateMenu, "History"); QAction* backAction = addActionToQMenuAndActionHash(navigateMenu, MenuOption::Back, 0, addressManager.data(), SLOT(goBack())); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index ed2eb6a2db..40530b7701 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -14,6 +14,9 @@ #include #include +#include + +#include "AvatarLogging.h" #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push @@ -36,13 +39,13 @@ #include #include #include -#include #include #include "Application.h" #include "InterfaceLogging.h" #include "Menu.h" #include "MyAvatar.h" +#include "OtherAvatar.h" #include "SceneScriptingInterface.h" // 50 times per second - target is 45hz, but this helps account for any small deviations @@ -54,6 +57,13 @@ static const quint64 MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS = USECS_PER_SECOND / // We add _myAvatar into the hash with all the other AvatarData, and we use the default NULL QUid as the key. const QUuid MY_AVATAR_KEY; // NULL key +namespace { + // For an unknown avatar-data packet, wait this long before requesting the identity. + constexpr std::chrono::milliseconds REQUEST_UNKNOWN_IDENTITY_DELAY { 5 * 1000 }; + constexpr int REQUEST_UNKNOWN_IDENTITY_TRANSMITS = 3; +} +using std::chrono::steady_clock; + AvatarManager::AvatarManager(QObject* parent) : _avatarsToFade(), _myAvatar(new MyAvatar(qApp->thread()), [](MyAvatar* ptr) { ptr->deleteLater(); }) @@ -118,6 +128,7 @@ void AvatarManager::updateMyAvatar(float deltaTime) { _lastSendAvatarDataTime = now; _myAvatarSendRate.increment(); } + } @@ -192,6 +203,15 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { while (!sortedAvatars.empty()) { const SortableAvatar& sortData = sortedAvatars.top(); const auto avatar = std::static_pointer_cast(sortData.getAvatar()); + const auto otherAvatar = std::static_pointer_cast(sortData.getAvatar()); + + // if the geometry is loaded then turn off the orb + if (avatar->getSkeletonModel()->isLoaded()) { + // remove the orb if it is there + otherAvatar->removeOrb(); + } else { + otherAvatar->updateOrbPosition(); + } bool ignoring = DependencyManager::get()->isPersonalMutingNode(avatar->getID()); if (ignoring) { @@ -277,6 +297,28 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { simulateAvatarFades(deltaTime); + // Check on avatars with pending identities: + steady_clock::time_point now = steady_clock::now(); + QWriteLocker writeLock(&_hashLock); + for (auto pendingAvatar = _pendingAvatars.begin(); pendingAvatar != _pendingAvatars.end(); ++pendingAvatar) { + if (now - pendingAvatar->creationTime >= REQUEST_UNKNOWN_IDENTITY_DELAY) { + // Too long without an ID + sendIdentityRequest(pendingAvatar->avatar->getID()); + if (++pendingAvatar->transmits >= REQUEST_UNKNOWN_IDENTITY_TRANSMITS) { + qCDebug(avatars) << "Requesting identity for unknown avatar (final request)" << + pendingAvatar->avatar->getID().toString(); + + pendingAvatar = _pendingAvatars.erase(pendingAvatar); + if (pendingAvatar == _pendingAvatars.end()) { + break; + } + } else { + pendingAvatar->creationTime = now; + qCDebug(avatars) << "Requesting identity for unknown avatar" << pendingAvatar->avatar->getID().toString(); + } + } + } + _avatarSimulationTime = (float)(usecTimestampNow() - startTime) / (float)USECS_PER_MSEC; } @@ -289,6 +331,20 @@ void AvatarManager::postUpdate(float deltaTime, const render::ScenePointer& scen } } +void AvatarManager::sendIdentityRequest(const QUuid& avatarID) const { + auto nodeList = DependencyManager::get(); + nodeList->eachMatchingNode( + [&](const SharedNodePointer& node)->bool { + return node->getType() == NodeType::AvatarMixer && node->getActiveSocket(); + }, + [&](const SharedNodePointer& node) { + auto packet = NLPacket::create(PacketType::AvatarIdentityRequest, NUM_BYTES_RFC4122_UUID, true); + packet->write(avatarID.toRfc4122()); + nodeList->sendPacket(std::move(packet), *node); + ++_identityRequestsSent; + }); +} + void AvatarManager::simulateAvatarFades(float deltaTime) { if (_avatarsToFade.empty()) { return; @@ -684,3 +740,49 @@ void AvatarManager::setAvatarSortCoefficient(const QString& name, const QScriptV DependencyManager::get()->broadcastToNodes(std::move(packet), NodeSet() << NodeType::AvatarMixer); } } + + QVariantMap AvatarManager::getPalData(const QList specificAvatarIdentifiers) { + QJsonArray palData; + + auto avatarMap = getHashCopy(); + AvatarHash::iterator itr = avatarMap.begin(); + while (itr != avatarMap.end()) { + std::shared_ptr avatar = std::static_pointer_cast(*itr); + QString currentSessionUUID = avatar->getSessionUUID().toString(); + if (specificAvatarIdentifiers.isEmpty() || specificAvatarIdentifiers.contains(currentSessionUUID)) { + QJsonObject thisAvatarPalData; + + auto myAvatar = DependencyManager::get()->getMyAvatar(); + + if (currentSessionUUID == myAvatar->getSessionUUID().toString()) { + currentSessionUUID = ""; + } + + thisAvatarPalData.insert("sessionUUID", currentSessionUUID); + thisAvatarPalData.insert("sessionDisplayName", avatar->getSessionDisplayName()); + thisAvatarPalData.insert("audioLoudness", avatar->getAudioLoudness()); + thisAvatarPalData.insert("isReplicated", avatar->getIsReplicated()); + + glm::vec3 position = avatar->getWorldPosition(); + QJsonObject jsonPosition; + jsonPosition.insert("x", position.x); + jsonPosition.insert("y", position.y); + jsonPosition.insert("z", position.z); + thisAvatarPalData.insert("position", jsonPosition); + + float palOrbOffset = 0.2f; + int headIndex = avatar->getJointIndex("Head"); + if (headIndex > 0) { + glm::vec3 jointTranslation = avatar->getAbsoluteJointTranslationInObjectFrame(headIndex); + palOrbOffset = jointTranslation.y / 2; + } + thisAvatarPalData.insert("palOrbOffset", palOrbOffset); + + palData.append(thisAvatarPalData); + } + ++itr; + } + QJsonObject doc; + doc.insert("data", palData); + return doc.toVariantMap(); +} diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 9d9466264b..ecf9a2d735 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -82,6 +82,7 @@ public: void updateMyAvatar(float deltaTime); void updateOtherAvatars(float deltaTime); + void sendIdentityRequest(const QUuid& avatarID) const; void postUpdate(float deltaTime, const render::ScenePointer& scene); @@ -160,7 +161,19 @@ public: */ Q_INVOKABLE void setAvatarSortCoefficient(const QString& name, const QScriptValue& value); + /**jsdoc + * Used in the PAL for getting PAL-related data about avatars nearby. Using this method is faster + * than iterating over each avatar and obtaining data about them in JavaScript, as that method + * locks and unlocks each avatar's data structure potentially hundreds of times per update tick. + * @function AvatarManager.getPalData + * @param {string[]} specificAvatarIdentifiers - A list of specific Avatar Identifiers about + * which you want to get PAL data + * @returns {object} + */ + Q_INVOKABLE QVariantMap getPalData(const QList specificAvatarIdentifiers = QList()); + float getMyAvatarSendRate() const { return _myAvatarSendRate.rate(); } + int getIdentityRequestsSent() const { return _identityRequestsSent; } public slots: @@ -198,6 +211,7 @@ private: int _numAvatarsNotUpdated { 0 }; float _avatarSimulationTime { 0.0f }; bool _shouldRender { true }; + mutable int _identityRequestsSent { 0 }; }; #endif // hifi_AvatarManager_h diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 76714170fb..fd121055a1 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -88,6 +88,10 @@ const float MyAvatar::ZOOM_MIN = 0.5f; const float MyAvatar::ZOOM_MAX = 25.0f; const float MyAvatar::ZOOM_DEFAULT = 1.5f; const float MIN_SCALE_CHANGED_DELTA = 0.001f; +const int MODE_READINGS_RING_BUFFER_SIZE = 500; +const float CENTIMETERS_PER_METER = 100.0f; + +//#define DEBUG_DRAW_HMD_MOVING_AVERAGE MyAvatar::MyAvatar(QThread* thread) : Avatar(thread), @@ -108,6 +112,7 @@ MyAvatar::MyAvatar(QThread* thread) : _hmdSensorMatrix(), _hmdSensorOrientation(), _hmdSensorPosition(), + _recentModeReadings(MODE_READINGS_RING_BUFFER_SIZE), _bodySensorMatrix(), _goToPending(false), _goToPosition(), @@ -257,6 +262,26 @@ void MyAvatar::setDominantHand(const QString& hand) { } } +void MyAvatar::requestDisableHandTouch() { + std::lock_guard guard(_disableHandTouchMutex); + _disableHandTouchCount++; + emit shouldDisableHandTouchChanged(_disableHandTouchCount > 0); +} + +void MyAvatar::requestEnableHandTouch() { + std::lock_guard guard(_disableHandTouchMutex); + _disableHandTouchCount = std::max(_disableHandTouchCount - 1, 0); + emit shouldDisableHandTouchChanged(_disableHandTouchCount > 0); +} + +void MyAvatar::disableHandTouchForID(const QUuid& entityID) { + emit disableHandTouchForIDChanged(entityID, true); +} + +void MyAvatar::enableHandTouchForID(const QUuid& entityID) { + emit disableHandTouchForIDChanged(entityID, false); +} + void MyAvatar::registerMetaTypes(ScriptEnginePointer engine) { QScriptValue value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); engine->globalObject().setProperty("MyAvatar", value); @@ -414,7 +439,8 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { void MyAvatar::update(float deltaTime) { // update moving average of HMD facing in xz plane. - const float HMD_FACING_TIMESCALE = 4.0f; // very slow average + const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); + float tau = deltaTime / HMD_FACING_TIMESCALE; _headControllerFacingMovingAverage = lerp(_headControllerFacingMovingAverage, _headControllerFacing, tau); @@ -423,6 +449,12 @@ void MyAvatar::update(float deltaTime) { _smoothOrientationTimer += deltaTime; } + float newHeightReading = getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y; + int newHeightReadingInCentimeters = glm::floor(newHeightReading * CENTIMETERS_PER_METER); + _recentModeReadings.insert(newHeightReadingInCentimeters); + setCurrentStandingHeight(computeStandingHeightMode(getControllerPoseInAvatarFrame(controller::Action::HEAD))); + setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); + #ifdef DEBUG_DRAW_HMD_MOVING_AVERAGE auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); glm::vec3 worldHeadPos = transformPoint(getSensorToWorldMatrix(), sensorHeadPose.getTranslation()); @@ -714,7 +746,8 @@ void MyAvatar::simulate(float deltaTime) { entityTree->recurseTreeWithOperator(&moveOperator); } }); - _characterController.setFlyingAllowed(zoneAllowsFlying && _enableFlying); + bool isPhysicsEnabled = qApp->isPhysicsEnabled(); + _characterController.setFlyingAllowed((zoneAllowsFlying && _enableFlying) || !isPhysicsEnabled); _characterController.setCollisionlessAllowed(collisionlessAllowed); } @@ -1097,8 +1130,8 @@ void MyAvatar::saveData() { settings.setValue("displayName", _displayName); settings.setValue("collisionSoundURL", _collisionSoundURL); settings.setValue("useSnapTurn", _useSnapTurn); - settings.setValue("clearOverlayWhenMoving", _clearOverlayWhenMoving); settings.setValue("userHeight", getUserHeight()); + settings.setValue("enabledFlying", getFlyingEnabled()); settings.endGroup(); } @@ -1248,11 +1281,10 @@ void MyAvatar::loadData() { settings.remove("avatarEntityData"); } setAvatarEntityDataChanged(true); - + setFlyingEnabled(settings.value("enabledFlying").toBool()); setDisplayName(settings.value("displayName").toString()); setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString()); setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool()); - setClearOverlayWhenMoving(settings.value("clearOverlayWhenMoving", _clearOverlayWhenMoving).toBool()); setDominantHand(settings.value("dominantHand", _dominantHand).toString().toLower()); setUserHeight(settings.value("userHeight", DEFAULT_AVATAR_HEIGHT).toDouble()); settings.endGroup(); @@ -1591,18 +1623,26 @@ void MyAvatar::removeAvatarEntities() { QVariantList MyAvatar::getAvatarEntitiesVariant() { QVariantList avatarEntitiesData; QScriptEngine scriptEngine; - forEachChild([&](SpatiallyNestablePointer child) { - if (child->getNestableType() == NestableType::Entity) { - auto modelEntity = std::dynamic_pointer_cast(child); - if (modelEntity) { - QVariantMap avatarEntityData; - EntityItemProperties entityProperties = modelEntity->getProperties(); - QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(&scriptEngine, entityProperties); - avatarEntityData["properties"] = scriptProperties.toVariant(); - avatarEntitiesData.append(QVariant(avatarEntityData)); + auto treeRenderer = DependencyManager::get(); + EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; + if (entityTree) { + AvatarEntityMap avatarEntities = getAvatarEntityData(); + for (auto entityID : avatarEntities.keys()) { + auto entity = entityTree->findEntityByID(entityID); + if (!entity) { + continue; } + QVariantMap avatarEntityData; + EncodeBitstreamParams params; + auto desiredProperties = entity->getEntityProperties(params); + desiredProperties += PROP_LOCAL_POSITION; + desiredProperties += PROP_LOCAL_ROTATION; + EntityItemProperties entityProperties = entity->getProperties(desiredProperties); + QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(&scriptEngine, entityProperties); + avatarEntityData["properties"] = scriptProperties.toVariant(); + avatarEntitiesData.append(QVariant(avatarEntityData)); } - }); + } return avatarEntitiesData; } @@ -1979,7 +2019,6 @@ QUrl MyAvatar::getAnimGraphUrl() const { } void MyAvatar::setAnimGraphUrl(const QUrl& url) { - if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setAnimGraphUrl", Q_ARG(QUrl, url)); return; @@ -1988,6 +2027,9 @@ void MyAvatar::setAnimGraphUrl(const QUrl& url) { if (_currentAnimGraphUrl.get() == url) { return; } + + emit animGraphUrlChanged(url); + destroyAnimGraph(); _skeletonModel->reset(); // Why is this necessary? Without this, we crash in the next render. @@ -2172,6 +2214,15 @@ void MyAvatar::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) _headData->setHasAudioEnabledFaceMovement(hasAudioEnabledFaceMovement); } +void MyAvatar::setRotationRecenterFilterLength(float length) { + const float MINIMUM_ROTATION_RECENTER_FILTER_LENGTH = 0.01f; + _rotationRecenterFilterLength = std::max(MINIMUM_ROTATION_RECENTER_FILTER_LENGTH, length); +} + +void MyAvatar::setRotationThreshold(float angleRadians) { + _rotationThreshold = angleRadians; +} + void MyAvatar::updateOrientation(float deltaTime) { // Smoothly rotate body with arrow keys @@ -2812,6 +2863,7 @@ void MyAvatar::setCollisionsEnabled(bool enabled) { } _characterController.setCollisionless(!enabled); + emit collisionsEnabledChanged(enabled); } bool MyAvatar::getCollisionsEnabled() { @@ -3135,6 +3187,148 @@ glm::mat4 MyAvatar::deriveBodyUsingCgModel() const { return worldToSensorMat * avatarToWorldMat * avatarHipsMat; } +static bool isInsideLine(const glm::vec3& a, const glm::vec3& b, const glm::vec3& c) { + return (((b.x - a.x) * (c.z - a.z) - (b.z - a.z) * (c.x - a.x)) > 0); +} + +static bool withinBaseOfSupport(const controller::Pose& head) { + float userScale = 1.0f; + + glm::vec3 frontLeft(-DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, -DEFAULT_AVATAR_ANTERIOR_STEPPING_THRESHOLD); + glm::vec3 frontRight(DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, -DEFAULT_AVATAR_ANTERIOR_STEPPING_THRESHOLD); + glm::vec3 backLeft(-DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, DEFAULT_AVATAR_POSTERIOR_STEPPING_THRESHOLD); + glm::vec3 backRight(DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, DEFAULT_AVATAR_POSTERIOR_STEPPING_THRESHOLD); + + bool isWithinSupport = false; + if (head.isValid()) { + bool withinFrontBase = isInsideLine(userScale * frontLeft, userScale * frontRight, head.getTranslation()); + bool withinBackBase = isInsideLine(userScale * backRight, userScale * backLeft, head.getTranslation()); + bool withinLateralBase = (isInsideLine(userScale * frontRight, userScale * backRight, head.getTranslation()) && + isInsideLine(userScale * backLeft, userScale * frontLeft, head.getTranslation())); + isWithinSupport = (withinFrontBase && withinBackBase && withinLateralBase); + } + return isWithinSupport; +} + +static bool headAngularVelocityBelowThreshold(const controller::Pose& head) { + glm::vec3 xzPlaneAngularVelocity(0.0f, 0.0f, 0.0f); + if (head.isValid()) { + xzPlaneAngularVelocity.x = head.getAngularVelocity().x; + xzPlaneAngularVelocity.z = head.getAngularVelocity().z; + } + float magnitudeAngularVelocity = glm::length(xzPlaneAngularVelocity); + bool isBelowThreshold = (magnitudeAngularVelocity < DEFAULT_AVATAR_HEAD_ANGULAR_VELOCITY_STEPPING_THRESHOLD); + + return isBelowThreshold; +} + +static bool isWithinThresholdHeightMode(const controller::Pose& head,const float& newMode) { + bool isWithinThreshold = true; + if (head.isValid()) { + isWithinThreshold = (head.getTranslation().y - newMode) > DEFAULT_AVATAR_MODE_HEIGHT_STEPPING_THRESHOLD; + } + return isWithinThreshold; +} + +float MyAvatar::computeStandingHeightMode(const controller::Pose& head) { + const float MODE_CORRECTION_FACTOR = 0.02f; + int greatestFrequency = 0; + int mode = 0; + // init mode in meters to the current mode + float modeInMeters = getCurrentStandingHeight(); + if (head.isValid()) { + std::map freq; + for(auto recentModeReadingsIterator = _recentModeReadings.begin(); recentModeReadingsIterator != _recentModeReadings.end(); ++recentModeReadingsIterator) { + freq[*recentModeReadingsIterator] += 1; + if (freq[*recentModeReadingsIterator] > greatestFrequency) { + greatestFrequency = freq[*recentModeReadingsIterator]; + mode = *recentModeReadingsIterator; + } + } + + modeInMeters = ((float)mode) / CENTIMETERS_PER_METER; + if (!(modeInMeters > getCurrentStandingHeight())) { + // if not greater check for a reset + if (getResetMode() && qApp->isHMDMode()) { + setResetMode(false); + float resetModeInCentimeters = glm::floor((head.getTranslation().y - MODE_CORRECTION_FACTOR)*CENTIMETERS_PER_METER); + modeInMeters = (resetModeInCentimeters / CENTIMETERS_PER_METER); + _recentModeReadings.clear(); + + } else { + // if not greater and no reset, keep the mode as it is + modeInMeters = getCurrentStandingHeight(); + + } + } + } + return modeInMeters; +} + +static bool handDirectionMatchesHeadDirection(const controller::Pose& leftHand, const controller::Pose& rightHand, const controller::Pose& head) { + const float VELOCITY_EPSILON = 0.02f; + bool leftHandDirectionMatchesHead = true; + bool rightHandDirectionMatchesHead = true; + glm::vec3 xzHeadVelocity(head.velocity.x, 0.0f, head.velocity.z); + if (leftHand.isValid() && head.isValid()) { + glm::vec3 xzLeftHandVelocity(leftHand.velocity.x, 0.0f, leftHand.velocity.z); + if ((glm::length(xzLeftHandVelocity) > VELOCITY_EPSILON) && (glm::length(xzHeadVelocity) > VELOCITY_EPSILON)) { + float handDotHeadLeft = glm::dot(glm::normalize(xzLeftHandVelocity), glm::normalize(xzHeadVelocity)); + leftHandDirectionMatchesHead = ((handDotHeadLeft > DEFAULT_HANDS_VELOCITY_DIRECTION_STEPPING_THRESHOLD)); + } else { + leftHandDirectionMatchesHead = false; + } + } + if (rightHand.isValid() && head.isValid()) { + glm::vec3 xzRightHandVelocity(rightHand.velocity.x, 0.0f, rightHand.velocity.z); + if ((glm::length(xzRightHandVelocity) > VELOCITY_EPSILON) && (glm::length(xzHeadVelocity) > VELOCITY_EPSILON)) { + float handDotHeadRight = glm::dot(glm::normalize(xzRightHandVelocity), glm::normalize(xzHeadVelocity)); + rightHandDirectionMatchesHead = (handDotHeadRight > DEFAULT_HANDS_VELOCITY_DIRECTION_STEPPING_THRESHOLD); + } else { + rightHandDirectionMatchesHead = false; + } + } + return leftHandDirectionMatchesHead && rightHandDirectionMatchesHead; +} + +static bool handAngularVelocityBelowThreshold(const controller::Pose& leftHand, const controller::Pose& rightHand) { + float leftHandXZAngularVelocity = 0.0f; + float rightHandXZAngularVelocity = 0.0f; + if (leftHand.isValid()) { + glm::vec3 xzLeftHandAngularVelocity(leftHand.angularVelocity.x, 0.0f, leftHand.angularVelocity.z); + leftHandXZAngularVelocity = glm::length(xzLeftHandAngularVelocity); + } + if (rightHand.isValid()) { + glm::vec3 xzRightHandAngularVelocity(rightHand.angularVelocity.x, 0.0f, rightHand.angularVelocity.z); + rightHandXZAngularVelocity = glm::length(xzRightHandAngularVelocity); + } + return ((leftHandXZAngularVelocity < DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD) && + (rightHandXZAngularVelocity < DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD)); +} + +static bool headVelocityGreaterThanThreshold(const controller::Pose& head) { + float headVelocityMagnitude = 0.0f; + if (head.isValid()) { + headVelocityMagnitude = glm::length(head.getVelocity()); + } + return headVelocityMagnitude > DEFAULT_HEAD_VELOCITY_STEPPING_THRESHOLD; +} + +glm::quat MyAvatar::computeAverageHeadRotation(const controller::Pose& head) { + const float AVERAGING_RATE = 0.03f; + return safeLerp(_averageHeadRotation, head.getRotation(), AVERAGING_RATE); +} + +static bool isHeadLevel(const controller::Pose& head, const glm::quat& averageHeadRotation) { + glm::vec3 diffFromAverageEulers(0.0f, 0.0f, 0.0f); + if (head.isValid()) { + glm::vec3 averageHeadEulers = glm::degrees(safeEulerAngles(averageHeadRotation)); + glm::vec3 currentHeadEulers = glm::degrees(safeEulerAngles(head.getRotation())); + diffFromAverageEulers = averageHeadEulers - currentHeadEulers; + } + return ((fabs(diffFromAverageEulers.x) < DEFAULT_HEAD_PITCH_STEPPING_TOLERANCE) && (fabs(diffFromAverageEulers.z) < DEFAULT_HEAD_ROLL_STEPPING_TOLERANCE)); +} + float MyAvatar::getUserHeight() const { return _userHeight.get(); } @@ -3301,7 +3495,7 @@ void MyAvatar::FollowHelper::decrementTimeRemaining(float dt) { } bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { - const float FOLLOW_ROTATION_THRESHOLD = cosf(PI / 6.0f); // 30 degrees + const float FOLLOW_ROTATION_THRESHOLD = cosf(myAvatar.getRotationThreshold()); glm::vec2 bodyFacing = getFacingDir2D(currentBodyMatrix); return glm::dot(-myAvatar.getHeadControllerFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD; } @@ -3328,7 +3522,37 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, } return fabs(lateralLeanAmount) > MAX_LATERAL_LEAN; +} +bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) const { + + // get the current readings + controller::Pose currentHeadPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD); + controller::Pose currentLeftHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND); + controller::Pose currentRightHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND); + + bool stepDetected = false; + if (!withinBaseOfSupport(currentHeadPose) && + headAngularVelocityBelowThreshold(currentHeadPose) && + isWithinThresholdHeightMode(currentHeadPose, myAvatar.getCurrentStandingHeight()) && + handDirectionMatchesHeadDirection(currentLeftHandPose, currentRightHandPose, currentHeadPose) && + handAngularVelocityBelowThreshold(currentLeftHandPose, currentRightHandPose) && + headVelocityGreaterThanThreshold(currentHeadPose) && + isHeadLevel(currentHeadPose, myAvatar.getAverageHeadRotation())) { + // a step is detected + stepDetected = true; + } else { + glm::vec3 defaultHipsPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); + glm::vec3 defaultHeadPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Head")); + glm::vec3 currentHeadPosition = currentHeadPose.getTranslation(); + float anatomicalHeadToHipsDistance = glm::length(defaultHeadPosition - defaultHipsPosition); + if (!isActive(Horizontal) && + (glm::length(currentHeadPosition - defaultHipsPosition) > (anatomicalHeadToHipsDistance + DEFAULT_AVATAR_SPINE_STRETCH_LIMIT))) { + myAvatar.setResetMode(true); + stepDetected = true; + } + } + return stepDetected; } bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { @@ -3347,16 +3571,25 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) { if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); + myAvatar.setHeadControllerFacingMovingAverage(myAvatar._headControllerFacing); } - if (!isActive(Horizontal) && shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix)) { - activate(Horizontal); + if (myAvatar.getCenterOfGravityModelEnabled()) { + if (!isActive(Horizontal) && (shouldActivateHorizontalCG(myAvatar) || hasDriveInput)) { + activate(Horizontal); + } + } else { + if (!isActive(Horizontal) && (shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { + activate(Horizontal); + } } + if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Vertical); } } else { if (!isActive(Rotation) && getForceActivateRotation()) { activate(Rotation); + myAvatar.setHeadControllerFacingMovingAverage(myAvatar._headControllerFacing); setForceActivateRotation(false); } if (!isActive(Horizontal) && getForceActivateHorizontal()) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 7cb1cfb4b4..9b5ddd360d 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -31,6 +31,7 @@ #include "AtRestDetector.h" #include "MyCharacterController.h" +#include "RingBufferHistory.h" #include class AvatarActionHold; @@ -195,6 +196,8 @@ class MyAvatar : public Avatar { Q_PROPERTY(bool hasProceduralBlinkFaceMovement READ getHasProceduralBlinkFaceMovement WRITE setHasProceduralBlinkFaceMovement) Q_PROPERTY(bool hasProceduralEyeFaceMovement READ getHasProceduralEyeFaceMovement WRITE setHasProceduralEyeFaceMovement) Q_PROPERTY(bool hasAudioEnabledFaceMovement READ getHasAudioEnabledFaceMovement WRITE setHasAudioEnabledFaceMovement) + Q_PROPERTY(float rotationRecenterFilterLength READ getRotationRecenterFilterLength WRITE setRotationRecenterFilterLength) + Q_PROPERTY(float rotationThreshold READ getRotationThreshold WRITE setRotationThreshold) //TODO: make gravity feature work Q_PROPERTY(glm::vec3 gravity READ getGravity WRITE setGravity) Q_PROPERTY(glm::vec3 leftHandPosition READ getLeftHandPosition) @@ -250,7 +253,7 @@ public: Q_ENUM(DriveKeys) explicit MyAvatar(QThread* thread); - ~MyAvatar(); + virtual ~MyAvatar(); void instantiableAvatar() override {}; void registerMetaTypes(ScriptEnginePointer engine); @@ -469,16 +472,6 @@ public: * @param {boolean} on */ Q_INVOKABLE void setSnapTurn(bool on) { _useSnapTurn = on; } - /**jsdoc - * @function MyAvatar.getClearOverlayWhenMoving - * @returns {boolean} - */ - Q_INVOKABLE bool getClearOverlayWhenMoving() const { return _clearOverlayWhenMoving; } - /**jsdoc - * @function MyAvatar.setClearOverlayWhenMoving - * @param {boolean} on - */ - Q_INVOKABLE void setClearOverlayWhenMoving(bool on) { _clearOverlayWhenMoving = on; } /**jsdoc @@ -512,6 +505,28 @@ public: * @returns {boolean} */ Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } + /**jsdoc + * Request to enable hand touch effect globally + * @function MyAvatar.requestEnableHandTouch + */ + Q_INVOKABLE void requestEnableHandTouch(); + /**jsdoc + * Request to disable hand touch effect globally + * @function MyAvatar.requestDisableHandTouch + */ + Q_INVOKABLE void requestDisableHandTouch(); + /**jsdoc + * Disables hand touch effect on a specific entity + * @function MyAvatar.disableHandTouchForID + * @param {Uuid} entityID - ID of the entity that will disable hand touch effect + */ + Q_INVOKABLE void disableHandTouchForID(const QUuid& entityID); + /**jsdoc + * Enables hand touch effect on a specific entity + * @function MyAvatar.enableHandTouchForID + * @param {Uuid} entityID - ID of the entity that will enable hand touch effect + */ + Q_INVOKABLE void enableHandTouchForID(const QUuid& entityID); bool useAdvancedMovementControls() const { return _useAdvancedMovementControls.get(); } void setUseAdvancedMovementControls(bool useAdvancedMovementControls) @@ -890,6 +905,13 @@ public: virtual void rebuildCollisionShape() override; const glm::vec2& getHeadControllerFacingMovingAverage() const { return _headControllerFacingMovingAverage; } + void setHeadControllerFacingMovingAverage(glm::vec2 currentHeadControllerFacing) { _headControllerFacingMovingAverage = currentHeadControllerFacing; } + float getCurrentStandingHeight() const { return _currentStandingHeight; } + void setCurrentStandingHeight(float newMode) { _currentStandingHeight = newMode; } + const glm::quat getAverageHeadRotation() const { return _averageHeadRotation; } + void setAverageHeadRotation(glm::quat rotation) { _averageHeadRotation = rotation; } + bool getResetMode() const { return _resetMode; } + void setResetMode(bool hasBeenReset) { _resetMode = hasBeenReset; } void setControllerPoseInSensorFrame(controller::Action action, const controller::Pose& pose); controller::Pose getControllerPoseInSensorFrame(controller::Action action) const; @@ -898,7 +920,12 @@ public: bool hasDriveInput() const; - QVariantList getAvatarEntitiesVariant(); + /**jsdoc + * Function returns list of avatar entities + * @function MyAvatar.getAvatarEntitiesVariant() + * @returns {object[]} + */ + Q_INVOKABLE QVariantList getAvatarEntitiesVariant(); void removeAvatarEntities(); /**jsdoc @@ -1025,6 +1052,9 @@ public: bool isReadyForPhysics() const; + float computeStandingHeightMode(const controller::Pose& head); + glm::quat computeAverageHeadRotation(const controller::Pose& head); + public slots: /**jsdoc @@ -1304,6 +1334,22 @@ signals: */ void collisionWithEntity(const Collision& collision); + /**jsdoc + * Triggered when collisions with avatar enabled or disabled + * @function MyAvatar.collisionsEnabledChanged + * @param {boolean} enabled + * @returns {Signal} + */ + void collisionsEnabledChanged(bool enabled); + + /**jsdoc + * Triggered when avatar's animation url changes + * @function MyAvatar.animGraphUrlChanged + * @param {url} url + * @returns {Signal} + */ + void animGraphUrlChanged(const QUrl& url); + /**jsdoc * @function MyAvatar.energyChanged * @param {number} energy @@ -1368,6 +1414,23 @@ signals: */ void scaleChanged(); + /**jsdoc + * Triggered when hand touch is globally enabled or disabled + * @function MyAvatar.shouldDisableHandTouchChanged + * @param {boolean} shouldDisable + * @returns {Signal} + */ + void shouldDisableHandTouchChanged(bool shouldDisable); + + /**jsdoc + * Triggered when hand touch is enabled or disabled for an specific entity + * @function MyAvatar.disableHandTouchForIDChanged + * @param {Uuid} entityID - ID of the entity that will enable hand touch effect + * @param {boolean} disable + * @returns {Signal} + */ + void disableHandTouchForIDChanged(const QUuid& entityID, bool disable); + private slots: void leaveDomain(); @@ -1397,6 +1460,10 @@ private: bool getHasProceduralEyeFaceMovement() const override { return _headData->getHasProceduralEyeFaceMovement(); } void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement); bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); } + void setRotationRecenterFilterLength(float length); + float getRotationRecenterFilterLength() const { return _rotationRecenterFilterLength; } + void setRotationThreshold(float angleRadians); + float getRotationThreshold() const { return _rotationThreshold; } bool isMyAvatar() const override { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; @@ -1434,7 +1501,7 @@ private: std::array _driveKeys; std::bitset _disabledDriveKeys; - bool _enableFlying { true }; + bool _enableFlying { false }; bool _wasPushing { false }; bool _isPushing { false }; bool _isBeingPushed { false }; @@ -1496,7 +1563,6 @@ private: ThreadSafeValueCache _prefOverrideAnimGraphUrl; QUrl _fstAnimGraphOverrideUrl; bool _useSnapTurn { true }; - bool _clearOverlayWhenMoving { true }; QString _dominantHand { DOMINANT_RIGHT_HAND }; const float ROLL_CONTROL_DEAD_ZONE_DEFAULT = 8.0f; // degrees @@ -1506,6 +1572,8 @@ private: float _hmdRollControlDeadZone { ROLL_CONTROL_DEAD_ZONE_DEFAULT }; float _hmdRollControlRate { ROLL_CONTROL_RATE_DEFAULT }; std::atomic _hasScriptedBlendShapes { false }; + std::atomic _rotationRecenterFilterLength { 4.0f }; + std::atomic _rotationThreshold { 0.5235f }; // 30 degrees in radians // working copy -- see AvatarData for thread-safe _sensorToWorldMatrixCache, used for outward facing access glm::mat4 _sensorToWorldMatrix { glm::mat4() }; @@ -1517,6 +1585,11 @@ private: // cache head controller pose in sensor space glm::vec2 _headControllerFacing; // facing vector in xz plane (sensor space) glm::vec2 _headControllerFacingMovingAverage { 0.0f, 0.0f }; // facing vector in xz plane (sensor space) + glm::quat _averageHeadRotation { 0.0f, 0.0f, 0.0f, 1.0f }; + + float _currentStandingHeight { 0.0f }; + bool _resetMode { true }; + RingBufferHistory _recentModeReadings; // cache of the current body position and orientation of the avatar's body, // in sensor space. @@ -1544,6 +1617,7 @@ private: bool shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; bool shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; bool shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; + bool shouldActivateHorizontalCG(MyAvatar& myAvatar) const; void prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& bodySensorMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput); glm::mat4 postPhysicsUpdate(const MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix); bool getForceActivateRotation() const; @@ -1593,6 +1667,7 @@ private: // all poses are in sensor-frame std::map _controllerPoseMap; mutable std::mutex _controllerPoseMapMutex; + mutable std::mutex _disableHandTouchMutex; bool _centerOfGravityModelEnabled { true }; bool _hmdLeanRecenterEnabled { true }; @@ -1632,7 +1707,8 @@ private: // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; - bool _haveReceivedHeightLimitsFromDomain = { false }; + bool _haveReceivedHeightLimitsFromDomain { false }; + int _disableHandTouchCount { 0 }; }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index c15b00ca19..0fc5e7521e 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -46,7 +46,7 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { } glm::mat4 hipsMat; - if (myAvatar->getCenterOfGravityModelEnabled()) { + if (myAvatar->getCenterOfGravityModelEnabled() && !isFlying) { // then we use center of gravity model hipsMat = myAvatar->deriveBodyUsingCgModel(); } else { @@ -109,6 +109,11 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { AnimPose avatarToRigPose(glm::vec3(1.0f), Quaternions::Y_180, glm::vec3(0.0f)); + glm::mat4 rigToAvatarMatrix = Matrices::Y_180; + glm::mat4 avatarToWorldMatrix = createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()); + glm::mat4 sensorToWorldMatrix = myAvatar->getSensorToWorldMatrix(); + params.rigToSensorMatrix = glm::inverse(sensorToWorldMatrix) * avatarToWorldMatrix * rigToAvatarMatrix; + // input action is the highest priority source for head orientation. auto avatarHeadPose = myAvatar->getControllerPoseInAvatarFrame(controller::Action::HEAD); if (avatarHeadPose.isValid()) { diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp new file mode 100644 index 0000000000..5e51658128 --- /dev/null +++ b/interface/src/avatar/OtherAvatar.cpp @@ -0,0 +1,60 @@ +// +// Created by Bradley Austin Davis on 2017/04/27 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "OtherAvatar.h" +#include "Application.h" + +OtherAvatar::OtherAvatar(QThread* thread) : Avatar(thread) { + // give the pointer to our head to inherited _headData variable from AvatarData + _headData = new Head(this); + _skeletonModel = std::make_shared(this, nullptr); + _skeletonModel->setLoadingPriority(OTHERAVATAR_LOADING_PRIORITY); + connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished); + connect(_skeletonModel.get(), &Model::rigReady, this, &Avatar::rigReady); + connect(_skeletonModel.get(), &Model::rigReset, this, &Avatar::rigReset); + + // add the purple orb + createOrb(); +} + +OtherAvatar::~OtherAvatar() { + removeOrb(); +} + +void OtherAvatar::removeOrb() { + if (qApp->getOverlays().isAddedOverlay(_otherAvatarOrbMeshPlaceholderID)) { + qApp->getOverlays().deleteOverlay(_otherAvatarOrbMeshPlaceholderID); + } +} + +void OtherAvatar::updateOrbPosition() { + if (_otherAvatarOrbMeshPlaceholder != nullptr) { + _otherAvatarOrbMeshPlaceholder->setWorldPosition(getHead()->getPosition()); + } +} + +void OtherAvatar::createOrb() { + if (_otherAvatarOrbMeshPlaceholderID == UNKNOWN_OVERLAY_ID || + !qApp->getOverlays().isAddedOverlay(_otherAvatarOrbMeshPlaceholderID)) { + _otherAvatarOrbMeshPlaceholder = std::make_shared(); + _otherAvatarOrbMeshPlaceholder->setAlpha(1.0f); + _otherAvatarOrbMeshPlaceholder->setColor({ 0xFF, 0x00, 0xFF }); + _otherAvatarOrbMeshPlaceholder->setIsSolid(false); + _otherAvatarOrbMeshPlaceholder->setPulseMin(0.5); + _otherAvatarOrbMeshPlaceholder->setPulseMax(1.0); + _otherAvatarOrbMeshPlaceholder->setColorPulse(1.0); + _otherAvatarOrbMeshPlaceholder->setIgnoreRayIntersection(true); + _otherAvatarOrbMeshPlaceholder->setDrawInFront(false); + _otherAvatarOrbMeshPlaceholderID = qApp->getOverlays().addOverlay(_otherAvatarOrbMeshPlaceholder); + // Position focus + _otherAvatarOrbMeshPlaceholder->setWorldOrientation(glm::quat(0.0f, 0.0f, 0.0f, 1.0)); + _otherAvatarOrbMeshPlaceholder->setWorldPosition(getHead()->getPosition()); + _otherAvatarOrbMeshPlaceholder->setDimensions(glm::vec3(0.5f, 0.5f, 0.5f)); + _otherAvatarOrbMeshPlaceholder->setVisible(true); + } +} diff --git a/interface/src/avatar/OtherAvatar.h b/interface/src/avatar/OtherAvatar.h new file mode 100644 index 0000000000..f33952b78b --- /dev/null +++ b/interface/src/avatar/OtherAvatar.h @@ -0,0 +1,32 @@ +// +// Created by Bradley Austin Davis on 2017/04/27 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_OtherAvatar_h +#define hifi_OtherAvatar_h + +#include +#include "ui/overlays/Overlays.h" +#include "ui/overlays/Sphere3DOverlay.h" +#include "InterfaceLogging.h" + +class OtherAvatar : public Avatar { +public: + explicit OtherAvatar(QThread* thread); + virtual ~OtherAvatar(); + + virtual void instantiableAvatar() override { }; + virtual void createOrb() override; + void updateOrbPosition(); + void removeOrb(); + +protected: + std::shared_ptr _otherAvatarOrbMeshPlaceholder { nullptr }; + OverlayID _otherAvatarOrbMeshPlaceholderID { UNKNOWN_OVERLAY_ID }; +}; + +#endif // hifi_OtherAvatar_h diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 69698e82a6..702251f867 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -28,15 +28,15 @@ // account synthesizes a result {status: 'success', data: {keyStatus: "preexisting"|"conflicting"|"ok"}} -QJsonObject Ledger::apiResponse(const QString& label, QNetworkReply& reply) { - QByteArray response = reply.readAll(); +QJsonObject Ledger::apiResponse(const QString& label, QNetworkReply* reply) { + QByteArray response = reply->readAll(); QJsonObject data = QJsonDocument::fromJson(response).object(); qInfo(commerce) << label << "response" << QJsonDocument(data).toJson(QJsonDocument::Compact); return data; } // Non-200 responses are not json: -QJsonObject Ledger::failResponse(const QString& label, QNetworkReply& reply) { - QString response = reply.readAll(); +QJsonObject Ledger::failResponse(const QString& label, QNetworkReply* reply) { + QString response = reply->readAll(); qWarning(commerce) << "FAILED" << label << response; // tempResult will be NULL if the response isn't valid JSON. @@ -52,8 +52,8 @@ QJsonObject Ledger::failResponse(const QString& label, QNetworkReply& reply) { return tempResult.object(); } } -#define ApiHandler(NAME) void Ledger::NAME##Success(QNetworkReply& reply) { emit NAME##Result(apiResponse(#NAME, reply)); } -#define FailHandler(NAME) void Ledger::NAME##Failure(QNetworkReply& reply) { emit NAME##Result(failResponse(#NAME, reply)); } +#define ApiHandler(NAME) void Ledger::NAME##Success(QNetworkReply* reply) { emit NAME##Result(apiResponse(#NAME, reply)); } +#define FailHandler(NAME) void Ledger::NAME##Failure(QNetworkReply* reply) { emit NAME##Result(failResponse(#NAME, reply)); } #define Handler(NAME) ApiHandler(NAME) FailHandler(NAME) Handler(buy) Handler(receiveAt) @@ -68,7 +68,7 @@ Handler(updateItem) void Ledger::send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, QJsonObject request) { auto accountManager = DependencyManager::get(); const QString URL = "/api/v1/commerce/"; - JSONCallbackParameters callbackParams(this, success, this, fail); + JSONCallbackParameters callbackParams(this, success, fail); qCInfo(commerce) << "Sending" << endpoint << QJsonDocument(request).toJson(QJsonDocument::Compact); accountManager->sendRequest(URL + endpoint, authType, @@ -223,12 +223,12 @@ QString transactionString(const QJsonObject& valueObject) { } static const QString MARKETPLACE_ITEMS_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace/items/"; -void Ledger::historySuccess(QNetworkReply& reply) { +void Ledger::historySuccess(QNetworkReply* reply) { // here we send a historyResult with some extra stuff in it // Namely, the styled text we'd like to show. The issue is the // QML cannot do that easily since it doesn't know what the wallet // public key(s) are. Let's keep it that way - QByteArray response = reply.readAll(); + QByteArray response = reply->readAll(); QJsonObject data = QJsonDocument::fromJson(response).object(); qInfo(commerce) << "history" << "response" << QJsonDocument(data).toJson(QJsonDocument::Compact); @@ -262,7 +262,7 @@ void Ledger::historySuccess(QNetworkReply& reply) { emit historyResult(newData); } -void Ledger::historyFailure(QNetworkReply& reply) { +void Ledger::historyFailure(QNetworkReply* reply) { failResponse("history", reply); } @@ -273,10 +273,10 @@ void Ledger::history(const QStringList& keys, const int& pageNumber, const int& keysQuery("history", "historySuccess", "historyFailure", params); } -void Ledger::accountSuccess(QNetworkReply& reply) { +void Ledger::accountSuccess(QNetworkReply* reply) { // lets set the appropriate stuff in the wallet now auto wallet = DependencyManager::get(); - QByteArray response = reply.readAll(); + QByteArray response = reply->readAll(); QJsonObject data = QJsonDocument::fromJson(response).object()["data"].toObject(); auto salt = QByteArray::fromBase64(data["salt"].toString().toUtf8()); @@ -312,7 +312,7 @@ void Ledger::accountSuccess(QNetworkReply& reply) { emit accountResult(responseData); } -void Ledger::accountFailure(QNetworkReply& reply) { +void Ledger::accountFailure(QNetworkReply* reply) { failResponse("account", reply); } void Ledger::account() { @@ -320,8 +320,8 @@ void Ledger::account() { } // The api/failResponse is called just for the side effect of logging. -void Ledger::updateLocationSuccess(QNetworkReply& reply) { apiResponse("updateLocation", reply); } -void Ledger::updateLocationFailure(QNetworkReply& reply) { failResponse("updateLocation", reply); } +void Ledger::updateLocationSuccess(QNetworkReply* reply) { apiResponse("updateLocation", reply); } +void Ledger::updateLocationFailure(QNetworkReply* reply) { failResponse("updateLocation", reply); } void Ledger::updateLocation(const QString& asset_id, const QString& location, const bool& alsoUpdateSiblings, const bool controlledFailure) { auto wallet = DependencyManager::get(); auto walletScriptingInterface = DependencyManager::get(); @@ -349,11 +349,11 @@ void Ledger::updateLocation(const QString& asset_id, const QString& location, co } } -void Ledger::certificateInfoSuccess(QNetworkReply& reply) { +void Ledger::certificateInfoSuccess(QNetworkReply* reply) { auto wallet = DependencyManager::get(); auto accountManager = DependencyManager::get(); - QByteArray response = reply.readAll(); + QByteArray response = reply->readAll(); QJsonObject replyObject = QJsonDocument::fromJson(response).object(); QStringList keys = wallet->listPublicKeys(); @@ -366,7 +366,7 @@ void Ledger::certificateInfoSuccess(QNetworkReply& reply) { qInfo(commerce) << "certificateInfo" << "response" << QJsonDocument(replyObject).toJson(QJsonDocument::Compact); emit certificateInfoResult(replyObject); } -void Ledger::certificateInfoFailure(QNetworkReply& reply) { +void Ledger::certificateInfoFailure(QNetworkReply* reply) { emit certificateInfoResult(failResponse("certificateInfo", reply)); } void Ledger::certificateInfo(const QString& certificateId) { diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index 8a8fd2630a..ba2f167f4b 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -65,36 +65,36 @@ signals: void updateCertificateStatus(const QString& certID, uint certStatus); public slots: - void buySuccess(QNetworkReply& reply); - void buyFailure(QNetworkReply& reply); - void receiveAtSuccess(QNetworkReply& reply); - void receiveAtFailure(QNetworkReply& reply); - void balanceSuccess(QNetworkReply& reply); - void balanceFailure(QNetworkReply& reply); - void inventorySuccess(QNetworkReply& reply); - void inventoryFailure(QNetworkReply& reply); - void historySuccess(QNetworkReply& reply); - void historyFailure(QNetworkReply& reply); - void accountSuccess(QNetworkReply& reply); - void accountFailure(QNetworkReply& reply); - void updateLocationSuccess(QNetworkReply& reply); - void updateLocationFailure(QNetworkReply& reply); - void certificateInfoSuccess(QNetworkReply& reply); - void certificateInfoFailure(QNetworkReply& reply); - void transferAssetToNodeSuccess(QNetworkReply& reply); - void transferAssetToNodeFailure(QNetworkReply& reply); - void transferAssetToUsernameSuccess(QNetworkReply& reply); - void transferAssetToUsernameFailure(QNetworkReply& reply); - void alreadyOwnedSuccess(QNetworkReply& reply); - void alreadyOwnedFailure(QNetworkReply& reply); - void availableUpdatesSuccess(QNetworkReply& reply); - void availableUpdatesFailure(QNetworkReply& reply); - void updateItemSuccess(QNetworkReply& reply); - void updateItemFailure(QNetworkReply& reply); + void buySuccess(QNetworkReply* reply); + void buyFailure(QNetworkReply* reply); + void receiveAtSuccess(QNetworkReply* reply); + void receiveAtFailure(QNetworkReply* reply); + void balanceSuccess(QNetworkReply* reply); + void balanceFailure(QNetworkReply* reply); + void inventorySuccess(QNetworkReply* reply); + void inventoryFailure(QNetworkReply* reply); + void historySuccess(QNetworkReply* reply); + void historyFailure(QNetworkReply* reply); + void accountSuccess(QNetworkReply* reply); + void accountFailure(QNetworkReply* reply); + void updateLocationSuccess(QNetworkReply* reply); + void updateLocationFailure(QNetworkReply* reply); + void certificateInfoSuccess(QNetworkReply* reply); + void certificateInfoFailure(QNetworkReply* reply); + void transferAssetToNodeSuccess(QNetworkReply* reply); + void transferAssetToNodeFailure(QNetworkReply* reply); + void transferAssetToUsernameSuccess(QNetworkReply* reply); + void transferAssetToUsernameFailure(QNetworkReply* reply); + void alreadyOwnedSuccess(QNetworkReply* reply); + void alreadyOwnedFailure(QNetworkReply* reply); + void availableUpdatesSuccess(QNetworkReply* reply); + void availableUpdatesFailure(QNetworkReply* reply); + void updateItemSuccess(QNetworkReply* reply); + void updateItemFailure(QNetworkReply* reply); private: - QJsonObject apiResponse(const QString& label, QNetworkReply& reply); - QJsonObject failResponse(const QString& label, QNetworkReply& reply); + QJsonObject apiResponse(const QString& label, QNetworkReply* reply); + QJsonObject failResponse(const QString& label, QNetworkReply* reply); void send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, QJsonObject request); void keysQuery(const QString& endpoint, const QString& success, const QString& fail, QJsonObject& extraRequestParams); void keysQuery(const QString& endpoint, const QString& success, const QString& fail); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index b960c0b703..1f44343bdc 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -208,7 +208,7 @@ void QmlCommerce::alreadyOwned(const QString& marketplaceId) { ledger->alreadyOwned(marketplaceId); } -QString QmlCommerce::getInstalledApps() { +QString QmlCommerce::getInstalledApps(const QString& justInstalledAppID) { QString installedAppsFromMarketplace; QStringList runningScripts = DependencyManager::get()->getRunningScripts(); @@ -217,6 +217,18 @@ QString QmlCommerce::getInstalledApps() { foreach(QString appFileName, apps) { installedAppsFromMarketplace += appFileName; installedAppsFromMarketplace += ","; + + // If we were supplied a "justInstalledAppID" argument, that means we're entering this function + // to get the new list of installed apps immediately after installing an app. + // In that case, the app we installed may not yet have its associated script running - + // that task is asynchronous and takes a nonzero amount of time. This is especially true + // for apps that are not in Interface's script cache. + // Thus, we protect against deleting the .app.json from the user's disk (below) + // by skipping that check for the app we just installed. + if ((justInstalledAppID != "") && ((justInstalledAppID + ".app.json") == appFileName)) { + continue; + } + QFile appFile(_appsPath + appFileName); if (appFile.open(QIODevice::ReadOnly)) { QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(appFile.readAll()); @@ -291,7 +303,8 @@ bool QmlCommerce::installApp(const QString& itemHref) { return false; } - emit appInstalled(itemHref); + QFileInfo appFileInfo(appFile); + emit appInstalled(appFileInfo.baseName()); return true; }); request->send(); @@ -321,7 +334,8 @@ bool QmlCommerce::uninstallApp(const QString& itemHref) { qCWarning(commerce) << "Couldn't delete local .app.json file during app uninstall. Continuing anyway. App filename is:" << appHref.fileName(); } - emit appUninstalled(itemHref); + QFileInfo appFileInfo(appFile); + emit appUninstalled(appFileInfo.baseName()); return true; } diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index a0c6916799..79d8e82e71 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -53,8 +53,8 @@ signals: void contentSetChanged(const QString& contentSetHref); - void appInstalled(const QString& appHref); - void appUninstalled(const QString& appHref); + void appInstalled(const QString& appID); + void appUninstalled(const QString& appID); protected: Q_INVOKABLE void getWalletStatus(); @@ -86,7 +86,7 @@ protected: Q_INVOKABLE void replaceContentSet(const QString& itemHref, const QString& certificateID); - Q_INVOKABLE QString getInstalledApps(); + Q_INVOKABLE QString getInstalledApps(const QString& justInstalledAppID = ""); Q_INVOKABLE bool installApp(const QString& appHref); Q_INVOKABLE bool uninstallApp(const QString& appHref); Q_INVOKABLE bool openApp(const QString& appHref); diff --git a/interface/src/main.cpp b/interface/src/main.cpp index d6665f1036..85a83d88d1 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -91,7 +91,7 @@ int main(int argc, const char* argv[]) { qDebug() << "UserActivityLogger is enabled:" << ual.isEnabled(); if (ual.isEnabled()) { - auto crashHandlerStarted = startCrashHandler(); + auto crashHandlerStarted = startCrashHandler(argv[0]); qDebug() << "Crash handler started:" << crashHandlerStarted; } diff --git a/interface/src/scripting/DesktopScriptingInterface.cpp b/interface/src/scripting/DesktopScriptingInterface.cpp index 34f196ac70..bda06cda48 100644 --- a/interface/src/scripting/DesktopScriptingInterface.cpp +++ b/interface/src/scripting/DesktopScriptingInterface.cpp @@ -14,6 +14,8 @@ #include #include +#include + #include "Application.h" #include "MainWindow.h" #include @@ -29,6 +31,14 @@ int DesktopScriptingInterface::getHeight() { return size.height(); } +QVariantMap DesktopScriptingInterface::getPresentationMode() { + static QVariantMap presentationModes { + { "VIRTUAL", Virtual }, + { "NATIVE", Native } + }; + return presentationModes; +} + void DesktopScriptingInterface::setHUDAlpha(float alpha) { qApp->getApplicationCompositor().setAlpha(alpha); } @@ -41,3 +51,14 @@ void DesktopScriptingInterface::show(const QString& path, const QString& title) DependencyManager::get()->show(path, title); } +InteractiveWindowPointer DesktopScriptingInterface::createWindow(const QString& sourceUrl, const QVariantMap& properties) { + if (QThread::currentThread() != thread()) { + InteractiveWindowPointer interactiveWindow = nullptr; + BLOCKING_INVOKE_METHOD(this, "createWindow", + Q_RETURN_ARG(InteractiveWindowPointer, interactiveWindow), + Q_ARG(QString, sourceUrl), + Q_ARG(QVariantMap, properties)); + return interactiveWindow; + } + return new InteractiveWindow(sourceUrl, properties);; +} diff --git a/interface/src/scripting/DesktopScriptingInterface.h b/interface/src/scripting/DesktopScriptingInterface.h index e62a3584d6..db42b5ca54 100644 --- a/interface/src/scripting/DesktopScriptingInterface.h +++ b/interface/src/scripting/DesktopScriptingInterface.h @@ -13,20 +13,48 @@ #define hifi_DesktopScriptingInterface_h #include +#include #include +#include "InteractiveWindow.h" + +/**jsdoc + * @namespace Desktop + * + * @hifi-interface + * @hifi-client-entity + * + * @property {number} width + * @property {number} height + * @property {number} ALWAYS_ON_TOP - InteractiveWindow flag for always showing a window on top + * @property {number} CLOSE_BUTTON_HIDES - InteractiveWindow flag for hiding the window instead of closing on window close by user + */ class DesktopScriptingInterface : public QObject, public Dependency { Q_OBJECT Q_PROPERTY(int width READ getWidth) // Physical width of screen(s) including task bars and system menus Q_PROPERTY(int height READ getHeight) // Physical height of screen(s) including task bars and system menus + Q_PROPERTY(QVariantMap PresentationMode READ getPresentationMode CONSTANT FINAL) + Q_PROPERTY(int ALWAYS_ON_TOP READ flagAlwaysOnTop CONSTANT FINAL) + Q_PROPERTY(int CLOSE_BUTTON_HIDES READ flagCloseButtonHides CONSTANT FINAL) + public: Q_INVOKABLE void setHUDAlpha(float alpha); Q_INVOKABLE void show(const QString& path, const QString& title); + Q_INVOKABLE InteractiveWindowPointer createWindow(const QString& sourceUrl, const QVariantMap& properties = QVariantMap()); + int getWidth(); int getHeight(); + + +private: + static int flagAlwaysOnTop() { return AlwaysOnTop; } + static int flagCloseButtonHides() { return CloseButtonHides; } + + Q_INVOKABLE static QVariantMap getPresentationMode(); }; + #endif // hifi_DesktopScriptingInterface_h diff --git a/interface/src/scripting/SettingsScriptingInterface.cpp b/interface/src/scripting/SettingsScriptingInterface.cpp index 2f14c33dc7..afafe1a350 100644 --- a/interface/src/scripting/SettingsScriptingInterface.cpp +++ b/interface/src/scripting/SettingsScriptingInterface.cpp @@ -35,5 +35,8 @@ QVariant SettingsScriptingInterface::getValue(const QString& setting, const QVar } void SettingsScriptingInterface::setValue(const QString& setting, const QVariant& value) { - Setting::Handle(setting).set(value); + // Make a deep-copy of the string. + // Dangling pointers can occur with QStrings that are implicitly shared from a QScriptEngine. + QString deepCopy = QString::fromUtf16(setting.utf16()); + Setting::Handle(deepCopy).set(value); } diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 0aea7a02c5..ba86925581 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -86,10 +86,6 @@ void WindowScriptingInterface::raise() { }); } -void WindowScriptingInterface::raiseMainWindow() { - raise(); -} - /// Display an alert box /// \param const QString& message message to display /// \return QScriptValue::UndefinedValue @@ -137,8 +133,13 @@ void WindowScriptingInterface::openUrl(const QUrl& url) { if (url.scheme() == URL_SCHEME_HIFI) { DependencyManager::get()->handleLookupString(url.toString()); } else { +#if defined(Q_OS_ANDROID) + QList args = { url.toString() }; + AndroidHelper::instance().requestActivity("WebView", true, args); +#else // address manager did not handle - ask QDesktopServices to handle QDesktopServices::openUrl(url); +#endif } } } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index dc868e6fcd..77895e0e76 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -80,13 +80,6 @@ public slots: */ void raise(); - /**jsdoc - * Raise the Interface window if it is minimized. If raised, the window gains focus. - * @function Window.raiseMainWindow - * @deprecated Use {@link Window.raise|raise} instead. - */ - void raiseMainWindow(); - /**jsdoc * Display a dialog with the specified message and an "OK" button. The dialog is non-modal; the script continues without * waiting for a user response. diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index ea660fb0e2..108f20b2dd 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -179,7 +179,7 @@ static const auto DEPTH_FORMAT = gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::DEPT void ApplicationOverlay::buildFramebufferObject() { PROFILE_RANGE(app, __FUNCTION__); - auto uiSize = glm::uvec2(glm::vec2(qApp->getUiSize()) * qApp->getRenderResolutionScale()); + auto uiSize = glm::uvec2(qApp->getUiSize()); if (!_overlayFramebuffer || uiSize != _overlayFramebuffer->getSize()) { _overlayFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("ApplicationOverlay")); } diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 39a5849d85..9804c6712a 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -113,9 +113,8 @@ void LoginDialog::linkSteam() { } JSONCallbackParameters callbackParams; - callbackParams.jsonCallbackReceiver = this; + callbackParams.callbackReceiver = this; callbackParams.jsonCallbackMethod = "linkCompleted"; - callbackParams.errorCallbackReceiver = this; callbackParams.errorCallbackMethod = "linkFailed"; const QString LINK_STEAM_PATH = "api/v1/user/steam/link"; @@ -141,9 +140,8 @@ void LoginDialog::createAccountFromStream(QString username) { } JSONCallbackParameters callbackParams; - callbackParams.jsonCallbackReceiver = this; + callbackParams.callbackReceiver = this; callbackParams.jsonCallbackMethod = "createCompleted"; - callbackParams.errorCallbackReceiver = this; callbackParams.errorCallbackMethod = "createFailed"; const QString CREATE_ACCOUNT_FROM_STEAM_PATH = "api/v1/user/steam/create"; @@ -174,39 +172,40 @@ void LoginDialog::openUrl(const QString& url) const { offscreenUi->load("Browser.qml", [=](QQmlContext* context, QObject* newObject) { newObject->setProperty("url", url); }); + LoginDialog::hide(); } else { if (!hmd->getShouldShowTablet() && !qApp->isHMDMode()) { offscreenUi->load("Browser.qml", [=](QQmlContext* context, QObject* newObject) { newObject->setProperty("url", url); }); + LoginDialog::hide(); } else { tablet->gotoWebScreen(url); } } } -void LoginDialog::linkCompleted(QNetworkReply& reply) { +void LoginDialog::linkCompleted(QNetworkReply* reply) { emit handleLinkCompleted(); } -void LoginDialog::linkFailed(QNetworkReply& reply) { - emit handleLinkFailed(reply.errorString()); +void LoginDialog::linkFailed(QNetworkReply* reply) { + emit handleLinkFailed(reply->errorString()); } -void LoginDialog::createCompleted(QNetworkReply& reply) { +void LoginDialog::createCompleted(QNetworkReply* reply) { emit handleCreateCompleted(); } -void LoginDialog::createFailed(QNetworkReply& reply) { - emit handleCreateFailed(reply.errorString()); +void LoginDialog::createFailed(QNetworkReply* reply) { + emit handleCreateFailed(reply->errorString()); } void LoginDialog::signup(const QString& email, const QString& username, const QString& password) { JSONCallbackParameters callbackParams; - callbackParams.jsonCallbackReceiver = this; + callbackParams.callbackReceiver = this; callbackParams.jsonCallbackMethod = "signupCompleted"; - callbackParams.errorCallbackReceiver = this; callbackParams.errorCallbackMethod = "signupFailed"; QJsonObject payload; @@ -228,7 +227,7 @@ void LoginDialog::signup(const QString& email, const QString& username, const QS QJsonDocument(payload).toJson()); } -void LoginDialog::signupCompleted(QNetworkReply& reply) { +void LoginDialog::signupCompleted(QNetworkReply* reply) { emit handleSignupCompleted(); } @@ -242,10 +241,10 @@ QString errorStringFromAPIObject(const QJsonValue& apiObject) { } } -void LoginDialog::signupFailed(QNetworkReply& reply) { +void LoginDialog::signupFailed(QNetworkReply* reply) { // parse the returned JSON to see what the problem was - auto jsonResponse = QJsonDocument::fromJson(reply.readAll()); + auto jsonResponse = QJsonDocument::fromJson(reply->readAll()); static const QString RESPONSE_DATA_KEY = "data"; diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index 5ebf866fbd..ad8cab9699 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -42,14 +42,14 @@ signals: void handleSignupFailed(QString errorString); public slots: - void linkCompleted(QNetworkReply& reply); - void linkFailed(QNetworkReply& reply); + void linkCompleted(QNetworkReply* reply); + void linkFailed(QNetworkReply* reply); - void createCompleted(QNetworkReply& reply); - void createFailed(QNetworkReply& reply); + void createCompleted(QNetworkReply* reply); + void createFailed(QNetworkReply* reply); - void signupCompleted(QNetworkReply& reply); - void signupFailed(QNetworkReply& reply); + void signupCompleted(QNetworkReply* reply); + void signupFailed(QNetworkReply* reply); protected slots: Q_INVOKABLE bool isSteamRunning() const; diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index d131bb3467..e27001567f 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -88,38 +88,24 @@ void OverlayConductor::update(float dt) { _hmdMode = false; } - bool isAtRest = updateAvatarIsAtRest(); - bool isMoving = !isAtRest; - bool shouldRecenter = false; - if (_flags & SuppressedByMove) { - if (!isMoving) { - _flags &= ~SuppressedByMove; - shouldRecenter = true; - } - } else { - if (myAvatar->getClearOverlayWhenMoving() && isMoving) { - _flags |= SuppressedByMove; - } - } - - if (_flags & SuppressedByHead) { - if (isAtRest) { - _flags &= ~SuppressedByHead; + if (_suppressedByHead) { + if (updateAvatarIsAtRest()) { + _suppressedByHead = false; shouldRecenter = true; } } else { if (_hmdMode && headOutsideOverlay()) { - _flags |= SuppressedByHead; + _suppressedByHead = true; } } - bool targetVisible = Menu::getInstance()->isOptionChecked(MenuOption::Overlays) && (0 == (_flags & SuppressMask)); + bool targetVisible = Menu::getInstance()->isOptionChecked(MenuOption::Overlays) && !_suppressedByHead; if (targetVisible != currentVisible) { offscreenUi->setPinned(!targetVisible); } - if (shouldRecenter && !_flags) { + if (shouldRecenter && !_suppressedByHead) { centerUI(); } } diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index cf69c32fc5..b47e23d28a 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -25,13 +25,7 @@ private: bool headOutsideOverlay() const; bool updateAvatarIsAtRest(); - enum SupressionFlags { - SuppressedByMove = 0x01, - SuppressedByHead = 0x02, - SuppressMask = 0x03, - }; - - uint8_t _flags { SuppressedByMove }; + bool _suppressedByHead { false }; bool _hmdMode { false }; // used by updateAvatarIsAtRest diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 3d3c432e92..d0fe92ea00 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -161,12 +161,6 @@ void setupPreferences() { preferences->addPreference(new CheckPreference(UI_CATEGORY, "Use reticle cursor instead of arrow", getter, setter)); } - { - auto getter = [=]()->bool { return myAvatar->getClearOverlayWhenMoving(); }; - auto setter = [=](bool value) { myAvatar->setClearOverlayWhenMoving(value); }; - preferences->addPreference(new CheckPreference(UI_CATEGORY, "Clear overlays when moving", getter, setter)); - } - static const QString VIEW_CATEGORY{ "View" }; { auto getter = [=]()->float { return myAvatar->getRealWorldFieldOfView(); }; @@ -233,6 +227,8 @@ void setupPreferences() { auto getter = [=]()->float { return myAvatar->getTargetScale(); }; auto setter = [=](float value) { myAvatar->setTargetScale(value); }; auto preference = new SpinnerSliderPreference(AVATAR_TUNING, "Avatar Scale", getter, setter); + preference->setMin(0.25); + preference->setMax(4); preference->setStep(0.05f); preference->setDecimals(2); preferences->addPreference(preference); @@ -304,22 +300,32 @@ void setupPreferences() { preference->setStep(0.001f); preferences->addPreference(preference); } + { + auto preference = new ButtonPreference(MOVEMENT, "RESET SENSORS", [] { + qApp->resetSensors(); + }); + preferences->addPreference(preference); + } static const QString AVATAR_CAMERA{ "Mouse Sensitivity" }; { auto getter = [=]()->float { return myAvatar->getPitchSpeed(); }; auto setter = [=](float value) { myAvatar->setPitchSpeed(value); }; - auto preference = new SpinnerPreference(AVATAR_CAMERA, "Pitch speed (degrees/second)", getter, setter); + auto preference = new SpinnerSliderPreference(AVATAR_CAMERA, "Y input:", getter, setter); preference->setMin(1.0f); preference->setMax(360.0f); + preference->setStep(1); + preference->setDecimals(1); preferences->addPreference(preference); } { auto getter = [=]()->float { return myAvatar->getYawSpeed(); }; auto setter = [=](float value) { myAvatar->setYawSpeed(value); }; - auto preference = new SpinnerPreference(AVATAR_CAMERA, "Yaw speed (degrees/second)", getter, setter); + auto preference = new SpinnerSliderPreference(AVATAR_CAMERA, "X input:", getter, setter); preference->setMin(1.0f); preference->setMax(360.0f); + preference->setStep(1); + preference->setDecimals(1); preferences->addPreference(preference); } diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index efcc85b23e..60c039ce1f 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -493,7 +493,7 @@ void Snapshot::uploadSnapshot(const QString& filename, const QUrl& href) { multiPart->append(imagePart); auto accountManager = DependencyManager::get(); - JSONCallbackParameters callbackParams(uploader, "uploadSuccess", uploader, "uploadFailure"); + JSONCallbackParameters callbackParams(uploader, "uploadSuccess", "uploadFailure"); accountManager->sendRequest(SNAPSHOT_UPLOAD_URL, AccountManagerAuth::Required, QNetworkAccessManager::PostOperation, callbackParams, nullptr, multiPart); diff --git a/interface/src/ui/SnapshotUploader.cpp b/interface/src/ui/SnapshotUploader.cpp index 4613871d25..694d0fa8f8 100644 --- a/interface/src/ui/SnapshotUploader.cpp +++ b/interface/src/ui/SnapshotUploader.cpp @@ -23,11 +23,11 @@ SnapshotUploader::SnapshotUploader(QUrl inWorldLocation, QString pathname) : _pathname(pathname) { } -void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { +void SnapshotUploader::uploadSuccess(QNetworkReply* reply) { const QString STORY_UPLOAD_URL = "/api/v1/user_stories"; // parse the reply for the thumbnail_url - QByteArray contents = reply.readAll(); + QByteArray contents = reply->readAll(); QJsonParseError jsonError; auto doc = QJsonDocument::fromJson(contents, &jsonError); if (jsonError.error == QJsonParseError::NoError) { @@ -60,7 +60,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { rootObject.insert("user_story", userStoryObject); auto accountManager = DependencyManager::get(); - JSONCallbackParameters callbackParams(this, "createStorySuccess", this, "createStoryFailure"); + JSONCallbackParameters callbackParams(this, "createStorySuccess", "createStoryFailure"); accountManager->sendRequest(STORY_UPLOAD_URL, AccountManagerAuth::Required, @@ -74,11 +74,11 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { } } -void SnapshotUploader::uploadFailure(QNetworkReply& reply) { - QString replyString = reply.readAll(); - qDebug() << "Error " << reply.errorString() << " uploading snapshot " << _pathname << " from " << _inWorldLocation; +void SnapshotUploader::uploadFailure(QNetworkReply* reply) { + QString replyString = reply->readAll(); + qDebug() << "Error " << reply->errorString() << " uploading snapshot " << _pathname << " from " << _inWorldLocation; if (replyString.size() == 0) { - replyString = reply.errorString(); + replyString = reply->errorString(); } replyString = replyString.left(1000); // Only print first 1000 characters of error qDebug() << "Snapshot upload reply error (truncated):" << replyString; @@ -86,17 +86,17 @@ void SnapshotUploader::uploadFailure(QNetworkReply& reply) { this->deleteLater(); } -void SnapshotUploader::createStorySuccess(QNetworkReply& reply) { - QString replyString = reply.readAll(); +void SnapshotUploader::createStorySuccess(QNetworkReply* reply) { + QString replyString = reply->readAll(); emit DependencyManager::get()->snapshotShared(false, replyString); this->deleteLater(); } -void SnapshotUploader::createStoryFailure(QNetworkReply& reply) { - QString replyString = reply.readAll(); - qDebug() << "Error " << reply.errorString() << " uploading snapshot story " << _pathname << " from " << _inWorldLocation; +void SnapshotUploader::createStoryFailure(QNetworkReply* reply) { + QString replyString = reply->readAll(); + qDebug() << "Error " << reply->errorString() << " uploading snapshot story " << _pathname << " from " << _inWorldLocation; if (replyString.size() == 0) { - replyString = reply.errorString(); + replyString = reply->errorString(); } replyString = replyString.left(1000); // Only print first 1000 characters of error qDebug() << "Snapshot story upload reply error (truncated):" << replyString; diff --git a/interface/src/ui/SnapshotUploader.h b/interface/src/ui/SnapshotUploader.h index ae6d5d55ca..d8e72730c7 100644 --- a/interface/src/ui/SnapshotUploader.h +++ b/interface/src/ui/SnapshotUploader.h @@ -21,12 +21,12 @@ class SnapshotUploader : public QObject { public: SnapshotUploader(QUrl inWorldLocation, QString pathname); public slots: - void uploadSuccess(QNetworkReply& reply); - void uploadFailure(QNetworkReply& reply); - void createStorySuccess(QNetworkReply& reply); - void createStoryFailure(QNetworkReply& reply); + void uploadSuccess(QNetworkReply* reply); + void uploadFailure(QNetworkReply* reply); + void createStorySuccess(QNetworkReply* reply); + void createStoryFailure(QNetworkReply* reply); private: QUrl _inWorldLocation; QString _pathname; }; -#endif // hifi_SnapshotUploader_h \ No newline at end of file +#endif // hifi_SnapshotUploader_h diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index c7ee868855..6bb615948c 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -333,7 +333,13 @@ void Stats::updateStats(bool force) { } auto gpuContext = qApp->getGPUContext(); - + auto displayPlugin = qApp->getActiveDisplayPlugin(); + if (displayPlugin) { + QVector2D dims(displayPlugin->getRecommendedRenderSize().x, displayPlugin->getRecommendedRenderSize().y); + dims *= displayPlugin->getRenderResolutionScale(); + STAT_UPDATE(gpuFrameSize, dims); + STAT_UPDATE(gpuFrameTimePerPixel, (float)(gpuContext->getFrameTimerGPUAverage()*1000000.0 / double(dims.x()*dims.y()))); + } // Update Frame timing (in ms) STAT_UPDATE(gpuFrameTime, (float)gpuContext->getFrameTimerGPUAverage()); STAT_UPDATE(batchFrameTime, (float)gpuContext->getFrameTimerBatchAverage()); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 36e923261d..f4181f9788 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -276,7 +276,9 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, gpuTextureExternalMemory, 0) STATS_PROPERTY(QString, gpuTextureMemoryPressureState, QString()) STATS_PROPERTY(int, gpuFreeMemory, 0) + STATS_PROPERTY(QVector2D, gpuFrameSize, QVector2D(0,0)) STATS_PROPERTY(float, gpuFrameTime, 0) + STATS_PROPERTY(float, gpuFrameTimePerPixel, 0) STATS_PROPERTY(float, batchFrameTime, 0) STATS_PROPERTY(float, engineFrameTime, 0) STATS_PROPERTY(float, avatarSimulationTime, 0) @@ -962,6 +964,20 @@ signals: */ void gpuFrameTimeChanged(); + /**jsdoc + * Triggered when the value of the gpuFrameTime property changes. + * @function Stats.gpuFrameTimeChanged + * @returns {Signal} + */ + void gpuFrameSizeChanged(); + + /**jsdoc + * Triggered when the value of the gpuFrameTime property changes. + * @function Stats.gpuFrameTimeChanged + * @returns {Signal} + */ + void gpuFrameTimePerPixelChanged(); + /**jsdoc * Triggered when the value of the batchFrameTime property changes. * @function Stats.batchFrameTimeChanged diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index a879dcfada..789b1f9969 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -97,6 +97,10 @@ static const float CONTEXT_OVERLAY_UNHOVERED_COLORPULSE = 1.0f; void ContextOverlayInterface::setEnabled(bool enabled) { _enabled = enabled; + if (!enabled) { + // Destroy any potentially-active ContextOverlays when disabling the interface + createOrDestroyContextOverlay(EntityItemID(), PointerEvent()); + } } void ContextOverlayInterface::clickDownOnEntity(const EntityItemID& entityItemID, const PointerEvent& event) { diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index b303e7f919..7489f6e189 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -27,6 +27,12 @@ ModelOverlay::ModelOverlay() { _model->setLoadingPriority(_loadPriority); _isLoaded = false; + + // Don't show overlay until textures have loaded + _visible = false; + + render::ScenePointer scene = qApp->getMain3DScene(); + _model->setVisibleInScene(false, scene); } ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) : @@ -101,10 +107,11 @@ void ModelOverlay::update(float deltatime) { emit DependencyManager::get()->modelAddedToScene(getID(), NestableType::Overlay, _model); } bool metaDirty = false; - if (_visibleDirty) { + if (_visibleDirty && _texturesLoaded) { _visibleDirty = false; // don't show overlays in mirrors or spectator-cam unless _isVisibleInSecondaryCamera is true uint8_t modelRenderTagMask = (_isVisibleInSecondaryCamera ? render::hifi::TAG_ALL_VIEWS : render::hifi::TAG_MAIN_VIEW); + _model->setTagMask(modelRenderTagMask, scene); _model->setVisibleInScene(getVisible(), scene); metaDirty = true; @@ -134,6 +141,8 @@ void ModelOverlay::update(float deltatime) { if (!_modelTextures.isEmpty()) { _model->setTextures(_modelTextures); } + + _model->setVisibleInScene(getVisible(), scene); _model->updateRenderItems(); } } diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index a54bc0795e..9a054ffaa3 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -116,7 +116,7 @@ void Overlays::renderHUD(RenderArgs* renderArgs) { auto geometryCache = DependencyManager::get(); auto textureCache = DependencyManager::get(); - auto size = glm::uvec2(glm::vec2(qApp->getUiSize()) * qApp->getRenderResolutionScale()); + auto size = glm::uvec2(qApp->getUiSize()); int width = size.x; int height = size.y; mat4 legacyProjection = glm::ortho(0, width, height, 0, -1000, 1000); diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 08b6927b4b..ec4199a32d 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -42,6 +42,7 @@ #include "scripting/MenuScriptingInterface.h" #include "scripting/SettingsScriptingInterface.h" #include +#include #include #include "FileDialogHelper.h" #include "avatar/AvatarManager.h" @@ -137,11 +138,8 @@ void Web3DOverlay::destroyWebSurface() { // Fix for crash in QtWebEngineCore when rapidly switching domains // Call stop on the QWebEngineView before destroying OffscreenQMLSurface. if (rootItem) { - QObject* obj = rootItem->findChild("webEngineView"); - if (obj) { - // stop loading - QMetaObject::invokeMethod(obj, "stop"); - } + // stop loading + QMetaObject::invokeMethod(rootItem, "stop"); } _webSurface->pause(); @@ -151,6 +149,11 @@ void Web3DOverlay::destroyWebSurface() { // If the web surface was fetched out of the cache, release it back into the cache if (_cachedWebSurface) { + // If it's going back into the cache make sure to explicitly set the URL to a blank page + // in order to stop any resource consumption or audio related to the page. + if (rootItem) { + rootItem->setProperty("url", "about:blank"); + } auto offscreenCache = DependencyManager::get(); // FIXME prevents crash on shutdown, but we shoudln't have to do this check if (offscreenCache) { @@ -253,6 +256,7 @@ void Web3DOverlay::setupQmlSurface() { _webSurface->getSurfaceContext()->setContextProperty("SoundCache", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); _webSurface->getSurfaceContext()->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("Render", AbstractViewStateInterface::instance()->getRenderEngine()->getConfiguration().get()); _webSurface->getSurfaceContext()->setContextProperty("Workload", qApp->getGameWorkload()._engine->getConfiguration().get()); _webSurface->getSurfaceContext()->setContextProperty("Controller", DependencyManager::get().data()); diff --git a/interface/src/workload/GameWorkloadRenderer.cpp b/interface/src/workload/GameWorkloadRenderer.cpp index 8cf19b56dd..a8b65492d3 100644 --- a/interface/src/workload/GameWorkloadRenderer.cpp +++ b/interface/src/workload/GameWorkloadRenderer.cpp @@ -40,7 +40,6 @@ void GameSpaceToRender::run(const workload::WorkloadContextPointer& runContext, auto visible = _showAllProxies || _showAllViews; auto showProxies = _showAllProxies; auto showViews = _showAllViews; - auto freezeViews = _freezeViews; render::Transaction transaction; auto scene = gameWorkloadContext->_scene; @@ -71,7 +70,7 @@ void GameSpaceToRender::run(const workload::WorkloadContextPointer& runContext, transaction.resetItem(_spaceRenderItemID, std::make_shared(renderItem)); } - transaction.updateItem(_spaceRenderItemID, [visible, showProxies, proxies, freezeViews, showViews, views](GameWorkloadRenderItem& item) { + transaction.updateItem(_spaceRenderItemID, [visible, showProxies, proxies, showViews, views](GameWorkloadRenderItem& item) { item.setVisible(visible); item.showProxies(showProxies); item.setAllProxies(proxies); diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 0833b28142..549989778e 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1244,7 +1244,8 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab bool leftArmEnabled, bool rightArmEnabled, float dt, const AnimPose& leftHandPose, const AnimPose& rightHandPose, const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, - const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo) { + const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo, + const glm::mat4& rigToSensorMatrix, const glm::mat4& sensorToRigMatrix) { const bool ENABLE_POLE_VECTORS = false; const float ELBOW_POLE_VECTOR_BLEND_FACTOR = 0.95f; @@ -1271,19 +1272,20 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab int elbowJointIndex = _animSkeleton->nameToJointIndex("LeftForeArm"); if (ENABLE_POLE_VECTORS && !leftArmEnabled && handJointIndex >= 0 && armJointIndex >= 0 && elbowJointIndex >= 0) { glm::vec3 poleVector = calculateElbowPoleVector(handJointIndex, elbowJointIndex, armJointIndex, hipsIndex, true); + glm::vec3 sensorPoleVector = transformVectorFast(rigToSensorMatrix, poleVector); // smooth toward desired pole vector from previous pole vector... to reduce jitter if (!_prevLeftHandPoleVectorValid) { _prevLeftHandPoleVectorValid = true; - _prevLeftHandPoleVector = poleVector; + _prevLeftHandPoleVector = sensorPoleVector; } - glm::quat deltaRot = rotationBetween(_prevLeftHandPoleVector, poleVector); + glm::quat deltaRot = rotationBetween(_prevLeftHandPoleVector, sensorPoleVector); glm::quat smoothDeltaRot = safeMix(deltaRot, Quaternions::IDENTITY, ELBOW_POLE_VECTOR_BLEND_FACTOR); _prevLeftHandPoleVector = smoothDeltaRot * _prevLeftHandPoleVector; _animVars.set("leftHandPoleVectorEnabled", true); _animVars.set("leftHandPoleReferenceVector", Vectors::UNIT_X); - _animVars.set("leftHandPoleVector", _prevLeftHandPoleVector); + _animVars.set("leftHandPoleVector", transformVectorFast(sensorToRigMatrix, _prevLeftHandPoleVector)); } else { _prevLeftHandPoleVectorValid = false; _animVars.set("leftHandPoleVectorEnabled", false); @@ -1318,19 +1320,20 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab int elbowJointIndex = _animSkeleton->nameToJointIndex("RightForeArm"); if (ENABLE_POLE_VECTORS && !rightArmEnabled && handJointIndex >= 0 && armJointIndex >= 0 && elbowJointIndex >= 0) { glm::vec3 poleVector = calculateElbowPoleVector(handJointIndex, elbowJointIndex, armJointIndex, hipsIndex, false); + glm::vec3 sensorPoleVector = transformVectorFast(rigToSensorMatrix, poleVector); // smooth toward desired pole vector from previous pole vector... to reduce jitter if (!_prevRightHandPoleVectorValid) { _prevRightHandPoleVectorValid = true; - _prevRightHandPoleVector = poleVector; + _prevRightHandPoleVector = sensorPoleVector; } - glm::quat deltaRot = rotationBetween(_prevRightHandPoleVector, poleVector); + glm::quat deltaRot = rotationBetween(_prevRightHandPoleVector, sensorPoleVector); glm::quat smoothDeltaRot = safeMix(deltaRot, Quaternions::IDENTITY, ELBOW_POLE_VECTOR_BLEND_FACTOR); _prevRightHandPoleVector = smoothDeltaRot * _prevRightHandPoleVector; _animVars.set("rightHandPoleVectorEnabled", true); _animVars.set("rightHandPoleReferenceVector", -Vectors::UNIT_X); - _animVars.set("rightHandPoleVector", _prevRightHandPoleVector); + _animVars.set("rightHandPoleVector", transformVectorFast(sensorToRigMatrix, _prevRightHandPoleVector)); } else { _prevRightHandPoleVectorValid = false; _animVars.set("rightHandPoleVectorEnabled", false); @@ -1345,7 +1348,8 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab } } -void Rig::updateFeet(bool leftFootEnabled, bool rightFootEnabled, const AnimPose& leftFootPose, const AnimPose& rightFootPose) { +void Rig::updateFeet(bool leftFootEnabled, bool rightFootEnabled, const AnimPose& leftFootPose, const AnimPose& rightFootPose, + const glm::mat4& rigToSensorMatrix, const glm::mat4& sensorToRigMatrix) { const float KNEE_POLE_VECTOR_BLEND_FACTOR = 0.95f; @@ -1360,19 +1364,20 @@ void Rig::updateFeet(bool leftFootEnabled, bool rightFootEnabled, const AnimPose int kneeJointIndex = _animSkeleton->nameToJointIndex("LeftLeg"); int upLegJointIndex = _animSkeleton->nameToJointIndex("LeftUpLeg"); glm::vec3 poleVector = calculateKneePoleVector(footJointIndex, kneeJointIndex, upLegJointIndex, hipsIndex, leftFootPose); + glm::vec3 sensorPoleVector = transformVectorFast(rigToSensorMatrix, poleVector); - // smooth toward desired pole vector from previous pole vector... to reduce jitter + // smooth toward desired pole vector from previous pole vector... to reduce jitter, but in sensor space. if (!_prevLeftFootPoleVectorValid) { _prevLeftFootPoleVectorValid = true; - _prevLeftFootPoleVector = poleVector; + _prevLeftFootPoleVector = sensorPoleVector; } - glm::quat deltaRot = rotationBetween(_prevLeftFootPoleVector, poleVector); + glm::quat deltaRot = rotationBetween(_prevLeftFootPoleVector, sensorPoleVector); glm::quat smoothDeltaRot = safeMix(deltaRot, Quaternions::IDENTITY, KNEE_POLE_VECTOR_BLEND_FACTOR); _prevLeftFootPoleVector = smoothDeltaRot * _prevLeftFootPoleVector; _animVars.set("leftFootPoleVectorEnabled", true); _animVars.set("leftFootPoleReferenceVector", Vectors::UNIT_Z); - _animVars.set("leftFootPoleVector", _prevLeftFootPoleVector); + _animVars.set("leftFootPoleVector", transformVectorFast(sensorToRigMatrix, _prevLeftFootPoleVector)); } else { _animVars.unset("leftFootPosition"); _animVars.unset("leftFootRotation"); @@ -1390,19 +1395,20 @@ void Rig::updateFeet(bool leftFootEnabled, bool rightFootEnabled, const AnimPose int kneeJointIndex = _animSkeleton->nameToJointIndex("RightLeg"); int upLegJointIndex = _animSkeleton->nameToJointIndex("RightUpLeg"); glm::vec3 poleVector = calculateKneePoleVector(footJointIndex, kneeJointIndex, upLegJointIndex, hipsIndex, rightFootPose); + glm::vec3 sensorPoleVector = transformVectorFast(rigToSensorMatrix, poleVector); // smooth toward desired pole vector from previous pole vector... to reduce jitter if (!_prevRightFootPoleVectorValid) { _prevRightFootPoleVectorValid = true; - _prevRightFootPoleVector = poleVector; + _prevRightFootPoleVector = sensorPoleVector; } - glm::quat deltaRot = rotationBetween(_prevRightFootPoleVector, poleVector); + glm::quat deltaRot = rotationBetween(_prevRightFootPoleVector, sensorPoleVector); glm::quat smoothDeltaRot = safeMix(deltaRot, Quaternions::IDENTITY, KNEE_POLE_VECTOR_BLEND_FACTOR); _prevRightFootPoleVector = smoothDeltaRot * _prevRightFootPoleVector; _animVars.set("rightFootPoleVectorEnabled", true); _animVars.set("rightFootPoleReferenceVector", Vectors::UNIT_Z); - _animVars.set("rightFootPoleVector", _prevRightFootPoleVector); + _animVars.set("rightFootPoleVector", transformVectorFast(sensorToRigMatrix, _prevRightFootPoleVector)); } else { _animVars.unset("rightFootPosition"); _animVars.unset("rightFootRotation"); @@ -1434,9 +1440,9 @@ void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm glm::quat deltaQuat = desiredQuat * glm::inverse(headQuat); - // limit swing rotation of the deltaQuat by a 30 degree cone. + // limit swing rotation of the deltaQuat by a 25 degree cone. // TODO: use swing twist decomposition constraint instead, for off axis rotation clamping. - const float MAX_ANGLE = 30.0f * RADIANS_PER_DEGREE; + const float MAX_ANGLE = 25.0f * RADIANS_PER_DEGREE; if (fabsf(glm::angle(deltaQuat)) > MAX_ANGLE) { deltaQuat = glm::angleAxis(glm::clamp(glm::angle(deltaQuat), -MAX_ANGLE, MAX_ANGLE), glm::axis(deltaQuat)); } @@ -1546,16 +1552,18 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo bool spine2Enabled = params.primaryControllerFlags[PrimaryControllerType_Spine2] & (uint8_t)ControllerFlags::Enabled; bool leftArmEnabled = params.secondaryControllerFlags[SecondaryControllerType_LeftArm] & (uint8_t)ControllerFlags::Enabled; bool rightArmEnabled = params.secondaryControllerFlags[SecondaryControllerType_RightArm] & (uint8_t)ControllerFlags::Enabled; + glm::mat4 sensorToRigMatrix = glm::inverse(params.rigToSensorMatrix); updateHead(headEnabled, hipsEnabled, params.primaryControllerPoses[PrimaryControllerType_Head]); updateHands(leftHandEnabled, rightHandEnabled, hipsEnabled, hipsEstimated, leftArmEnabled, rightArmEnabled, dt, params.primaryControllerPoses[PrimaryControllerType_LeftHand], params.primaryControllerPoses[PrimaryControllerType_RightHand], - params.hipsShapeInfo, params.spineShapeInfo, params.spine1ShapeInfo, params.spine2ShapeInfo); + params.hipsShapeInfo, params.spineShapeInfo, params.spine1ShapeInfo, params.spine2ShapeInfo, + params.rigToSensorMatrix, sensorToRigMatrix); updateFeet(leftFootEnabled, rightFootEnabled, - params.primaryControllerPoses[PrimaryControllerType_LeftFoot], params.primaryControllerPoses[PrimaryControllerType_RightFoot]); - + params.primaryControllerPoses[PrimaryControllerType_LeftFoot], params.primaryControllerPoses[PrimaryControllerType_RightFoot], + params.rigToSensorMatrix, sensorToRigMatrix); if (headEnabled) { // Blend IK chains toward the joint limit centers, this should stablize head and hand ik. diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index e30b5d655c..ffa3a128b9 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -75,6 +75,7 @@ public: }; struct ControllerParameters { + glm::mat4 rigToSensorMatrix; AnimPose primaryControllerPoses[NumPrimaryControllerTypes]; // rig space uint8_t primaryControllerFlags[NumPrimaryControllerTypes]; AnimPose secondaryControllerPoses[NumSecondaryControllerTypes]; // rig space @@ -231,8 +232,10 @@ protected: bool leftArmEnabled, bool rightArmEnabled, float dt, const AnimPose& leftHandPose, const AnimPose& rightHandPose, const FBXJointShapeInfo& hipsShapeInfo, const FBXJointShapeInfo& spineShapeInfo, - const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo); - void updateFeet(bool leftFootEnabled, bool rightFootEnabled, const AnimPose& leftFootPose, const AnimPose& rightFootPose); + const FBXJointShapeInfo& spine1ShapeInfo, const FBXJointShapeInfo& spine2ShapeInfo, + const glm::mat4& rigToSensorMatrix, const glm::mat4& sensorToRigMatrix); + void updateFeet(bool leftFootEnabled, bool rightFootEnabled, const AnimPose& leftFootPose, const AnimPose& rightFootPose, + const glm::mat4& rigToSensorMatrix, const glm::mat4& sensorToRigMatrix); void updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::vec3& lookAt, const glm::vec3& saccade); void calcAnimAlpha(float speed, const std::vector& referenceSpeeds, float* alphaOut) const; @@ -359,16 +362,16 @@ protected: int _nextStateHandlerId { 0 }; QMutex _stateMutex; - glm::vec3 _prevRightFootPoleVector { Vectors::UNIT_Z }; + glm::vec3 _prevRightFootPoleVector { Vectors::UNIT_Z }; // sensor space bool _prevRightFootPoleVectorValid { false }; - glm::vec3 _prevLeftFootPoleVector { Vectors::UNIT_Z }; + glm::vec3 _prevLeftFootPoleVector { Vectors::UNIT_Z }; // sensor space bool _prevLeftFootPoleVectorValid { false }; - glm::vec3 _prevRightHandPoleVector { -Vectors::UNIT_Z }; + glm::vec3 _prevRightHandPoleVector { -Vectors::UNIT_Z }; // sensor space bool _prevRightHandPoleVectorValid { false }; - glm::vec3 _prevLeftHandPoleVector { -Vectors::UNIT_Z }; + glm::vec3 _prevLeftHandPoleVector { -Vectors::UNIT_Z }; // sensor space bool _prevLeftHandPoleVectorValid { false }; int _rigId; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index a5f79290cd..a6f0416a30 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1426,6 +1426,8 @@ bool AudioClient::setIsStereoInput(bool isStereoInput) { // restart the input device switchInputToAudioDevice(_inputDeviceInfo); + + emit isStereoInputChanged(_isStereoInput); } return stereoInputChanged; @@ -1463,6 +1465,8 @@ void AudioClient::outputFormatChanged() { } bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo inputDeviceInfo, bool isShutdownRequest) { + Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); + qCDebug(audioclient) << __FUNCTION__ << "inputDeviceInfo: [" << inputDeviceInfo.deviceName() << "]"; bool supportedFormat = false; @@ -1663,6 +1667,8 @@ void AudioClient::outputNotify() { } bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo outputDeviceInfo, bool isShutdownRequest) { + Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); + qCDebug(audioclient) << "AudioClient::switchOutputToAudioDevice() outputDeviceInfo: [" << outputDeviceInfo.deviceName() << "]"; bool supportedFormat = false; @@ -2021,7 +2027,7 @@ void AudioClient::setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 sca void AudioClient::startThread() { - moveToNewNamedThread(this, "Audio Thread", [this] { start(); }); + moveToNewNamedThread(this, "Audio Thread", [this] { start(); }, QThread::TimeCriticalPriority); } void AudioClient::setInputVolume(float volume, bool emitSignal) { diff --git a/libraries/audio/src/AbstractAudioInterface.h b/libraries/audio/src/AbstractAudioInterface.h index 30cbceeb0e..bbfd79d0aa 100644 --- a/libraries/audio/src/AbstractAudioInterface.h +++ b/libraries/audio/src/AbstractAudioInterface.h @@ -44,6 +44,9 @@ public slots: virtual bool setIsStereoInput(bool stereo) = 0; virtual bool isStereoInput() = 0; + +signals: + void isStereoInputChanged(bool isStereo); }; Q_DECLARE_METATYPE(AbstractAudioInterface*) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 1e4f33a95e..69356cdfaa 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -212,6 +212,8 @@ void Avatar::setTargetScale(float targetScale) { _targetScale = newValue; _scaleChanged = usecTimestampNow(); _isAnimatingScale = true; + + emit targetScaleChanged(targetScale); } } @@ -1338,6 +1340,9 @@ void Avatar::scaleVectorRelativeToPosition(glm::vec3 &positionToScale) const { } void Avatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { + if (!isMyAvatar()) { + createOrb(); + } AvatarData::setSkeletonModelURL(skeletonModelURL); if (QThread::currentThread() == thread()) { _skeletonModel->setURL(_skeletonModelURL); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 98246330c4..157f7b2ec6 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -23,7 +23,6 @@ #include #include - #include "Head.h" #include "SkeletonModel.h" #include "Rig.h" @@ -41,7 +40,6 @@ static const float SCALING_RATIO = .05f; extern const float CHAT_MESSAGE_SCALE; extern const float CHAT_MESSAGE_HEIGHT; - enum ScreenTintLayer { SCREEN_TINT_BEFORE_LANDSCAPE = 0, SCREEN_TINT_BEFORE_AVATARS, @@ -69,7 +67,7 @@ public: static void setShowNamesAboveHeads(bool show); explicit Avatar(QThread* thread); - ~Avatar(); + virtual ~Avatar(); virtual void instantiableAvatar() = 0; @@ -109,6 +107,7 @@ public: float getLODDistance() const; virtual bool isMyAvatar() const override { return false; } + virtual void createOrb() { } virtual QVector getJointRotations() const override; using AvatarData::getJointRotation; @@ -167,8 +166,8 @@ public: virtual int parseDataFromBuffer(const QByteArray& buffer) override; - static void renderJointConnectingCone( gpu::Batch& batch, glm::vec3 position1, glm::vec3 position2, - float radius1, float radius2, const glm::vec4& color); + static void renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, glm::vec3 position2, + float radius1, float radius2, const glm::vec4& color); virtual void applyCollision(const glm::vec3& contactPoint, const glm::vec3& penetration) { } @@ -235,7 +234,7 @@ public: /// Scales a world space position vector relative to the avatar position and scale /// \param vector position to be scaled. Will store the result - void scaleVectorRelativeToPosition(glm::vec3 &positionToScale) const; + void scaleVectorRelativeToPosition(glm::vec3& positionToScale) const; void slamPosition(const glm::vec3& position); virtual void updateAttitude(const glm::quat& orientation) override; @@ -254,7 +253,6 @@ public: void setPositionViaScript(const glm::vec3& position) override; void setOrientationViaScript(const glm::quat& orientation) override; - /**jsdoc * @function MyAvatar.getParentID * @returns {Uuid} @@ -283,7 +281,6 @@ public: // This calls through to the SpatiallyNestable versions, but is here to expose these to JavaScript. Q_INVOKABLE virtual void setParentJointIndex(quint16 parentJointIndex) override; - /**jsdoc * Returns an array of joints, where each joint is an object containing name, index, and parentIndex fields. * @function MyAvatar.getSkeleton @@ -349,7 +346,6 @@ public: // not all subclasses of AvatarData have access to this data. virtual bool canMeasureEyeHeight() const override { return true; } - virtual float getModelScale() const { return _modelScale; } virtual void setModelScale(float scale) { _modelScale = scale; } virtual glm::vec3 scaleForChildren() const override { return glm::vec3(getModelScale()); } @@ -365,6 +361,9 @@ public: virtual scriptable::ScriptableModelBase getScriptableModel() override; +signals: + void targetScaleChanged(float targetScale); + public slots: // FIXME - these should be migrated to use Pose data instead @@ -382,7 +381,7 @@ public slots: /**jsdoc * Get the rotation of the left palm in world coordinates. * @function MyAvatar.getLeftPalmRotation - * @returns {Vec3} The rotation of the left palm in world coordinates. + * @returns {Quat} The rotation of the left palm in world coordinates. * @example Report the rotation of your avatar's left palm. * print(JSON.stringify(MyAvatar.getLeftPalmRotation())); */ @@ -399,7 +398,7 @@ public slots: /**jsdoc * Get the rotation of the right palm in world coordinates. * @function MyAvatar.getRightPalmRotation - * @returns {Vec3} The rotation of the right palm in world coordinates. + * @returns {Quat} The rotation of the right palm in world coordinates. * @example Report the rotation of your avatar's right palm. * print(JSON.stringify(MyAvatar.getRightPalmRotation())); */ diff --git a/libraries/avatars-renderer/src/avatars-renderer/Head.cpp b/libraries/avatars-renderer/src/avatars-renderer/Head.cpp index 5800c1404b..bdee6d9147 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Head.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Head.cpp @@ -220,30 +220,44 @@ void Head::calculateMouthShapes(float deltaTime) { void Head::applyEyelidOffset(glm::quat headOrientation) { // Adjusts the eyelid blendshape coefficients so that the eyelid follows the iris as the head pitches. - - if (disableEyelidAdjustment) { + bool isBlinking = (_rightEyeBlinkVelocity != 0.0f && _rightEyeBlinkVelocity != 0.0f); + if (disableEyelidAdjustment || isBlinking) { return; } - glm::quat eyeRotation = rotationBetween(headOrientation * IDENTITY_FORWARD, getLookAtPosition() - _eyePosition); - eyeRotation = eyeRotation * glm::angleAxis(safeEulerAngles(headOrientation).y, IDENTITY_UP); // Rotation w.r.t. head - float eyePitch = safeEulerAngles(eyeRotation).x; + const float EYE_PITCH_TO_COEFFICIENT = 3.5f; // Empirically determined + const float MAX_EYELID_OFFSET = 1.5f; + const float BLINK_DOWN_MULTIPLIER = 0.25f; + const float OPEN_DOWN_MULTIPLIER = 0.3f; + const float BROW_UP_MULTIPLIER = 0.5f; - const float EYE_PITCH_TO_COEFFICIENT = 1.6f; // Empirically determined - const float MAX_EYELID_OFFSET = 0.8f; // So that don't fully close eyes when looking way down - float eyelidOffset = glm::clamp(-eyePitch * EYE_PITCH_TO_COEFFICIENT, -1.0f, MAX_EYELID_OFFSET); + glm::vec3 lookAt = glm::normalize(getLookAtPosition() - _eyePosition); + glm::vec3 headUp = headOrientation * Vectors::UNIT_Y; + float eyePitch = (PI / 2.0f) - acos(glm::dot(lookAt, headUp)); + float eyelidOffset = glm::clamp(abs(eyePitch * EYE_PITCH_TO_COEFFICIENT), 0.0f, MAX_EYELID_OFFSET); - for (int i = 0; i < 2; i++) { - const int LEFT_EYE = 8; - float eyeCoefficient = _transientBlendshapeCoefficients[i] - _transientBlendshapeCoefficients[LEFT_EYE + i]; - eyeCoefficient = glm::clamp(eyelidOffset + eyeCoefficient * (1.0f - eyelidOffset), -1.0f, 1.0f); - if (eyeCoefficient > 0.0f) { - _transientBlendshapeCoefficients[i] = eyeCoefficient; - _transientBlendshapeCoefficients[LEFT_EYE + i] = 0.0f; + float blinkUpCoefficient = -eyelidOffset; + float blinkDownCoefficient = BLINK_DOWN_MULTIPLIER * eyelidOffset; + + float openUpCoefficient = eyelidOffset; + float openDownCoefficient = OPEN_DOWN_MULTIPLIER * eyelidOffset; + + float browsUpCoefficient = BROW_UP_MULTIPLIER * eyelidOffset; + float browsDownCoefficient = 0.0f; - } else { - _transientBlendshapeCoefficients[i] = 0.0f; - _transientBlendshapeCoefficients[LEFT_EYE + i] = -eyeCoefficient; + bool isLookingUp = (eyePitch > 0); + + if (isLookingUp) { + for (int i = 0; i < 2; i++) { + _transientBlendshapeCoefficients[EYE_BLINK_INDICES[i]] = blinkUpCoefficient; + _transientBlendshapeCoefficients[EYE_OPEN_INDICES[i]] = openUpCoefficient; + _transientBlendshapeCoefficients[BROWS_U_INDICES[i]] = browsUpCoefficient; + } + } else { + for (int i = 0; i < 2; i++) { + _transientBlendshapeCoefficients[EYE_BLINK_INDICES[i]] = blinkDownCoefficient; + _transientBlendshapeCoefficients[EYE_OPEN_INDICES[i]] = openDownCoefficient; + _transientBlendshapeCoefficients[BROWS_U_INDICES[i]] = browsDownCoefficient; } } } diff --git a/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.cpp deleted file mode 100644 index 7678c03276..0000000000 --- a/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.cpp +++ /dev/null @@ -1,19 +0,0 @@ -// -// Created by Bradley Austin Davis on 2017/04/27 -// Copyright 2013-2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "OtherAvatar.h" - -OtherAvatar::OtherAvatar(QThread* thread) : Avatar(thread) { - // give the pointer to our head to inherited _headData variable from AvatarData - _headData = new Head(this); - _skeletonModel = std::make_shared(this, nullptr); - _skeletonModel->setLoadingPriority(OTHERAVATAR_LOADING_PRIORITY); - connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished); - connect(_skeletonModel.get(), &Model::rigReady, this, &Avatar::rigReady); - connect(_skeletonModel.get(), &Model::rigReset, this, &Avatar::rigReset); -} diff --git a/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.h b/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.h deleted file mode 100644 index df09d7fd99..0000000000 --- a/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// Created by Bradley Austin Davis on 2017/04/27 -// Copyright 2013-2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_OtherAvatar_h -#define hifi_OtherAvatar_h - -#include "Avatar.h" - -class OtherAvatar : public Avatar { -public: - explicit OtherAvatar(QThread* thread); - virtual void instantiableAvatar() override {}; -}; - -#endif // hifi_OtherAvatar_h diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index e5ae7ec999..5e799b8401 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -401,7 +401,6 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(float sensorToWorldScale READ getSensorToWorldScale) public: - virtual QString getName() const override { return QString("Avatar:") + _displayName; } static const QString FRAME_NAME; diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 974ae92432..174e81bb31 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -89,11 +89,15 @@ AvatarSharedPointer AvatarHashMap::addAvatar(const QUuid& sessionUUID, const QWe return avatar; } -AvatarSharedPointer AvatarHashMap::newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { +AvatarSharedPointer AvatarHashMap::newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer, + bool& isNew) { QWriteLocker locker(&_hashLock); auto avatar = _avatarHash.value(sessionUUID); if (!avatar) { avatar = addAvatar(sessionUUID, mixerWeakPointer); + isNew = true; + } else { + isNew = false; } return avatar; } @@ -125,8 +129,13 @@ AvatarSharedPointer AvatarHashMap::parseAvatarData(QSharedPointer(); + bool isNewAvatar; if (sessionUUID != _lastOwnerSessionUUID && (!nodeList->isIgnoringNode(sessionUUID) || nodeList->getRequestsDomainListData())) { - auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); + auto avatar = newOrExistingAvatar(sessionUUID, sendingNode, isNewAvatar); + if (isNewAvatar) { + QWriteLocker locker(&_hashLock); + _pendingAvatars.insert(sessionUUID, { std::chrono::steady_clock::now(), 0, avatar }); + } // have the matching (or new) avatar parse the data from the packet int bytesRead = avatar->parseDataFromBuffer(byteArray); @@ -157,6 +166,7 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer { QReadLocker locker(&_hashLock); + _pendingAvatars.remove(identityUUID); auto me = _avatarHash.find(EMPTY); if ((me != _avatarHash.end()) && (identityUUID == me.value()->getSessionUUID())) { // We add MyAvatar to _avatarHash with an empty UUID. Code relies on this. In order to correctly handle an @@ -168,7 +178,8 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer if (!nodeList->isIgnoringNode(identityUUID) || nodeList->getRequestsDomainListData()) { // mesh URL for a UUID, find avatar in our list - auto avatar = newOrExistingAvatar(identityUUID, sendingNode); + bool isNewAvatar; + auto avatar = newOrExistingAvatar(identityUUID, sendingNode, isNewAvatar); bool identityChanged = false; bool displayNameChanged = false; bool skeletonModelUrlChanged = false; @@ -189,6 +200,7 @@ void AvatarHashMap::processKillAvatar(QSharedPointer message, S void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason) { QWriteLocker locker(&_hashLock); + _pendingAvatars.remove(sessionUUID); auto removedAvatar = _avatarHash.take(sessionUUID); if (removedAvatar) { diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index ef6f7845eb..fd2cd76fbf 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -19,6 +19,7 @@ #include #include +#include #include @@ -145,13 +146,21 @@ protected: virtual AvatarSharedPointer parseAvatarData(QSharedPointer message, SharedNodePointer sendingNode); virtual AvatarSharedPointer newSharedAvatar(); virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); - AvatarSharedPointer newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); + AvatarSharedPointer newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer, + bool& isNew); virtual AvatarSharedPointer findAvatar(const QUuid& sessionUUID) const; // uses a QReadLocker on the hashLock virtual void removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason = KillAvatarReason::NoReason); virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason); AvatarHash _avatarHash; + struct PendingAvatar { + std::chrono::steady_clock::time_point creationTime; + int transmits; + AvatarSharedPointer avatar; + }; + using AvatarPendingHash = QHash; + AvatarPendingHash _pendingAvatars; mutable QReadWriteLock _hashLock; private: diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index 4119d7a459..19f5efcd16 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -17,7 +17,6 @@ #include #include -#include #include #include @@ -33,7 +32,7 @@ HeadData::HeadData(AvatarData* owningAvatar) : _summedBlendshapeCoefficients(QVector(0, 0.0f)), _owningAvatar(owningAvatar) { - + computeBlendshapesLookupMap(); } glm::quat HeadData::getRawOrientation() const { @@ -71,16 +70,10 @@ void HeadData::setOrientation(const glm::quat& orientation) { setHeadOrientation(orientation); } -//Lazily construct a lookup map from the blendshapes -static const QMap& getBlendshapesLookupMap() { - static std::once_flag once; - static QMap blendshapeLookupMap; - std::call_once(once, [&] { - for (int i = 0; i < NUM_FACESHIFT_BLENDSHAPES; i++) { - blendshapeLookupMap[FACESHIFT_BLENDSHAPES[i]] = i; - } - }); - return blendshapeLookupMap; +void HeadData::computeBlendshapesLookupMap(){ + for (int i = 0; i < NUM_FACESHIFT_BLENDSHAPES; i++) { + _blendshapeLookupMap[FACESHIFT_BLENDSHAPES[i]] = i; + } } int HeadData::getNumSummedBlendshapeCoefficients() const { @@ -108,11 +101,10 @@ const QVector& HeadData::getSummedBlendshapeCoefficients() { } void HeadData::setBlendshape(QString name, float val) { - const auto& blendshapeLookupMap = getBlendshapesLookupMap(); //Check to see if the named blendshape exists, and then set its value if it does - auto it = blendshapeLookupMap.find(name); - if (it != blendshapeLookupMap.end()) { + auto it = _blendshapeLookupMap.find(name); + if (it != _blendshapeLookupMap.end()) { if (_blendshapeCoefficients.size() <= it.value()) { _blendshapeCoefficients.resize(it.value() + 1); } @@ -123,6 +115,18 @@ void HeadData::setBlendshape(QString name, float val) { } } +int HeadData::getBlendshapeIndex(const QString& name) { + auto it = _blendshapeLookupMap.find(name); + int index = it != _blendshapeLookupMap.end() ? it.value() : -1; + return index; +} + +void HeadData::getBlendshapeIndices(const std::vector& blendShapeNames, std::vector& indexes) { + for (auto& name : blendShapeNames) { + indexes.push_back(getBlendshapeIndex(name)); + } +} + static const QString JSON_AVATAR_HEAD_ROTATION = QStringLiteral("rotation"); static const QString JSON_AVATAR_HEAD_BLENDSHAPE_COEFFICIENTS = QStringLiteral("blendShapes"); static const QString JSON_AVATAR_HEAD_LEAN_FORWARD = QStringLiteral("leanForward"); @@ -131,10 +135,9 @@ static const QString JSON_AVATAR_HEAD_LOOKAT = QStringLiteral("lookAt"); QJsonObject HeadData::toJson() const { QJsonObject headJson; - const auto& blendshapeLookupMap = getBlendshapesLookupMap(); QJsonObject blendshapesJson; - for (auto name : blendshapeLookupMap.keys()) { - auto index = blendshapeLookupMap[name]; + for (auto name : _blendshapeLookupMap.keys()) { + auto index = _blendshapeLookupMap[name]; float value = 0.0f; if (index < _blendshapeCoefficients.size()) { value += _blendshapeCoefficients[index]; diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index f9c4b52139..6d211da2cd 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -20,6 +20,7 @@ #include #include +#include // degrees const float MIN_HEAD_YAW = -180.0f; @@ -55,6 +56,8 @@ public: void setOrientation(const glm::quat& orientation); void setBlendshape(QString name, float val); + int getBlendshapeIndex(const QString& name); + void getBlendshapeIndices(const std::vector& blendShapeNames, std::vector& indexes); const QVector& getBlendshapeCoefficients() const { return _blendshapeCoefficients; } const QVector& getSummedBlendshapeCoefficients(); int getNumSummedBlendshapeCoefficients() const; @@ -114,6 +117,7 @@ protected: QVector _blendshapeCoefficients; QVector _transientBlendshapeCoefficients; QVector _summedBlendshapeCoefficients; + QMap _blendshapeLookupMap; AvatarData* _owningAvatar; private: @@ -122,6 +126,7 @@ private: HeadData& operator= (const HeadData&); void setHeadOrientation(const glm::quat& orientation); + void computeBlendshapesLookupMap(); }; #endif // hifi_HeadData_h diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index c35e641ffd..a0d5cb0920 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -277,7 +277,7 @@ bool CompositorHelper::getReticleOverDesktop() const { // as being over the desktop. if (isHMD()) { QMutexLocker locker(&_reticleLock); - glm::vec2 maxOverlayPosition = glm::vec2(_currentDisplayPlugin->getRecommendedUiSize()) * _currentDisplayPlugin->getRenderResolutionScale(); + glm::vec2 maxOverlayPosition = glm::vec2(_currentDisplayPlugin->getRecommendedUiSize()); static const glm::vec2 minOverlayPosition; if (glm::any(glm::lessThan(_reticlePositionInHMD, minOverlayPosition)) || glm::any(glm::greaterThan(_reticlePositionInHMD, maxOverlayPosition))) { @@ -319,7 +319,7 @@ void CompositorHelper::sendFakeMouseEvent() { void CompositorHelper::setReticlePosition(const glm::vec2& position, bool sendFakeEvent) { if (isHMD()) { - glm::vec2 maxOverlayPosition = glm::vec2(_currentDisplayPlugin->getRecommendedUiSize()) * _currentDisplayPlugin->getRenderResolutionScale(); + glm::vec2 maxOverlayPosition = glm::vec2(_currentDisplayPlugin->getRecommendedUiSize()); // FIXME don't allow negative mouseExtra glm::vec2 mouseExtra = (MOUSE_EXTENTS_PIXELS - maxOverlayPosition) / 2.0f; glm::vec2 minMouse = vec2(0) - mouseExtra; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 0d556544bb..9200843cf8 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -888,7 +888,7 @@ OpenGLDisplayPlugin::~OpenGLDisplayPlugin() { } void OpenGLDisplayPlugin::updateCompositeFramebuffer() { - auto renderSize = glm::uvec2(glm::vec2(getRecommendedRenderSize()) * getRenderResolutionScale()); + auto renderSize = glm::uvec2(getRecommendedRenderSize()); if (!_compositeFramebuffer || _compositeFramebuffer->getSize() != renderSize) { _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); } diff --git a/libraries/embedded-webserver/src/HTTPManager.cpp b/libraries/embedded-webserver/src/HTTPManager.cpp index 4a75994e12..ccebeaf9cc 100644 --- a/libraries/embedded-webserver/src/HTTPManager.cpp +++ b/libraries/embedded-webserver/src/HTTPManager.cpp @@ -48,6 +48,13 @@ void HTTPManager::incomingConnection(qintptr socketDescriptor) { } bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) { + // Reject paths with embedded NULs + if (url.path().contains(QChar(0x00))) { + connection->respond(HTTPConnection::StatusCode400, "Embedded NULs not allowed in requests"); + qCWarning(embeddedwebserver) << "Received a request with embedded NULs"; + return true; + } + if (!skipSubHandler && requestHandledByRequestHandler(connection, url)) { // this request was handled by our request handler object // so we don't need to attempt to do so in the document root @@ -57,17 +64,27 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, if (!_documentRoot.isEmpty()) { // check to see if there is a file to serve from the document root for this path QString subPath = url.path(); - + // remove any slash at the beginning of the path if (subPath.startsWith('/')) { subPath.remove(0, 1); } - + + QString absoluteDocumentRoot { QFileInfo(_documentRoot).absolutePath() }; QString filePath; - - if (QFileInfo(_documentRoot + subPath).isFile()) { - filePath = _documentRoot + subPath; - } else if (subPath.size() > 0 && !subPath.endsWith('/')) { + QFileInfo pathFileInfo { _documentRoot + subPath }; + QString absoluteFilePath { pathFileInfo.absoluteFilePath() }; + + // The absolute path for this file isn't under the document root + if (absoluteFilePath.indexOf(absoluteDocumentRoot) != 0) { + qCWarning(embeddedwebserver) << absoluteFilePath << "is outside the document root"; + connection->respond(HTTPConnection::StatusCode400, "Requested path outside document root"); + return true; + } + + if (pathFileInfo.isFile()) { + filePath = absoluteFilePath; + } else if (subPath.size() > 0 && !subPath.endsWith('/') && pathFileInfo.isDir()) { // this could be a directory with a trailing slash // send a redirect to the path with a slash so we can QString redirectLocation = '/' + subPath + '/'; @@ -80,6 +97,7 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, redirectHeader.insert(QByteArray("Location"), redirectLocation.toUtf8()); connection->respond(HTTPConnection::StatusCode302, "", HTTPConnection::DefaultContentType, redirectHeader); + return true; } // if the last thing is a trailing slash then we want to look for index file @@ -87,8 +105,8 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, QStringList possibleIndexFiles = QStringList() << "index.html" << "index.shtml"; foreach (const QString& possibleIndexFilename, possibleIndexFiles) { - if (QFileInfo(_documentRoot + subPath + possibleIndexFilename).exists()) { - filePath = _documentRoot + subPath + possibleIndexFilename; + if (QFileInfo(absoluteFilePath + possibleIndexFilename).exists()) { + filePath = absoluteFilePath + possibleIndexFilename; break; } } diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index e8f57ea834..78801df715 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -363,12 +363,18 @@ bool EntityRenderer::needsRenderUpdateFromEntity(const EntityItemPointer& entity return false; } -void EntityRenderer::updateModelTransform() { +void EntityRenderer::updateModelTransformAndBound() { bool success = false; auto newModelTransform = _entity->getTransformToCenter(success); if (success) { _modelTransform = newModelTransform; } + + success = false; + auto bound = _entity->getAABox(success); + if (success) { + _bound = bound; + } } void EntityRenderer::doRenderUpdateSynchronous(const ScenePointer& scene, Transaction& transaction, const EntityItemPointer& entity) { @@ -380,15 +386,7 @@ void EntityRenderer::doRenderUpdateSynchronous(const ScenePointer& scene, Transa } _prevIsTransparent = transparent; - bool success = false; - auto bound = entity->getAABox(success); - if (success) { - _bound = bound; - } - auto newModelTransform = entity->getTransformToCenter(success); - if (success) { - _modelTransform = newModelTransform; - } + updateModelTransformAndBound(); _moving = entity->isMovingRelativeToParent(); _visible = entity->getVisible(); diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 40966c4f41..496649eb5f 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -97,7 +97,7 @@ protected: virtual void doRender(RenderArgs* args) = 0; bool isFading() const { return _isFading; } - void updateModelTransform(); + void updateModelTransformAndBound(); virtual bool isTransparent() const { return _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f : false; } inline bool isValidRenderItem() const { return _renderItemID != Item::INVALID_ITEM_ID; } diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index a6a6dc05f2..309671f49e 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -126,7 +126,7 @@ void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePoi void* key = (void*)this; AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [this] () { withWriteLock([&] { - updateModelTransform(); + updateModelTransformAndBound(); _renderTransform = getModelTransform(); }); }); diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 2e0656ab81..c50b3bd760 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -106,10 +106,8 @@ void ShapeEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce _position = entity->getWorldPosition(); _dimensions = entity->getScaledDimensions(); _orientation = entity->getWorldOrientation(); - bool success = false; - auto newModelTransform = entity->getTransformToCenter(success); - _renderTransform = success ? newModelTransform : getModelTransform(); - + updateModelTransformAndBound(); + _renderTransform = getModelTransform(); if (_shape == entity::Sphere) { _renderTransform.postScale(SPHERE_ENTITY_SCALE); } diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index ce4b6d9175..08a3b585e4 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -70,7 +70,7 @@ void TextEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scen AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [this, entity] () { withWriteLock([&] { _dimensions = entity->getScaledDimensions(); - updateModelTransform(); + updateModelTransformAndBound(); _renderTransform = getModelTransform(); }); }); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 17d6d58781..793b4aa158 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -206,7 +206,7 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene glm::vec2 windowSize = getWindowSize(entity); _webSurface->resize(QSize(windowSize.x, windowSize.y)); - updateModelTransform(); + updateModelTransformAndBound(); _renderTransform = getModelTransform(); _renderTransform.postScale(entity->getScaledDimensions()); }); diff --git a/libraries/entities/src/AnimationPropertyGroup.cpp b/libraries/entities/src/AnimationPropertyGroup.cpp index 2db85eb7ac..95bdae43b9 100644 --- a/libraries/entities/src/AnimationPropertyGroup.cpp +++ b/libraries/entities/src/AnimationPropertyGroup.cpp @@ -22,13 +22,14 @@ const float AnimationPropertyGroup::MAXIMUM_POSSIBLE_FRAME = 100000.0f; bool operator==(const AnimationPropertyGroup& a, const AnimationPropertyGroup& b) { return - (a._currentFrame == b._currentFrame) && (a._running == b._running) && (a._loop == b._loop) && (a._hold == b._hold) && (a._firstFrame == b._firstFrame) && (a._lastFrame == b._lastFrame) && + (a._fps == b._fps) && + (a._allowTranslation == b._allowTranslation) && (a._url == b._url); } @@ -40,6 +41,8 @@ bool operator!=(const AnimationPropertyGroup& a, const AnimationPropertyGroup& b (a._hold != b._hold) || (a._firstFrame != b._firstFrame) || (a._lastFrame != b._lastFrame) || + (a._fps != b._fps) || + (a._allowTranslation != b._allowTranslation) || (a._url != b._url); } diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 31ec189cf9..8e382fabd4 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -2414,11 +2414,7 @@ bool EntityItem::shouldSuppressLocationEdits() const { } // if any of the ancestors are MyAvatar, suppress - if (isChildOfMyAvatar()) { - return true; - } - - return false; + return isChildOfMyAvatar(); } QList EntityItem::getActionsOfType(EntityDynamicType typeToGet) const { diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 14c42a1ac2..d2e40eb527 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1284,6 +1284,8 @@ bool EntityScriptingInterface::appendPoint(QUuid entityID, const glm::vec3& poin EntityItemPointer entity = static_cast(_entityTree->findEntityByEntityItemID(entityID)); if (!entity) { qCDebug(entities) << "EntityScriptingInterface::setPoints no entity with ID" << entityID; + // There is no entity + return false; } EntityTypes::EntityType entityType = entity->getType(); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 25bbe70abf..66dd6adfb5 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2582,6 +2582,13 @@ bool EntityTree::readFromMap(QVariantMap& map) { } } + // Zero out the spread values that were fixed in version ParticleEntityFix so they behave the same as before + if (contentVersion < (int)EntityVersion::ParticleEntityFix) { + properties.setRadiusSpread(0.0f); + properties.setAlphaSpread(0.0f); + properties.setColorSpread({0, 0, 0}); + } + EntityItemPointer entity = addEntity(entityItemID, properties); if (!entity) { qCDebug(entities) << "adding Entity failed:" << entityItemID << properties.getType(); diff --git a/libraries/entities/src/SimpleEntitySimulation.cpp b/libraries/entities/src/SimpleEntitySimulation.cpp index 3ed84035ac..28dc7b26c4 100644 --- a/libraries/entities/src/SimpleEntitySimulation.cpp +++ b/libraries/entities/src/SimpleEntitySimulation.cpp @@ -100,7 +100,7 @@ void SimpleEntitySimulation::changeEntityInternal(EntityItemPointer entity) { } } else if (entity->isMovingRelativeToParent()) { SetOfEntities::iterator itr = _simpleKinematicEntities.find(entity); - if (itr != _simpleKinematicEntities.end()) { + if (itr == _simpleKinematicEntities.end()) { _simpleKinematicEntities.insert(entity); entity->setLastSimulated(usecTimestampNow()); } @@ -118,7 +118,7 @@ void SimpleEntitySimulation::changeEntityInternal(EntityItemPointer entity) { if (entity->isMovingRelativeToParent()) { SetOfEntities::iterator itr = _simpleKinematicEntities.find(entity); - if (itr != _simpleKinematicEntities.end()) { + if (itr == _simpleKinematicEntities.end()) { _simpleKinematicEntities.insert(entity); entity->setLastSimulated(usecTimestampNow()); } diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index b830313723..d0efed3df4 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1816,7 +1816,6 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } } - float clusterScale = extractUniformScale(fbxCluster.inverseBindMatrix); glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform; ShapeVertices& points = shapeVertices.at(jointIndex); @@ -1832,7 +1831,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS if (weight >= EXPANSION_WEIGHT_THRESHOLD) { // transform to joint-frame and save for later const glm::mat4 vertexTransform = meshToJoint * glm::translate(extracted.mesh.vertices.at(newIndex)); - points.push_back(extractTranslation(vertexTransform) * clusterScale); + points.push_back(extractTranslation(vertexTransform)); } // look for an unused slot in the weights vector @@ -1886,12 +1885,11 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS FBXJoint& joint = geometry.joints[jointIndex]; // transform cluster vertices to joint-frame and save for later - float clusterScale = extractUniformScale(firstFBXCluster.inverseBindMatrix); glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform; ShapeVertices& points = shapeVertices.at(jointIndex); foreach (const glm::vec3& vertex, extracted.mesh.vertices) { const glm::mat4 vertexTransform = meshToJoint * glm::translate(vertex); - points.push_back(extractTranslation(vertexTransform) * clusterScale); + points.push_back(extractTranslation(vertexTransform)); } // Apply geometric offset, if present, by transforming the vertices directly diff --git a/libraries/fbx/src/GLTFReader.cpp b/libraries/fbx/src/GLTFReader.cpp index 1fa4b3873e..300e6f4846 100644 --- a/libraries/fbx/src/GLTFReader.cpp +++ b/libraries/fbx/src/GLTFReader.cpp @@ -21,14 +21,17 @@ #include #include + #include #include #include +#include #include #include #include +#include #include "FBXReader.h" @@ -786,13 +789,18 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { QVector raw_vertices; QVector raw_normals; - addArrayOfType(indicesBuffer.blob, + bool success = addArrayOfType(indicesBuffer.blob, indicesBufferview.byteOffset + indicesAccBoffset, - indicesBufferview.byteLength, + indicesAccessor.count, part.triangleIndices, indicesAccessor.type, indicesAccessor.componentType); + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF INDICES data for model " << _url; + continue; + } + QList keys = primitive.attributes.values.keys(); foreach(auto &key, keys) { @@ -805,44 +813,60 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { int accBoffset = accessor.defined["byteOffset"] ? accessor.byteOffset : 0; if (key == "POSITION") { QVector vertices; - addArrayOfType(buffer.blob, + success = addArrayOfType(buffer.blob, bufferview.byteOffset + accBoffset, - bufferview.byteLength, vertices, + accessor.count, vertices, accessor.type, accessor.componentType); + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF POSITION data for model " << _url; + continue; + } for (int n = 0; n < vertices.size(); n = n + 3) { mesh.vertices.push_back(glm::vec3(vertices[n], vertices[n + 1], vertices[n + 2])); } } else if (key == "NORMAL") { QVector normals; - addArrayOfType(buffer.blob, + success = addArrayOfType(buffer.blob, bufferview.byteOffset + accBoffset, - bufferview.byteLength, + accessor.count, normals, accessor.type, accessor.componentType); + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF NORMAL data for model " << _url; + continue; + } for (int n = 0; n < normals.size(); n = n + 3) { mesh.normals.push_back(glm::vec3(normals[n], normals[n + 1], normals[n + 2])); } } else if (key == "TEXCOORD_0") { QVector texcoords; - addArrayOfType(buffer.blob, + success = addArrayOfType(buffer.blob, bufferview.byteOffset + accBoffset, - bufferview.byteLength, + accessor.count, texcoords, accessor.type, accessor.componentType); + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF TEXCOORD_0 data for model " << _url; + continue; + } for (int n = 0; n < texcoords.size(); n = n + 2) { mesh.texCoords.push_back(glm::vec2(texcoords[n], texcoords[n + 1])); } } else if (key == "TEXCOORD_1") { QVector texcoords; - addArrayOfType(buffer.blob, + success = addArrayOfType(buffer.blob, bufferview.byteOffset + accBoffset, - bufferview.byteLength, + accessor.count, texcoords, accessor.type, accessor.componentType); + if (!success) { + qWarning(modelformat) << "There was a problem reading glTF TEXCOORD_1 data for model " << _url; + continue; + } for (int n = 0; n < texcoords.size(); n = n + 2) { mesh.texCoords1.push_back(glm::vec2(texcoords[n], texcoords[n + 1])); } @@ -888,8 +912,16 @@ bool GLTFReader::buildGeometry(FBXGeometry& geometry, const QUrl& url) { FBXGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping, const QUrl& url, bool loadLightmaps, float lightmapLevel) { + _url = url; + // Normalize url for local files + QUrl normalizeUrl = DependencyManager::get()->normalizeURL(url); + if (normalizeUrl.scheme().isEmpty() || (normalizeUrl.scheme() == "file")) { + QString localFileName = PathUtils::expandToLocalDataAbsolutePath(normalizeUrl).toLocalFile(); + _url = QUrl(QFileInfo(localFileName).absoluteFilePath()); + } + parseGLTF(model); //_file.dump(); FBXGeometry* geometryPtr = new FBXGeometry(); @@ -904,6 +936,7 @@ FBXGeometry* GLTFReader::readGLTF(QByteArray& model, const QVariantHash& mapping bool GLTFReader::readBinary(const QString& url, QByteArray& outdata) { QUrl binaryUrl = _url.resolved(QUrl(url).fileName()); + qCDebug(modelformat) << "binaryUrl: " << binaryUrl << " OriginalUrl: " << _url; bool success; std::tie(success, outdata) = requestData(binaryUrl); @@ -1018,13 +1051,12 @@ void GLTFReader::setFBXMaterial(FBXMaterial& fbxmat, const GLTFMaterial& materia fbxmat.opacityTexture = getFBXTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]); fbxmat.albedoTexture = getFBXTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]); fbxmat.useAlbedoMap = true; - fbxmat.metallicTexture = getFBXTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]); - fbxmat.useMetallicMap = true; } if (material.pbrMetallicRoughness.defined["metallicRoughnessTexture"]) { fbxmat.roughnessTexture = getFBXTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]); fbxmat.useRoughnessMap = true; - + fbxmat.metallicTexture = getFBXTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]); + fbxmat.useMetallicMap = true; } if (material.pbrMetallicRoughness.defined["roughnessFactor"]) { fbxmat._material->setRoughness(material.pbrMetallicRoughness.roughnessFactor); @@ -1043,7 +1075,7 @@ void GLTFReader::setFBXMaterial(FBXMaterial& fbxmat, const GLTFMaterial& materia } template -bool GLTFReader::readArray(const QByteArray& bin, int byteOffset, int byteLength, +bool GLTFReader::readArray(const QByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType) { QDataStream blobstream(bin); @@ -1051,142 +1083,77 @@ bool GLTFReader::readArray(const QByteArray& bin, int byteOffset, int byteLength blobstream.setVersion(QDataStream::Qt_5_9); blobstream.setFloatingPointPrecision(QDataStream::FloatingPointPrecision::SinglePrecision); - int vsize = byteLength / sizeof(T); - - qCDebug(modelformat) << "size1: " << vsize; + qCDebug(modelformat) << "size1: " << count; int dataskipped = blobstream.skipRawData(byteOffset); qCDebug(modelformat) << "dataskipped: " << dataskipped; - - while (outarray.size() < vsize) { - - T value1, value2, value3, value4, - value5, value6, value7, value8, - value9, value10, value11, value12, - value13, value14, value15, value16; - - if (accessorType == GLTFAccessorType::SCALAR) { - - blobstream >> value1; - - outarray.push_back(value1); - } else if (accessorType == GLTFAccessorType::VEC2) { - - blobstream >> value1; - blobstream >> value2; - - outarray.push_back(value1); - outarray.push_back(value2); - } else if (accessorType == GLTFAccessorType::VEC3) { - - blobstream >> value1; - blobstream >> value2; - blobstream >> value3; - - outarray.push_back(value1); - outarray.push_back(value2); - outarray.push_back(value3); - } else if (accessorType == GLTFAccessorType::VEC4 || accessorType == GLTFAccessorType::MAT2) { - - blobstream >> value1; - blobstream >> value2; - blobstream >> value3; - blobstream >> value4; - - outarray.push_back(value1); - outarray.push_back(value2); - outarray.push_back(value3); - outarray.push_back(value4); - } else if (accessorType == GLTFAccessorType::MAT3) { - - blobstream >> value1; - blobstream >> value2; - blobstream >> value3; - blobstream >> value4; - blobstream >> value5; - blobstream >> value6; - blobstream >> value7; - blobstream >> value8; - blobstream >> value9; - - outarray.push_back(value1); - outarray.push_back(value2); - outarray.push_back(value3); - outarray.push_back(value4); - outarray.push_back(value5); - outarray.push_back(value6); - outarray.push_back(value7); - outarray.push_back(value8); - outarray.push_back(value9); - } else if (accessorType == GLTFAccessorType::MAT4) { - - blobstream >> value1; - blobstream >> value2; - blobstream >> value3; - blobstream >> value4; - blobstream >> value5; - blobstream >> value6; - blobstream >> value7; - blobstream >> value8; - blobstream >> value9; - blobstream >> value10; - blobstream >> value11; - blobstream >> value12; - blobstream >> value13; - blobstream >> value14; - blobstream >> value15; - blobstream >> value16; - - outarray.push_back(value1); - outarray.push_back(value2); - outarray.push_back(value3); - outarray.push_back(value4); - outarray.push_back(value5); - outarray.push_back(value6); - outarray.push_back(value7); - outarray.push_back(value8); - outarray.push_back(value9); - outarray.push_back(value10); - outarray.push_back(value11); - outarray.push_back(value12); - outarray.push_back(value13); - outarray.push_back(value14); - outarray.push_back(value15); - outarray.push_back(value16); - + int bufferCount = 0; + switch (accessorType) { + case GLTFAccessorType::SCALAR: + bufferCount = 1; + break; + case GLTFAccessorType::VEC2: + bufferCount = 2; + break; + case GLTFAccessorType::VEC3: + bufferCount = 3; + break; + case GLTFAccessorType::VEC4: + bufferCount = 4; + break; + case GLTFAccessorType::MAT2: + bufferCount = 4; + break; + case GLTFAccessorType::MAT3: + bufferCount = 9; + break; + case GLTFAccessorType::MAT4: + bufferCount = 16; + break; + default: + qWarning(modelformat) << "Unknown accessorType: " << accessorType; + blobstream.unsetDevice(); + return false; + } + for (int i = 0; i < count; i++) { + for (int j = 0; j < bufferCount; j++) { + if (!blobstream.atEnd()) { + T value; + blobstream >> value; + outarray.push_back(value); + } else { + blobstream.unsetDevice(); + return false; + } } } + blobstream.unsetDevice(); return true; } template -bool GLTFReader::addArrayOfType(const QByteArray& bin, int byteOffset, int byteLength, +bool GLTFReader::addArrayOfType(const QByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType, int componentType) { switch (componentType) { case GLTFAccessorComponentType::BYTE: {} case GLTFAccessorComponentType::UNSIGNED_BYTE: { - readArray(bin, byteOffset, byteLength, outarray, accessorType); - break; + return readArray(bin, byteOffset, count, outarray, accessorType); } case GLTFAccessorComponentType::SHORT: { - readArray(bin, byteOffset, byteLength, outarray, accessorType); - break; + return readArray(bin, byteOffset, count, outarray, accessorType); } case GLTFAccessorComponentType::UNSIGNED_INT: { - readArray(bin, byteOffset, byteLength, outarray, accessorType); - break; + return readArray(bin, byteOffset, count, outarray, accessorType); } case GLTFAccessorComponentType::UNSIGNED_SHORT: { - readArray(bin, byteOffset, byteLength, outarray, accessorType); - break; + return readArray(bin, byteOffset, count, outarray, accessorType); } case GLTFAccessorComponentType::FLOAT: { - readArray(bin, byteOffset, byteLength, outarray, accessorType); - break; + return readArray(bin, byteOffset, count, outarray, accessorType); } } - return true; + return false; } void GLTFReader::retriangulate(const QVector& inIndices, const QVector& in_vertices, diff --git a/libraries/fbx/src/GLTFReader.h b/libraries/fbx/src/GLTFReader.h index 28c1d8282f..2183256b87 100644 --- a/libraries/fbx/src/GLTFReader.h +++ b/libraries/fbx/src/GLTFReader.h @@ -762,11 +762,11 @@ private: bool readBinary(const QString& url, QByteArray& outdata); template - bool readArray(const QByteArray& bin, int byteOffset, int byteLength, + bool readArray(const QByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType); template - bool addArrayOfType(const QByteArray& bin, int byteOffset, int byteLength, + bool addArrayOfType(const QByteArray& bin, int byteOffset, int count, QVector& outarray, int accessorType, int componentType); void retriangulate(const QVector& in_indices, const QVector& in_vertices, diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackendPipeline.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackendPipeline.cpp index 91f1d8bb8c..adea3292e1 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackendPipeline.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackendPipeline.cpp @@ -48,12 +48,12 @@ void GLBackend::do_setPipeline(const Batch& batch, size_t paramOffset) { return; } - // check the program cache - // pick the program version - // check the program cache - // pick the program version + // check the program cache + // pick the program version + // check the program cache + // pick the program version #ifdef GPU_STEREO_CAMERA_BUFFER - GLuint glprogram = pipelineObject->_program->getProgram((GLShader::Version) isStereo()); + GLuint glprogram = pipelineObject->_program->getProgram((GLShader::Version)isStereo()); #else GLuint glprogram = pipelineObject->_program->getProgram(); #endif @@ -85,10 +85,11 @@ void GLBackend::do_setPipeline(const Batch& batch, size_t paramOffset) { } else { cameraCorrectionBuffer = syncGPUObject(*_pipeline._cameraCorrectionBufferIdentity._buffer); } + // Invalidate uniform buffer cache slot + _uniform._buffers[_pipeline._cameraCorrectionLocation].reset(); glBindBufferRange(GL_UNIFORM_BUFFER, _pipeline._cameraCorrectionLocation, cameraCorrectionBuffer->_id, 0, sizeof(CameraCorrection)); - } - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); _pipeline._invalidProgram = false; } } @@ -97,7 +98,7 @@ void GLBackend::updatePipeline() { if (_pipeline._invalidProgram) { // doing it here is aproblem for calls to glUniform.... so will do it on assing... glUseProgram(_pipeline._program); - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); _pipeline._invalidProgram = false; } @@ -106,12 +107,12 @@ void GLBackend::updatePipeline() { // first reset to default what should be // the fields which were not to default and are default now resetPipelineState(_pipeline._state->_signature); - + // Update the signature cache with what's going to be touched _pipeline._stateSignatureCache |= _pipeline._state->_signature; // And perform - for (auto command: _pipeline._state->_commands) { + for (auto command : _pipeline._state->_commands) { command->run(this); } } else { @@ -142,8 +143,8 @@ void GLBackend::releaseUniformBuffer(uint32_t slot) { if (buf) { auto* object = Backend::getGPUObject(*buf); if (object) { - glBindBufferBase(GL_UNIFORM_BUFFER, slot, 0); // RELEASE - (void) CHECK_GL_ERROR(); + glBindBufferBase(GL_UNIFORM_BUFFER, slot, 0); // RELEASE + (void)CHECK_GL_ERROR(); } buf.reset(); } @@ -157,8 +158,9 @@ void GLBackend::resetUniformStage() { void GLBackend::do_setUniformBuffer(const Batch& batch, size_t paramOffset) { GLuint slot = batch._params[paramOffset + 3]._uint; - if (slot >(GLuint)MAX_NUM_UNIFORM_BUFFERS) { - qCDebug(gpugllogging) << "GLBackend::do_setUniformBuffer: Trying to set a uniform Buffer at slot #" << slot << " which doesn't exist. MaxNumUniformBuffers = " << getMaxNumUniformBuffers(); + if (slot > (GLuint)MAX_NUM_UNIFORM_BUFFERS) { + qCDebug(gpugllogging) << "GLBackend::do_setUniformBuffer: Trying to set a uniform Buffer at slot #" << slot + << " which doesn't exist. MaxNumUniformBuffers = " << getMaxNumUniformBuffers(); return; } BufferPointer uniformBuffer = batch._buffers.get(batch._params[paramOffset + 2]._uint); @@ -169,7 +171,7 @@ void GLBackend::do_setUniformBuffer(const Batch& batch, size_t paramOffset) { releaseUniformBuffer(slot); return; } - + // check cache before thinking if (_uniform._buffers[slot] == uniformBuffer) { return; @@ -181,7 +183,7 @@ void GLBackend::do_setUniformBuffer(const Batch& batch, size_t paramOffset) { glBindBufferRange(GL_UNIFORM_BUFFER, slot, object->_buffer, rangeStart, rangeSize); _uniform._buffers[slot] = uniformBuffer; - (void) CHECK_GL_ERROR(); + (void)CHECK_GL_ERROR(); } else { releaseUniformBuffer(slot); return; @@ -195,27 +197,28 @@ void GLBackend::releaseResourceTexture(uint32_t slot) { if (object) { GLuint target = object->_target; glActiveTexture(GL_TEXTURE0 + slot); - glBindTexture(target, 0); // RELEASE - (void) CHECK_GL_ERROR(); + glBindTexture(target, 0); // RELEASE + (void)CHECK_GL_ERROR(); } tex.reset(); } } void GLBackend::resetResourceStage() { - for (uint32_t i = 0; i < _resource._buffers.size(); i++) { + uint32_t i; + for (i = 0; i < _resource._buffers.size(); i++) { releaseResourceBuffer(i); } - for (uint32_t i = 0; i < _resource._textures.size(); i++) { + for (i = 0; i < _resource._textures.size(); i++) { releaseResourceTexture(i); } } - void GLBackend::do_setResourceBuffer(const Batch& batch, size_t paramOffset) { GLuint slot = batch._params[paramOffset + 1]._uint; if (slot >= (GLuint)MAX_NUM_RESOURCE_BUFFERS) { - qCDebug(gpugllogging) << "GLBackend::do_setResourceBuffer: Trying to set a resource Buffer at slot #" << slot << " which doesn't exist. MaxNumResourceBuffers = " << getMaxNumResourceBuffers(); + qCDebug(gpugllogging) << "GLBackend::do_setResourceBuffer: Trying to set a resource Buffer at slot #" << slot + << " which doesn't exist. MaxNumResourceBuffers = " << getMaxNumResourceBuffers(); return; } @@ -236,7 +239,7 @@ void GLBackend::do_setResourceBuffer(const Batch& batch, size_t paramOffset) { // If successful bind then cache it if (bindResourceBuffer(slot, resourceBuffer)) { _resource._buffers[slot] = resourceBuffer; - } else { // else clear slot and cache + } else { // else clear slot and cache releaseResourceBuffer(slot); return; } @@ -244,8 +247,9 @@ void GLBackend::do_setResourceBuffer(const Batch& batch, size_t paramOffset) { void GLBackend::do_setResourceTexture(const Batch& batch, size_t paramOffset) { GLuint slot = batch._params[paramOffset + 1]._uint; - if (slot >= (GLuint) MAX_NUM_RESOURCE_TEXTURES) { - qCDebug(gpugllogging) << "GLBackend::do_setResourceTexture: Trying to set a resource Texture at slot #" << slot << " which doesn't exist. MaxNumResourceTextures = " << getMaxNumResourceTextures(); + if (slot >= (GLuint)MAX_NUM_RESOURCE_TEXTURES) { + qCDebug(gpugllogging) << "GLBackend::do_setResourceTexture: Trying to set a resource Texture at slot #" << slot + << " which doesn't exist. MaxNumResourceTextures = " << getMaxNumResourceTextures(); return; } @@ -264,11 +268,14 @@ void GLBackend::bindResourceTexture(uint32_t slot, const TexturePointer& resourc void GLBackend::do_setResourceFramebufferSwapChainTexture(const Batch& batch, size_t paramOffset) { GLuint slot = batch._params[paramOffset + 1]._uint; if (slot >= (GLuint)MAX_NUM_RESOURCE_TEXTURES) { - qCDebug(gpugllogging) << "GLBackend::do_setResourceFramebufferSwapChainTexture: Trying to set a resource Texture at slot #" << slot << " which doesn't exist. MaxNumResourceTextures = " << getMaxNumResourceTextures(); + qCDebug(gpugllogging) + << "GLBackend::do_setResourceFramebufferSwapChainTexture: Trying to set a resource Texture at slot #" << slot + << " which doesn't exist. MaxNumResourceTextures = " << getMaxNumResourceTextures(); return; } - auto swapChain = std::static_pointer_cast(batch._swapChains.get(batch._params[paramOffset + 0]._uint)); + auto swapChain = + std::static_pointer_cast(batch._swapChains.get(batch._params[paramOffset + 0]._uint)); if (!swapChain) { releaseResourceTexture(slot); diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackendTransform.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackendTransform.cpp index 35d292cd46..ed356acf68 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackendTransform.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackendTransform.cpp @@ -168,7 +168,9 @@ void GLBackend::TransformStageState::update(size_t commandIndex, const StereoSta void GLBackend::TransformStageState::bindCurrentCamera(int eye) const { if (_currentCameraOffset != INVALID_OFFSET) { - glBindBufferRange(GL_UNIFORM_BUFFER, TRANSFORM_CAMERA_SLOT, _cameraBuffer, _currentCameraOffset + eye * _cameraUboSize, sizeof(CameraBufferElement)); + static_assert(TRANSFORM_CAMERA_SLOT >= MAX_NUM_UNIFORM_BUFFERS, "TransformCamera may overlap pipeline uniform buffer slots. Invalidate uniform buffer slot cache for safety (call _uniform._buffers[TRANSFORM_CAMERA_SLOT].reset())."); + glBindBufferRange(GL_UNIFORM_BUFFER, TRANSFORM_CAMERA_SLOT, _cameraBuffer, _currentCameraOffset + eye * _cameraUboSize, + sizeof(CameraBufferElement)); } } diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLTexture.cpp index e11f8f01c7..e9494a1271 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLTexture.cpp @@ -59,7 +59,11 @@ const size_t GLVariableAllocationSupport::MAX_BUFFER_SIZE = MAX_TRANSFER_SIZE; GLenum GLTexture::getGLTextureType(const Texture& texture) { switch (texture.getType()) { case Texture::TEX_2D: - return GL_TEXTURE_2D; + if (!texture.isArray()) { + return GL_TEXTURE_2D; + } else { + return GL_TEXTURE_2D_ARRAY; + } break; case Texture::TEX_CUBE: @@ -77,6 +81,7 @@ GLenum GLTexture::getGLTextureType(const Texture& texture) { uint8_t GLTexture::getFaceCount(GLenum target) { switch (target) { case GL_TEXTURE_2D: + case GL_TEXTURE_2D_ARRAY: return TEXTURE_2D_NUM_FACES; case GL_TEXTURE_CUBE_MAP: return TEXTURE_CUBE_NUM_FACES; @@ -86,17 +91,22 @@ uint8_t GLTexture::getFaceCount(GLenum target) { } } const std::vector& GLTexture::getFaceTargets(GLenum target) { - static std::vector cubeFaceTargets { + static const std::vector cubeFaceTargets { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z }; - static std::vector faceTargets { + static const std::vector faceTargets { GL_TEXTURE_2D }; + static const std::vector arrayFaceTargets{ + GL_TEXTURE_2D_ARRAY + }; switch (target) { case GL_TEXTURE_2D: return faceTargets; + case GL_TEXTURE_2D_ARRAY: + return arrayFaceTargets; case GL_TEXTURE_CUBE_MAP: return cubeFaceTargets; default: diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp index a5ef2d92e1..1d512103bd 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp @@ -64,7 +64,12 @@ public: } if (gltexture) { - glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, gltexture->_texture, 0); + if (gltexture->_target == GL_TEXTURE_2D) { + glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, gltexture->_texture, 0); + } else { + glFramebufferTextureLayer(GL_FRAMEBUFFER, colorAttachments[unit], gltexture->_texture, 0, + b._subresource); + } _colorBuffers.push_back(colorAttachments[unit]); } else { glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, 0, 0); @@ -91,7 +96,12 @@ public: } if (gltexture) { - glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); + if (gltexture->_target == GL_TEXTURE_2D) { + glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); + } else { + glFramebufferTextureLayer(GL_FRAMEBUFFER, attachement, gltexture->_texture, 0, + _gpuObject.getDepthStencilBufferSubresource()); + } } else { glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, 0, 0); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index a255cc5878..97b1a96a1d 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -182,7 +182,7 @@ void GL41Texture::syncSampler() const { glTexParameteri(_target, GL_TEXTURE_MAG_FILTER, fm.magFilter); if (sampler.doComparison()) { - glTexParameteri(_target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE_ARB); + glTexParameteri(_target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); glTexParameteri(_target, GL_TEXTURE_COMPARE_FUNC, COMPARISON_TO_GL[sampler.getComparisonFunction()]); } else { glTexParameteri(_target, GL_TEXTURE_COMPARE_MODE, GL_NONE); @@ -197,7 +197,7 @@ void GL41Texture::syncSampler() const { glTexParameterf(_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); glTexParameterf(_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); - glTexParameterf(_target, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); + glTexParameterf(_target, GL_TEXTURE_MAX_ANISOTROPY, sampler.getMaxAnisotropy()); } using GL41FixedAllocationTexture = GL41Backend::GL41FixedAllocationTexture; @@ -215,12 +215,19 @@ GL41FixedAllocationTexture::~GL41FixedAllocationTexture() { void GL41FixedAllocationTexture::allocateStorage() const { const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); const auto numMips = _gpuObject.getNumMips(); + const auto numSlices = _gpuObject.getNumSlices(); // glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); for (GLint level = 0; level < numMips; level++) { Vec3u dimensions = _gpuObject.evalMipDimensions(level); for (GLenum target : getFaceTargets(_target)) { - glTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, nullptr); + if (!_gpuObject.isArray()) { + glTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, + texelFormat.type, nullptr); + } else { + glTexImage3D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, numSlices, 0, + texelFormat.format, texelFormat.type, nullptr); + } } } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp index ca53d6c624..86332558e3 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp @@ -60,7 +60,11 @@ public: } if (gltexture) { - glNamedFramebufferTexture(_id, colorAttachments[unit], gltexture->_texture, 0); + if (gltexture->_target == GL_TEXTURE_2D) { + glNamedFramebufferTexture(_id, colorAttachments[unit], gltexture->_texture, 0); + } else { + glNamedFramebufferTextureLayer(_id, colorAttachments[unit], gltexture->_texture, 0, b._subresource); + } _colorBuffers.push_back(colorAttachments[unit]); } else { glNamedFramebufferTexture(_id, colorAttachments[unit], 0, 0); @@ -87,14 +91,18 @@ public: } if (gltexture) { - glNamedFramebufferTexture(_id, attachement, gltexture->_texture, 0); + if (gltexture->_target == GL_TEXTURE_2D) { + glNamedFramebufferTexture(_id, attachement, gltexture->_texture, 0); + } else { + glNamedFramebufferTextureLayer(_id, attachement, gltexture->_texture, 0, + _gpuObject.getDepthStencilBufferSubresource()); + } } else { glNamedFramebufferTexture(_id, attachement, 0, 0); } _depthStamp = _gpuObject.getDepthStamp(); } - // Last but not least, define where we draw if (!_colorBuffers.empty()) { glNamedFramebufferDrawBuffers(_id, (GLsizei)_colorBuffers.size(), _colorBuffers.data()); diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index fda7ac22dd..e02f12819e 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -152,7 +152,7 @@ public: glSamplerParameteri(result, GL_TEXTURE_MIN_FILTER, fm.minFilter); glSamplerParameteri(result, GL_TEXTURE_MAG_FILTER, fm.magFilter); if (sampler.doComparison()) { - glSamplerParameteri(result, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE_ARB); + glSamplerParameteri(result, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); glSamplerParameteri(result, GL_TEXTURE_COMPARE_FUNC, COMPARISON_TO_GL[sampler.getComparisonFunction()]); } else { glSamplerParameteri(result, GL_TEXTURE_COMPARE_MODE, GL_NONE); @@ -341,7 +341,7 @@ void GL45Texture::syncSampler() const { glTextureParameteri(_id, GL_TEXTURE_MAG_FILTER, fm.magFilter); if (sampler.doComparison()) { - glTextureParameteri(_id, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE_ARB); + glTextureParameteri(_id, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); glTextureParameteri(_id, GL_TEXTURE_COMPARE_FUNC, COMPARISON_TO_GL[sampler.getComparisonFunction()]); } else { glTextureParameteri(_id, GL_TEXTURE_COMPARE_MODE, GL_NONE); @@ -374,8 +374,13 @@ void GL45FixedAllocationTexture::allocateStorage() const { const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); const auto dimensions = _gpuObject.getDimensions(); const auto mips = _gpuObject.getNumMips(); + const auto numSlices = _gpuObject.getNumSlices(); - glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); + if (!_gpuObject.isArray()) { + glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); + } else { + glTextureStorage3D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y, numSlices); + } glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, mips - 1); diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp index 0bf1548a4b..9c3a83ce13 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp @@ -64,7 +64,12 @@ public: } if (gltexture) { - glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, gltexture->_texture, 0); + if (gltexture->_target == GL_TEXTURE_2D) { + glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, gltexture->_texture, 0); + } else { + glFramebufferTextureLayer(GL_FRAMEBUFFER, colorAttachments[unit], gltexture->_texture, 0, + b._subresource); + } _colorBuffers.push_back(colorAttachments[unit]); } else { glFramebufferTexture2D(GL_FRAMEBUFFER, colorAttachments[unit], GL_TEXTURE_2D, 0, 0); @@ -91,7 +96,12 @@ public: } if (gltexture) { - glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); + if (gltexture->_target == GL_TEXTURE_2D) { + glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, gltexture->_texture, 0); + } else { + glFramebufferTextureLayer(GL_FRAMEBUFFER, attachement, gltexture->_texture, 0, + _gpuObject.getDepthStencilBufferSubresource()); + } } else { glFramebufferTexture2D(GL_FRAMEBUFFER, attachement, GL_TEXTURE_2D, 0, 0); } diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp index bbc02c2af6..911dfb8bb8 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp @@ -268,16 +268,27 @@ GLsizei getCompressedImageSize(int width, int height, GLenum internalFormat) { void GLESFixedAllocationTexture::allocateStorage() const { const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); const auto numMips = _gpuObject.getNumMips(); + const auto numSlices = _gpuObject.getNumSlices(); // glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); for (GLint level = 0; level < numMips; level++) { Vec3u dimensions = _gpuObject.evalMipDimensions(level); for (GLenum target : getFaceTargets(_target)) { if (texelFormat.isCompressed()) { - glCompressedTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, - getCompressedImageSize(dimensions.x, dimensions.y, texelFormat.internalFormat), nullptr); + auto size = getCompressedImageSize(dimensions.x, dimensions.y, texelFormat.internalFormat); + if (!_gpuObject.isArray()) { + glCompressedTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, size, nullptr); + } else { + glCompressedTexImage3D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, numSlices, 0, size * numSlices, nullptr); + } } else { - glTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, nullptr); + if (!_gpuObject.isArray()) { + glTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, + texelFormat.type, nullptr); + } else { + glTexImage3D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, numSlices, 0, + texelFormat.format, texelFormat.type, nullptr); + } } } } diff --git a/libraries/gpu/src/gpu/Frame.cpp b/libraries/gpu/src/gpu/Frame.cpp index d08a8ab56d..f1001d97d2 100644 --- a/libraries/gpu/src/gpu/Frame.cpp +++ b/libraries/gpu/src/gpu/Frame.cpp @@ -21,10 +21,7 @@ Frame::~Frame() { framebuffer.reset(); } - assert(bufferUpdates.empty()); - if (!bufferUpdates.empty()) { - qFatal("Buffer sync error... frame destroyed without buffer updates being applied"); - } + bufferUpdates.clear(); } void Frame::finish() { diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index a92243f808..34262b0cd9 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -184,6 +184,10 @@ TexturePointer Texture::createRenderBuffer(const Element& texelFormat, uint16 wi return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, 1, 0, numMips, sampler); } +TexturePointer Texture::createRenderBufferArray(const Element& texelFormat, uint16 width, uint16 height, uint16 numSlices, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, 1, numSlices, numMips, sampler); +} + TexturePointer Texture::create1D(const Element& texelFormat, uint16 width, uint16 numMips, const Sampler& sampler) { return create(TextureUsageType::RESOURCE, TEX_1D, texelFormat, width, 1, 1, 1, 0, numMips, sampler); } @@ -192,6 +196,10 @@ TexturePointer Texture::create2D(const Element& texelFormat, uint16 width, uint1 return create(TextureUsageType::RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, numMips, sampler); } +TexturePointer Texture::create2DArray(const Element& texelFormat, uint16 width, uint16 height, uint16 numSlices, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::STRICT_RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, numSlices, numMips, sampler); +} + TexturePointer Texture::createStrict(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips, const Sampler& sampler) { return create(TextureUsageType::STRICT_RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, numMips, sampler); } diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 09b2bc9475..9ad5dc0816 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -374,9 +374,11 @@ public: static const uint16 SINGLE_MIP = 1; static TexturePointer create1D(const Element& texelFormat, uint16 width, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); static TexturePointer create2D(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static TexturePointer create2DArray(const Element& texelFormat, uint16 width, uint16 height, uint16 numSlices, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); static TexturePointer create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); static TexturePointer createCube(const Element& texelFormat, uint16 width, uint16 numMips = 1, const Sampler& sampler = Sampler()); static TexturePointer createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static TexturePointer createRenderBufferArray(const Element& texelFormat, uint16 width, uint16 height, uint16 numSlices, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); static TexturePointer createStrict(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); static TexturePointer createExternal(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp index 0d4abe78d3..1b7b552078 100644 --- a/libraries/gpu/src/gpu/Texture_ktx.cpp +++ b/libraries/gpu/src/gpu/Texture_ktx.cpp @@ -515,7 +515,7 @@ TexturePointer Texture::build(const ktx::KTXDescriptor& descriptor) { header.getPixelHeight(), header.getPixelDepth(), 1, // num Samples - header.getNumberOfSlices(), + header.isArray() ? header.getNumberOfSlices() : 0, header.getNumberOfLevels(), samplerDesc); texture->setUsage(gpuktxKeyValue._usage); diff --git a/libraries/graphics/src/graphics/Haze.cpp b/libraries/graphics/src/graphics/Haze.cpp index d5a060b90b..ded48429ba 100644 --- a/libraries/graphics/src/graphics/Haze.cpp +++ b/libraries/graphics/src/graphics/Haze.cpp @@ -177,9 +177,9 @@ void Haze::setHazeBaseReference(const float hazeBaseReference) { void Haze::setHazeBackgroundBlend(const float hazeBackgroundBlend) { auto& params = _hazeParametersBuffer.get(); - - if (params.hazeBackgroundBlend != hazeBackgroundBlend) { - _hazeParametersBuffer.edit().hazeBackgroundBlend = hazeBackgroundBlend; + auto newBlend = 1.0f - hazeBackgroundBlend; + if (params.hazeBackgroundBlend != newBlend) { + _hazeParametersBuffer.edit().hazeBackgroundBlend = newBlend; } } diff --git a/libraries/ktx/src/ktx/KTX.h b/libraries/ktx/src/ktx/KTX.h index 54a8188a42..d755a482e3 100644 --- a/libraries/ktx/src/ktx/KTX.h +++ b/libraries/ktx/src/ktx/KTX.h @@ -163,6 +163,7 @@ namespace ktx { uint32_t getPixelDepth() const { return (pixelDepth ? pixelDepth : 1); } uint32_t getNumberOfSlices() const { return (numberOfArrayElements ? numberOfArrayElements : 1); } uint32_t getNumberOfLevels() const { return (numberOfMipmapLevels ? numberOfMipmapLevels : 1); } + bool isArray() const { return numberOfArrayElements > 0; } bool isCompressed() const { return glFormat == COMPRESSED_FORMAT; } uint32_t evalMaxDimension() const; diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 05f0ec12b5..5b3196a2bf 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -46,21 +47,18 @@ Q_DECLARE_METATYPE(JSONCallbackParameters) const QString ACCOUNTS_GROUP = "accounts"; -JSONCallbackParameters::JSONCallbackParameters(QObject* jsonCallbackReceiver, const QString& jsonCallbackMethod, - QObject* errorCallbackReceiver, const QString& errorCallbackMethod, - QObject* updateReceiver, const QString& updateSlot) : - jsonCallbackReceiver(jsonCallbackReceiver), +JSONCallbackParameters::JSONCallbackParameters(QObject* callbackReceiver, + const QString& jsonCallbackMethod, + const QString& errorCallbackMethod) : + callbackReceiver(callbackReceiver), jsonCallbackMethod(jsonCallbackMethod), - errorCallbackReceiver(errorCallbackReceiver), - errorCallbackMethod(errorCallbackMethod), - updateReciever(updateReceiver), - updateSlot(updateSlot) + errorCallbackMethod(errorCallbackMethod) { } -QJsonObject AccountManager::dataObjectFromResponse(QNetworkReply &requestReply) { - QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); +QJsonObject AccountManager::dataObjectFromResponse(QNetworkReply* requestReply) { + QJsonObject jsonObject = QJsonDocument::fromJson(requestReply->readAll()).object(); static const QString STATUS_KEY = "status"; static const QString DATA_KEY = "data"; @@ -74,8 +72,7 @@ QJsonObject AccountManager::dataObjectFromResponse(QNetworkReply &requestReply) AccountManager::AccountManager(UserAgentGetter userAgentGetter) : _userAgentGetter(userAgentGetter), - _authURL(), - _pendingCallbackMap() + _authURL() { qRegisterMetaType("OAuthAccessToken"); qRegisterMetaTypeStreamOperators("OAuthAccessToken"); @@ -323,75 +320,66 @@ void AccountManager::sendRequest(const QString& path, } } - - if (!callbackParams.isEmpty()) { - // if we have information for a callback, insert the callbackParams into our local map - _pendingCallbackMap.insert(networkReply, callbackParams); - - if (callbackParams.updateReciever && !callbackParams.updateSlot.isEmpty()) { - callbackParams.updateReciever->connect(networkReply, SIGNAL(uploadProgress(qint64, qint64)), - callbackParams.updateSlot.toStdString().c_str()); + connect(networkReply, &QNetworkReply::finished, this, [this, networkReply] { + // double check if the finished network reply had a session ID in the header and make + // sure that our session ID matches that value if so + if (networkReply->hasRawHeader(METAVERSE_SESSION_ID_HEADER)) { + _sessionID = networkReply->rawHeader(METAVERSE_SESSION_ID_HEADER); } + }); + + + if (callbackParams.isEmpty()) { + connect(networkReply, &QNetworkReply::finished, networkReply, &QNetworkReply::deleteLater); + } else { + // There's a cleaner way to fire the JSON/error callbacks below and ensure that deleteLater is called for the + // request reply - unfortunately it requires Qt 5.10 which the Android build does not support as of 06/26/18 + + connect(networkReply, &QNetworkReply::finished, callbackParams.callbackReceiver, + [callbackParams, networkReply] { + if (networkReply->error() == QNetworkReply::NoError) { + if (!callbackParams.jsonCallbackMethod.isEmpty()) { + bool invoked = QMetaObject::invokeMethod(callbackParams.callbackReceiver, + qPrintable(callbackParams.jsonCallbackMethod), + Q_ARG(QNetworkReply*, networkReply)); + + if (!invoked) { + QString error = "Could not invoke " + callbackParams.jsonCallbackMethod + " with QNetworkReply* " + + "on callbackReceiver."; + qCWarning(networking) << error; + Q_ASSERT_X(invoked, "AccountManager::passErrorToCallback", qPrintable(error)); + } + } else { + if (VERBOSE_HTTP_REQUEST_DEBUGGING) { + qCDebug(networking) << "Received JSON response from metaverse API that has no matching callback."; + qCDebug(networking) << QJsonDocument::fromJson(networkReply->readAll()); + } + } + } else { + if (!callbackParams.errorCallbackMethod.isEmpty()) { + bool invoked = QMetaObject::invokeMethod(callbackParams.callbackReceiver, + qPrintable(callbackParams.errorCallbackMethod), + Q_ARG(QNetworkReply*, networkReply)); + + if (!invoked) { + QString error = "Could not invoke " + callbackParams.errorCallbackMethod + " with QNetworkReply* " + + "on callbackReceiver."; + qCWarning(networking) << error; + Q_ASSERT_X(invoked, "AccountManager::passErrorToCallback", qPrintable(error)); + } + + } else { + if (VERBOSE_HTTP_REQUEST_DEBUGGING) { + qCDebug(networking) << "Received error response from metaverse API that has no matching callback."; + qCDebug(networking) << "Error" << networkReply->error() << "-" << networkReply->errorString(); + qCDebug(networking) << networkReply->readAll(); + } + } + } + + networkReply->deleteLater(); + }); } - - // if we ended up firing of a request, hook up to it now - connect(networkReply, SIGNAL(finished()), SLOT(processReply())); - } -} - -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); - } - requestReply->deleteLater(); -} - -void AccountManager::passSuccessToCallback(QNetworkReply* requestReply) { - JSONCallbackParameters callbackParams = _pendingCallbackMap.value(requestReply); - - if (callbackParams.jsonCallbackReceiver) { - // invoke the right method on the callback receiver - QMetaObject::invokeMethod(callbackParams.jsonCallbackReceiver, qPrintable(callbackParams.jsonCallbackMethod), - Q_ARG(QNetworkReply&, *requestReply)); - - // remove the related reply-callback group from the map - _pendingCallbackMap.remove(requestReply); - - } else { - if (VERBOSE_HTTP_REQUEST_DEBUGGING) { - qCDebug(networking) << "Received JSON response from metaverse API that has no matching callback."; - qCDebug(networking) << QJsonDocument::fromJson(requestReply->readAll()); - } - - requestReply->deleteLater(); - } -} - -void AccountManager::passErrorToCallback(QNetworkReply* requestReply) { - JSONCallbackParameters callbackParams = _pendingCallbackMap.value(requestReply); - - if (callbackParams.errorCallbackReceiver) { - // invoke the right method on the callback receiver - QMetaObject::invokeMethod(callbackParams.errorCallbackReceiver, qPrintable(callbackParams.errorCallbackMethod), - Q_ARG(QNetworkReply&, *requestReply)); - - // remove the related reply-callback group from the map - _pendingCallbackMap.remove(requestReply); - } else { - if (VERBOSE_HTTP_REQUEST_DEBUGGING) { - qCDebug(networking) << "Received error response from metaverse API that has no matching callback."; - qCDebug(networking) << "Error" << requestReply->error() << "-" << requestReply->errorString(); - qCDebug(networking) << requestReply->readAll(); - } - - requestReply->deleteLater(); } } @@ -817,16 +805,15 @@ void AccountManager::processGeneratedKeypair(QByteArray publicKey, QByteArray pr // setup callback parameters so we know once the keypair upload has succeeded or failed JSONCallbackParameters callbackParameters; - callbackParameters.jsonCallbackReceiver = this; + callbackParameters.callbackReceiver = this; callbackParameters.jsonCallbackMethod = "publicKeyUploadSucceeded"; - callbackParameters.errorCallbackReceiver = this; callbackParameters.errorCallbackMethod = "publicKeyUploadFailed"; sendRequest(uploadPath, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, callbackParameters, QByteArray(), requestMultiPart); } -void AccountManager::publicKeyUploadSucceeded(QNetworkReply& reply) { +void AccountManager::publicKeyUploadSucceeded(QNetworkReply* reply) { qCDebug(networking) << "Uploaded public key to Metaverse API. RSA keypair generation is completed."; // public key upload complete - store the matching private key and persist the account to settings @@ -838,23 +825,17 @@ void AccountManager::publicKeyUploadSucceeded(QNetworkReply& reply) { _isWaitingForKeypairResponse = false; emit newKeypair(); - - // delete the reply object now that we are done with it - reply.deleteLater(); } -void AccountManager::publicKeyUploadFailed(QNetworkReply& reply) { +void AccountManager::publicKeyUploadFailed(QNetworkReply* reply) { // the public key upload has failed - qWarning() << "Public key upload failed from AccountManager" << reply.errorString(); + qWarning() << "Public key upload failed from AccountManager" << reply->errorString(); // we aren't waiting for a response any longer _isWaitingForKeypairResponse = false; // clear our pending private key _pendingPrivateKey.clear(); - - // delete the reply object now that we are done with it - reply.deleteLater(); } void AccountManager::handleKeypairGenerationError() { diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 9068966512..a79b69fe2b 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -28,18 +28,14 @@ class JSONCallbackParameters { public: - JSONCallbackParameters(QObject* jsonCallbackReceiver = nullptr, const QString& jsonCallbackMethod = QString(), - QObject* errorCallbackReceiver = nullptr, const QString& errorCallbackMethod = QString(), - QObject* updateReceiver = nullptr, const QString& updateSlot = QString()); + JSONCallbackParameters(QObject* callbackReceiver = nullptr, const QString& jsonCallbackMethod = QString(), + const QString& errorCallbackMethod = QString()); - bool isEmpty() const { return !jsonCallbackReceiver && !errorCallbackReceiver; } + bool isEmpty() const { return !callbackReceiver; } - QObject* jsonCallbackReceiver; + QObject* callbackReceiver; QString jsonCallbackMethod; - QObject* errorCallbackReceiver; QString errorCallbackMethod; - QObject* updateReciever; - QString updateSlot; }; namespace AccountManagerAuth { @@ -90,7 +86,7 @@ public: DataServerAccountInfo& getAccountInfo() { return _accountInfo; } void setAccountInfo(const DataServerAccountInfo &newAccountInfo); - static QJsonObject dataObjectFromResponse(QNetworkReply& requestReply); + static QJsonObject dataObjectFromResponse(QNetworkReply* requestReply); QUuid getSessionID() const { return _sessionID; } void setSessionID(const QUuid& sessionID); @@ -126,11 +122,10 @@ signals: void newKeypair(); private slots: - void processReply(); void handleKeypairGenerationError(); void processGeneratedKeypair(QByteArray publicKey, QByteArray privateKey); - void publicKeyUploadSucceeded(QNetworkReply& reply); - void publicKeyUploadFailed(QNetworkReply& reply); + void publicKeyUploadSucceeded(QNetworkReply* reply); + void publicKeyUploadFailed(QNetworkReply* reply); void generateNewKeypair(bool isUserKeypair = true, const QUuid& domainID = QUuid()); private: @@ -146,8 +141,6 @@ private: UserAgentGetter _userAgentGetter; QUrl _authURL; - - QMap _pendingCallbackMap; DataServerAccountInfo _accountInfo; bool _isWaitingForTokenRefresh { false }; diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 317be194b8..00e552af89 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -215,9 +215,8 @@ const JSONCallbackParameters& AddressManager::apiCallbackParameters() { static JSONCallbackParameters callbackParams; if (!hasSetupParameters) { - callbackParams.jsonCallbackReceiver = this; + callbackParams.callbackReceiver = this; callbackParams.jsonCallbackMethod = "handleAPIResponse"; - callbackParams.errorCallbackReceiver = this; callbackParams.errorCallbackMethod = "handleAPIError"; } @@ -377,8 +376,8 @@ void AddressManager::handleLookupString(const QString& lookupString, bool fromSu const QString DATA_OBJECT_DOMAIN_KEY = "domain"; -void AddressManager::handleAPIResponse(QNetworkReply& requestReply) { - QJsonObject responseObject = QJsonDocument::fromJson(requestReply.readAll()).object(); +void AddressManager::handleAPIResponse(QNetworkReply* requestReply) { + QJsonObject responseObject = QJsonDocument::fromJson(requestReply->readAll()).object(); QJsonObject dataObject = responseObject["data"].toObject(); // Lookup succeeded, don't keep re-trying it (especially on server restarts) @@ -396,7 +395,7 @@ void AddressManager::handleAPIResponse(QNetworkReply& requestReply) { const char OVERRIDE_PATH_KEY[] = "override_path"; const char LOOKUP_TRIGGER_KEY[] = "lookup_trigger"; -void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const QNetworkReply& reply) { +void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const QNetworkReply* reply) { const QString DATA_OBJECT_PLACE_KEY = "place"; const QString DATA_OBJECT_USER_LOCATION_KEY = "location"; @@ -461,7 +460,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const emit possibleDomainChangeRequiredViaICEForID(iceServerAddress, domainID); } - LookupTrigger trigger = (LookupTrigger) reply.property(LOOKUP_TRIGGER_KEY).toInt(); + LookupTrigger trigger = (LookupTrigger) reply->property(LOOKUP_TRIGGER_KEY).toInt(); // set our current root place id to the ID that came back @@ -495,7 +494,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const } // check if we had a path to override the path returned - QString overridePath = reply.property(OVERRIDE_PATH_KEY).toString(); + QString overridePath = reply->property(OVERRIDE_PATH_KEY).toString(); if (!overridePath.isEmpty() && overridePath != "/") { // make sure we don't re-handle an overriden path if this was a refresh of info from API @@ -543,10 +542,10 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const } } -void AddressManager::handleAPIError(QNetworkReply& errorReply) { - qCDebug(networking) << "AddressManager API error -" << errorReply.error() << "-" << errorReply.errorString(); +void AddressManager::handleAPIError(QNetworkReply* errorReply) { + qCDebug(networking) << "AddressManager API error -" << errorReply->error() << "-" << errorReply->errorString(); - if (errorReply.error() == QNetworkReply::ContentNotFoundError) { + if (errorReply->error() == QNetworkReply::ContentNotFoundError) { // if this is a lookup that has no result, don't keep re-trying it _previousLookup.clear(); @@ -853,17 +852,16 @@ void AddressManager::refreshPreviousLookup() { void AddressManager::copyAddress() { if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "copyAddress"); + QMetaObject::invokeMethod(qApp, "copyToClipboard", Q_ARG(QString, currentShareableAddress().toString())); return; } - // assume that the address is being copied because the user wants a shareable address QGuiApplication::clipboard()->setText(currentShareableAddress().toString()); } void AddressManager::copyPath() { if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "copyPath"); + QMetaObject::invokeMethod(qApp, "copyToClipboard", Q_ARG(QString, currentPath())); return; } @@ -874,14 +872,14 @@ QString AddressManager::getDomainID() const { return DependencyManager::get()->getDomainHandler().getUUID().toString(); } -void AddressManager::handleShareableNameAPIResponse(QNetworkReply& requestReply) { +void AddressManager::handleShareableNameAPIResponse(QNetworkReply* requestReply) { // make sure that this response is for the domain we're currently connected to auto domainID = DependencyManager::get()->getDomainHandler().getUUID(); - if (requestReply.url().toString().contains(uuidStringWithoutCurlyBraces(domainID))) { + if (requestReply->url().toString().contains(uuidStringWithoutCurlyBraces(domainID))) { // check for a name or default name in the API response - QJsonObject responseObject = QJsonDocument::fromJson(requestReply.readAll()).object(); + QJsonObject responseObject = QJsonDocument::fromJson(requestReply->readAll()).object(); QJsonObject domainObject = responseObject["domain"].toObject(); const QString DOMAIN_NAME_KEY = "name"; @@ -917,7 +915,7 @@ void AddressManager::lookupShareableNameForDomainID(const QUuid& domainID) { // no error callback handling // in the case of an error we simply assume there is no default place name - callbackParams.jsonCallbackReceiver = this; + callbackParams.callbackReceiver = this; callbackParams.jsonCallbackMethod = "handleShareableNameAPIResponse"; DependencyManager::get()->sendRequest(GET_DOMAIN_ID.arg(uuidStringWithoutCurlyBraces(domainID)), diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index 38eb7ee670..37b85a9acd 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -417,13 +417,13 @@ signals: void goForwardPossible(bool isPossible); private slots: - void handleAPIResponse(QNetworkReply& requestReply); - void handleAPIError(QNetworkReply& errorReply); + void handleAPIResponse(QNetworkReply* requestReply); + void handleAPIError(QNetworkReply* errorReply); - void handleShareableNameAPIResponse(QNetworkReply& requestReply); + void handleShareableNameAPIResponse(QNetworkReply* requestReply); private: - void goToAddressFromObject(const QVariantMap& addressMap, const QNetworkReply& reply); + void goToAddressFromObject(const QVariantMap& addressMap, const QNetworkReply* reply); // Set host and port, and return `true` if it was changed. bool setHost(const QString& host, LookupTrigger trigger, quint16 port = 0); diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 871dc26899..39c8b5b1a1 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -475,13 +475,16 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer= MAX_SILENT_DOMAIN_SERVER_CHECK_INS) { + if (_checkInPacketsSinceLastReply > MAX_SILENT_DOMAIN_SERVER_CHECK_INS) { // we haven't heard back from DS in MAX_SILENT_DOMAIN_SERVER_CHECK_INS // so emit our signal that says that qCDebug(networking) << "Limit of silent domain checkins reached"; emit limitOfSilentDomainCheckInsReached(); + return true; + } else { + return false; } } diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 4d98391104..a428110db6 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -96,7 +96,7 @@ public: void softReset(); int getCheckInPacketsSinceLastReply() const { return _checkInPacketsSinceLastReply; } - void sentCheckInPacket(); + bool checkInPacketTimeout(); void clearPendingCheckins() { _checkInPacketsSinceLastReply = 0; } /**jsdoc diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp index f946e97bf4..620e60945b 100644 --- a/libraries/networking/src/NLPacket.cpp +++ b/libraries/networking/src/NLPacket.cpp @@ -199,7 +199,9 @@ void NLPacket::readVersion() { } void NLPacket::readSourceID() { - if (!PacketTypeEnum::getNonSourcedPackets().contains(_type)) { + if (PacketTypeEnum::getNonSourcedPackets().contains(_type)) { + _sourceID = NULL_LOCAL_ID; + } else { _sourceID = sourceIDInHeader(*this); } } diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 4920ea97c7..2ce734dd26 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -289,6 +289,12 @@ void NodeList::addSetOfNodeTypesToNodeInterestSet(const NodeSet& setOfNodeTypes) } void NodeList::sendDomainServerCheckIn() { + + if (!_sendDomainServerCheckInEnabled) { + qCDebug(networking) << "Refusing to send a domain-server check in while it is disabled."; + return; + } + if (thread() != QThread::currentThread()) { QMetaObject::invokeMethod(this, "sendDomainServerCheckIn", Qt::QueuedConnection); return; @@ -305,7 +311,8 @@ void NodeList::sendDomainServerCheckIn() { } else if (_domainHandler.getIP().isNull() && _domainHandler.requiresICE()) { qCDebug(networking) << "Waiting for ICE discovered domain-server socket. Will not send domain-server check in."; handleICEConnectionToDomainServer(); - } else if (!_domainHandler.getIP().isNull()) { + // let the domain handler know we are due to send a checkin packet + } else if (!_domainHandler.getIP().isNull() && !_domainHandler.checkInPacketTimeout()) { PacketType domainPacketType = !_domainHandler.isConnected() ? PacketType::DomainConnectRequest : PacketType::DomainListRequest; @@ -419,10 +426,15 @@ void NodeList::sendDomainServerCheckIn() { flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SendDSCheckIn); + // Send duplicate check-ins in the exponentially increasing sequence 1, 1, 2, 4, ... + int outstandingCheckins = _domainHandler.getCheckInPacketsSinceLastReply(); + int checkinCount = outstandingCheckins > 1 ? std::pow(2, outstandingCheckins - 2) : 1; + for (int i = 1; i < checkinCount; ++i) { + auto packetCopy = domainPacket->createCopy(*domainPacket); + sendPacket(std::move(packetCopy), _domainHandler.getSockAddr()); + } sendPacket(std::move(domainPacket), _domainHandler.getSockAddr()); - - // let the domain handler know we sent another check in or connect packet - _domainHandler.sentCheckInPacket(); + } } @@ -549,7 +561,7 @@ void NodeList::handleICEConnectionToDomainServer() { _domainHandler.getICEClientID(), _domainHandler.getPendingDomainID()); } -} +} void NodeList::pingPunchForDomainServer() { // make sure if we're here that we actually still need to ping the domain-server diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index c5cf5e9524..78d3fad696 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -90,6 +90,9 @@ public: bool getRequestsDomainListData() { return _requestsDomainListData; } void setRequestsDomainListData(bool isRequesting); + bool getSendDomainServerCheckInEnabled() { return _sendDomainServerCheckInEnabled; } + void setSendDomainServerCheckInEnabled(bool enabled) { _sendDomainServerCheckInEnabled = enabled; } + void removeFromIgnoreMuteSets(const QUuid& nodeID); virtual bool isDomainServer() const override { return false; } @@ -169,6 +172,8 @@ private: QTimer _keepAlivePingTimer; bool _requestsDomainListData { false }; + bool _sendDomainServerCheckInEnabled { true }; + mutable QReadWriteLock _ignoredSetLock; tbb::concurrent_unordered_set _ignoredNodeIDs; mutable QReadWriteLock _personalMutedSetLock; diff --git a/libraries/networking/src/ResourceManager.cpp b/libraries/networking/src/ResourceManager.cpp index 6df15129a2..553f0d0a61 100644 --- a/libraries/networking/src/ResourceManager.cpp +++ b/libraries/networking/src/ResourceManager.cpp @@ -39,8 +39,13 @@ ResourceManager::ResourceManager(bool atpSupportEnabled) : _atpSupportEnabled(at } ResourceManager::~ResourceManager() { - _thread.terminate(); - _thread.wait(); + if (_thread.isRunning()) { + _thread.quit(); + static const auto MAX_RESOURCE_MANAGER_THREAD_QUITTING_TIME = MSECS_PER_SECOND / 2; + if (!_thread.wait(MAX_RESOURCE_MANAGER_THREAD_QUITTING_TIME)) { + _thread.terminate(); + } + } } void ResourceManager::setUrlPrefixOverride(const QString& prefix, const QString& replacement) { diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp index 7a92d4bad9..a5ee417939 100644 --- a/libraries/networking/src/UserActivityLogger.cpp +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -65,7 +65,7 @@ void UserActivityLogger::logAction(QString action, QJsonObject details, JSONCall // if no callbacks specified, call our owns if (params.isEmpty()) { - params.errorCallbackReceiver = this; + params.callbackReceiver = this; params.errorCallbackMethod = "requestError"; } @@ -75,8 +75,8 @@ void UserActivityLogger::logAction(QString action, QJsonObject details, JSONCall params, NULL, multipart); } -void UserActivityLogger::requestError(QNetworkReply& errorReply) { - qCDebug(networking) << errorReply.error() << "-" << errorReply.errorString(); +void UserActivityLogger::requestError(QNetworkReply* errorReply) { + qCDebug(networking) << errorReply->error() << "-" << errorReply->errorString(); } void UserActivityLogger::launch(QString applicationVersion, bool previousSessionCrashed, int previousSessionRuntime) { diff --git a/libraries/networking/src/UserActivityLogger.h b/libraries/networking/src/UserActivityLogger.h index b44c60eba7..e4b91b1e81 100644 --- a/libraries/networking/src/UserActivityLogger.h +++ b/libraries/networking/src/UserActivityLogger.h @@ -50,7 +50,7 @@ public slots: void wentTo(AddressManager::LookupTrigger trigger, QString destinationType, QString destinationName); private slots: - void requestError(QNetworkReply& errorReply); + void requestError(QNetworkReply* errorReply); private: UserActivityLogger(); diff --git a/libraries/networking/src/udt/Connection.cpp b/libraries/networking/src/udt/Connection.cpp index c1fe6ccd85..0bc86a28ad 100644 --- a/libraries/networking/src/udt/Connection.cpp +++ b/libraries/networking/src/udt/Connection.cpp @@ -239,7 +239,7 @@ void Connection::sync() { sendACK(); } - if (_lossList.getLength() > 0) { + if (_congestionControl->shouldNAK() && _lossList.getLength() > 0) { // check if we need to re-transmit a loss list // we do this if it has been longer than the current nakInterval since we last sent auto now = p_high_resolution_clock::now(); @@ -271,10 +271,13 @@ void Connection::sendACK(bool wasCausedBySyncTimeout) { SequenceNumber nextACKNumber = nextACK(); Q_ASSERT_X(nextACKNumber >= _lastSentACK, "Connection::sendACK", "Sending lower ACK, something is wrong"); - - if (nextACKNumber == _lastSentACK) { - // We already sent this ACK, but check if we should re-send it. - if (nextACKNumber < _lastReceivedAcknowledgedACK) { + + // if our congestion control doesn't want to send an ACK for every packet received + // check if we already sent this ACK + if (_congestionControl->_ackInterval > 1 && nextACKNumber == _lastSentACK) { + + // if we use ACK2s, check if the receiving side already confirmed receipt of this ACK + if (_congestionControl->shouldACK2() && nextACKNumber < _lastReceivedAcknowledgedACK) { // we already got an ACK2 for this ACK we would be sending, don't bother return; } @@ -287,11 +290,11 @@ void Connection::sendACK(bool wasCausedBySyncTimeout) { } } // we have received new packets since the last sent ACK + // or our congestion control dictates that we always send ACKs // update the last sent ACK _lastSentACK = nextACKNumber; - _ackPacket->reset(); // We need to reset it every time. // pack in the ACK sub-sequence number @@ -448,20 +451,22 @@ bool Connection::processReceivedSequenceNumber(SequenceNumber sequenceNumber, in // mark our last receive time as now (to push the potential expiry farther) _lastReceiveTime = p_high_resolution_clock::now(); - - // check if this is a packet pair we should estimate bandwidth from, or just a regular packet - if (((uint32_t) sequenceNumber & 0xF) == 0) { - _receiveWindow.onProbePair1Arrival(); - } else if (((uint32_t) sequenceNumber & 0xF) == 1) { - // only use this packet for bandwidth estimation if we didn't just receive a control packet in its place - if (!_receivedControlProbeTail) { - _receiveWindow.onProbePair2Arrival(); - } else { - // reset our control probe tail marker so the next probe that comes with data can be used - _receivedControlProbeTail = false; + + if (_congestionControl->shouldProbe()) { + // check if this is a packet pair we should estimate bandwidth from, or just a regular packet + if (((uint32_t) sequenceNumber & 0xF) == 0) { + _receiveWindow.onProbePair1Arrival(); + } else if (((uint32_t) sequenceNumber & 0xF) == 1) { + // only use this packet for bandwidth estimation if we didn't just receive a control packet in its place + if (!_receivedControlProbeTail) { + _receiveWindow.onProbePair2Arrival(); + } else { + // reset our control probe tail marker so the next probe that comes with data can be used + _receivedControlProbeTail = false; + } } - } + _receiveWindow.onPacketArrival(); // If this is not the next sequence number, report loss diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index a77cb68bef..13ffcb5120 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -92,6 +92,8 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(PingVersion::IncludeConnectionID); case PacketType::AvatarQuery: return static_cast(AvatarQueryVersion::ConicalFrustums); + case PacketType::AvatarIdentityRequest: + return 22; default: return 21; } diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 62801da35b..6e1aca83e5 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -57,7 +57,7 @@ public: ICEServerQuery, OctreeStats, UNUSED_PACKET_TYPE_1, - UNUSED_PACKET_TYPE_2, + AvatarIdentityRequest, AssignmentClientStatus, NoisyMute, AvatarIdentity, diff --git a/libraries/octree/src/OctreeProcessor.cpp b/libraries/octree/src/OctreeProcessor.cpp index db78e985e6..beaac1198c 100644 --- a/libraries/octree/src/OctreeProcessor.cpp +++ b/libraries/octree/src/OctreeProcessor.cpp @@ -20,12 +20,6 @@ #include "OctreeLogging.h" -OctreeProcessor::OctreeProcessor() : - _tree(NULL), - _managedTree(false) -{ -} - void OctreeProcessor::init() { if (!_tree) { _tree = createTree(); @@ -34,6 +28,9 @@ void OctreeProcessor::init() { } OctreeProcessor::~OctreeProcessor() { + if (_tree) { + _tree->eraseAllOctreeElements(false); + } } void OctreeProcessor::setTree(OctreePointer newTree) { diff --git a/libraries/octree/src/OctreeProcessor.h b/libraries/octree/src/OctreeProcessor.h index 25e280abca..325b33cd15 100644 --- a/libraries/octree/src/OctreeProcessor.h +++ b/libraries/octree/src/OctreeProcessor.h @@ -28,7 +28,6 @@ class OctreeProcessor : public QObject, public QEnableSharedFromThis { Q_OBJECT public: - OctreeProcessor(); virtual ~OctreeProcessor(); virtual char getMyNodeType() const = 0; @@ -61,7 +60,7 @@ protected: virtual OctreePointer createTree() = 0; OctreePointer _tree; - bool _managedTree; + bool _managedTree { false }; SimpleMovingAverage _elementsPerPacket; SimpleMovingAverage _entitiesPerPacket; diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 64eda975cf..cee0e6a1fa 100755 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -262,23 +262,53 @@ void CharacterController::playerStep(btCollisionWorld* collisionWorld, btScalar btVector3 linearDisplacement = clampLength(vel * dt, MAX_DISPLACEMENT); // clamp displacement to prevent tunneling. btVector3 endPos = startPos + linearDisplacement; + // resolve the simple linearDisplacement + _followLinearDisplacement += linearDisplacement; + + // now for the rotational part... btQuaternion startRot = bodyTransform.getRotation(); btQuaternion desiredRot = _followDesiredBodyTransform.getRotation(); - if (desiredRot.dot(startRot) < 0.0f) { - desiredRot = -desiredRot; + + // startRot as default rotation + btQuaternion endRot = startRot; + + // the dot product between two quaternions is equal to +/- cos(angle/2) + // where 'angle' is that of the rotation between them + float qDot = desiredRot.dot(startRot); + + // when the abs() value of the dot product is approximately 1.0 + // then the two rotations are effectively adjacent + const float MIN_DOT_PRODUCT_OF_ADJACENT_QUATERNIONS = 0.99999f; // corresponds to approx 0.5 degrees + if (fabsf(qDot) < MIN_DOT_PRODUCT_OF_ADJACENT_QUATERNIONS) { + if (qDot < 0.0f) { + // the quaternions are actually on opposite hyperhemispheres + // so we move one to agree with the other and negate qDot + desiredRot = -desiredRot; + qDot = -qDot; + } + btQuaternion deltaRot = desiredRot * startRot.inverse(); + + // the axis is the imaginary part, but scaled by sin(angle/2) + btVector3 axis(deltaRot.getX(), deltaRot.getY(), deltaRot.getZ()); + axis /= sqrtf(1.0f - qDot * qDot); + + // compute the angle we will resolve for this dt, but don't overshoot + float angle = 2.0f * acosf(qDot); + if ( dt < _followTimeRemaining) { + angle *= dt / _followTimeRemaining; + } + + // accumulate rotation + deltaRot = btQuaternion(axis, angle); + _followAngularDisplacement = (deltaRot * _followAngularDisplacement).normalize(); + + // in order to accumulate displacement of avatar position, we need to take _shapeLocalOffset into account. + btVector3 shapeLocalOffset = glmToBullet(_shapeLocalOffset); + + endRot = deltaRot * startRot; + btVector3 swingDisplacement = rotateVector(endRot, -shapeLocalOffset) - rotateVector(startRot, -shapeLocalOffset); + _followLinearDisplacement += swingDisplacement; } - btQuaternion deltaRot = desiredRot * startRot.inverse(); - float angularSpeed = deltaRot.getAngle() / _followTimeRemaining; - btQuaternion angularDisplacement = btQuaternion(deltaRot.getAxis(), angularSpeed * dt); - btQuaternion endRot = angularDisplacement * startRot; - - // in order to accumulate displacement of avatar position, we need to take _shapeLocalOffset into account. - btVector3 shapeLocalOffset = glmToBullet(_shapeLocalOffset); - btVector3 swingDisplacement = rotateVector(endRot, -shapeLocalOffset) - rotateVector(startRot, -shapeLocalOffset); - - _followLinearDisplacement = linearDisplacement + swingDisplacement + _followLinearDisplacement; - _followAngularDisplacement = angularDisplacement * _followAngularDisplacement; - _rigidBody->setWorldTransform(btTransform(endRot, endPos)); } _followTime += dt; @@ -378,9 +408,6 @@ void CharacterController::setState(State desiredState, const char* reason) { #else void CharacterController::setState(State desiredState) { #endif - if (!_flyingAllowed && desiredState == State::Hover) { - desiredState = State::InAir; - } if (desiredState != _state) { #ifdef DEBUG_STATE_CHANGE @@ -746,7 +773,7 @@ void CharacterController::updateState() { const float JUMP_SPEED = _scaleFactor * DEFAULT_AVATAR_JUMP_SPEED; if ((velocity.dot(_currentUp) <= (JUMP_SPEED / 2.0f)) && ((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport)) { SET_STATE(State::Ground, "hit ground"); - } else { + } else if (_flyingAllowed) { btVector3 desiredVelocity = _targetVelocity; if (desiredVelocity.length2() < MIN_TARGET_SPEED_SQUARED) { desiredVelocity = btVector3(0.0f, 0.0f, 0.0f); @@ -760,14 +787,17 @@ void CharacterController::updateState() { // Transition to hover if we are above the fall threshold SET_STATE(State::Hover, "above fall threshold"); } + } else if (!rayHasHit && !_hasSupport) { + SET_STATE(State::Hover, "no ground detected"); } break; } case State::Hover: btScalar horizontalSpeed = (velocity - velocity.dot(_currentUp) * _currentUp).length(); bool flyingFast = horizontalSpeed > (MAX_WALKING_SPEED * 0.75f); - - if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) { + if (!_flyingAllowed && rayHasHit) { + SET_STATE(State::InAir, "flying not allowed"); + } else if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) { SET_STATE(State::InAir, "near ground"); } else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) { SET_STATE(State::Ground, "touching ground"); @@ -826,9 +856,6 @@ bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPositio void CharacterController::setFlyingAllowed(bool value) { if (value != _flyingAllowed) { _flyingAllowed = value; - if (!_flyingAllowed && _state == State::Hover) { - SET_STATE(State::InAir, "flying not allowed"); - } } } diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index e7b9f17739..a610a6f2a6 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -234,7 +234,7 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const { return; } assert(entityTreeIsLocked()); - if (_motionType == MOTION_TYPE_KINEMATIC && !_entity->hasAncestorOfType(NestableType::Avatar)) { + if (_motionType == MOTION_TYPE_KINEMATIC) { BT_PROFILE("kinematicIntegration"); // This is physical kinematic motion which steps strictly by the subframe count // of the physics simulation and uses full gravity for acceleration. @@ -327,13 +327,6 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { return true; } - bool parentTransformSuccess; - Transform localToWorld = _entity->getParentTransform(parentTransformSuccess); - Transform worldToLocal; - if (parentTransformSuccess) { - localToWorld.evalInverse(worldToLocal); - } - int numSteps = simulationStep - _lastStep; float dt = (float)(numSteps) * PHYSICS_ENGINE_FIXED_SUBSTEP; @@ -361,6 +354,10 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { return true; } + if (_body->isStaticOrKinematicObject()) { + return false; + } + _lastStep = simulationStep; if (glm::length2(_serverVelocity) > 0.0f) { // the entity-server doesn't know where avatars are, so it doesn't do simple extrapolation for children of @@ -388,6 +385,12 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { // TODO: compensate for _worldOffset offset here // compute position error + bool parentTransformSuccess; + Transform localToWorld = _entity->getParentTransform(parentTransformSuccess); + Transform worldToLocal; + if (parentTransformSuccess) { + localToWorld.evalInverse(worldToLocal); + } btTransform worldTrans = _body->getWorldTransform(); glm::vec3 position = worldToLocal.transform(bulletToGLM(worldTrans.getOrigin())); @@ -407,20 +410,23 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { if (glm::length2(_serverAngularVelocity) > 0.0f) { // compute rotation error - float attenuation = powf(1.0f - _body->getAngularDamping(), dt); - _serverAngularVelocity *= attenuation; + // // Bullet caps the effective rotation velocity inside its rotation integration step, therefore // we must integrate with the same algorithm and timestep in order achieve similar results. - for (int i = 0; i < numSteps; ++i) { - _serverRotation = glm::normalize(computeBulletRotationStep(_serverAngularVelocity, - PHYSICS_ENGINE_FIXED_SUBSTEP) * _serverRotation); + float attenuation = powf(1.0f - _body->getAngularDamping(), PHYSICS_ENGINE_FIXED_SUBSTEP); + _serverAngularVelocity *= attenuation; + glm::quat rotation = computeBulletRotationStep(_serverAngularVelocity, PHYSICS_ENGINE_FIXED_SUBSTEP); + for (int i = 1; i < numSteps; ++i) { + _serverAngularVelocity *= attenuation; + rotation = computeBulletRotationStep(_serverAngularVelocity, PHYSICS_ENGINE_FIXED_SUBSTEP) * rotation; } + _serverRotation = glm::normalize(rotation * _serverRotation); + const float MIN_ROTATION_DOT = 0.99999f; // This corresponds to about 0.5 degrees of rotation + glm::quat actualRotation = worldToLocal.getRotation() * bulletToGLM(worldTrans.getRotation()); + return (fabsf(glm::dot(actualRotation, _serverRotation)) < MIN_ROTATION_DOT); } - const float MIN_ROTATION_DOT = 0.99999f; // This corresponds to about 0.5 degrees of rotation - glm::quat actualRotation = worldToLocal.getRotation() * bulletToGLM(worldTrans.getRotation()); - - return (fabsf(glm::dot(actualRotation, _serverRotation)) < MIN_ROTATION_DOT); + return false; } bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { @@ -834,3 +840,14 @@ void EntityMotionState::clearObjectVelocities() const { } _entity->setAcceleration(glm::vec3(0.0f)); } + +void EntityMotionState::saveKinematicState(btScalar timeStep) { + _body->saveKinematicState(timeStep); + + // This is a WORKAROUND for a quirk in Bullet: due to floating point error slow spinning kinematic objects will + // have a measured angular velocity of zero. This probably isn't a bug that the Bullet team is interested in + // fixing since there is one very simple workaround: use double-precision math for the physics simulation. + // We're not ready migrate to double-precision yet so we explicitly work around it by slamming the RigidBody's + // angular velocity with the value in the entity. + _body->setAngularVelocity(glmToBullet(_entity->getWorldAngularVelocity())); +} diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index c2d4f8d87e..b215537d55 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -97,6 +97,7 @@ public: OwnershipState getOwnershipState() const { return _ownershipState; } void setRegion(uint8_t region); + void saveKinematicState(btScalar timeStep) override; protected: void updateSendVelocities(); diff --git a/libraries/physics/src/ObjectMotionState.cpp b/libraries/physics/src/ObjectMotionState.cpp index 161d6bd636..0e6eb2511d 100644 --- a/libraries/physics/src/ObjectMotionState.cpp +++ b/libraries/physics/src/ObjectMotionState.cpp @@ -347,7 +347,7 @@ void ObjectMotionState::updateLastKinematicStep() { } void ObjectMotionState::updateBodyMassProperties() { - float mass = getMass(); + btScalar mass = getMass(); btVector3 inertia(1.0f, 1.0f, 1.0f); if (mass > 0.0f) { _body->getCollisionShape()->calculateLocalInertia(mass, inertia); @@ -356,3 +356,7 @@ void ObjectMotionState::updateBodyMassProperties() { _body->updateInertiaTensor(); } +void ObjectMotionState::saveKinematicState(btScalar timeStep) { + _body->saveKinematicState(timeStep); +} + diff --git a/libraries/physics/src/ObjectMotionState.h b/libraries/physics/src/ObjectMotionState.h index 4a3b200559..7439c1c38d 100644 --- a/libraries/physics/src/ObjectMotionState.h +++ b/libraries/physics/src/ObjectMotionState.h @@ -165,6 +165,7 @@ public: virtual bool isLocallyOwned() const { return false; } virtual bool isLocallyOwnedOrShouldBe() const { return false; } // aka shouldEmitCollisionEvents() + virtual void saveKinematicState(btScalar timeStep); friend class PhysicsEngine; diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index b990d3612b..5666e75aa1 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -59,7 +59,10 @@ void PhysicalEntitySimulation::addEntityInternal(EntityItemPointer entity) { _entitiesToAddToPhysics.insert(entity); } } else if (canBeKinematic && entity->isMovingRelativeToParent()) { - _simpleKinematicEntities.insert(entity); + SetOfEntities::iterator itr = _simpleKinematicEntities.find(entity); + if (itr == _simpleKinematicEntities.end()) { + _simpleKinematicEntities.insert(entity); + } } } @@ -150,7 +153,10 @@ void PhysicalEntitySimulation::changeEntityInternal(EntityItemPointer entity) { removeOwnershipData(motionState); _entitiesToRemoveFromPhysics.insert(entity); if (canBeKinematic && entity->isMovingRelativeToParent()) { - _simpleKinematicEntities.insert(entity); + SetOfEntities::iterator itr = _simpleKinematicEntities.find(entity); + if (itr == _simpleKinematicEntities.end()) { + _simpleKinematicEntities.insert(entity); + } } } else { _incomingChanges.insert(motionState); @@ -160,11 +166,20 @@ void PhysicalEntitySimulation::changeEntityInternal(EntityItemPointer entity) { // The intent is for this object to be in the PhysicsEngine, but it has no MotionState yet. // Perhaps it's shape has changed and it can now be added? _entitiesToAddToPhysics.insert(entity); - _simpleKinematicEntities.remove(entity); // just in case it's non-physical-kinematic + SetOfEntities::iterator itr = _simpleKinematicEntities.find(entity); + if (itr != _simpleKinematicEntities.end()) { + _simpleKinematicEntities.erase(itr); + } } else if (canBeKinematic && entity->isMovingRelativeToParent()) { - _simpleKinematicEntities.insert(entity); + SetOfEntities::iterator itr = _simpleKinematicEntities.find(entity); + if (itr == _simpleKinematicEntities.end()) { + _simpleKinematicEntities.insert(entity); + } } else { - _simpleKinematicEntities.remove(entity); // just in case it's non-physical-kinematic + SetOfEntities::iterator itr = _simpleKinematicEntities.find(entity); + if (itr != _simpleKinematicEntities.end()) { + _simpleKinematicEntities.erase(itr); + } } } @@ -212,7 +227,6 @@ const VectorOfMotionStates& PhysicalEntitySimulation::getObjectsToRemoveFromPhys assert(motionState); // TODO CLEan this, just a n extra check to avoid the crash that shouldn;t happen if (motionState) { - _entitiesToAddToPhysics.remove(entity); if (entity->isDead() && entity->getElement()) { _deadEntities.insert(entity); @@ -255,7 +269,10 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re // this entity should no longer be on the internal _entitiesToAddToPhysics entityItr = _entitiesToAddToPhysics.erase(entityItr); if (entity->isMovingRelativeToParent()) { - _simpleKinematicEntities.insert(entity); + SetOfEntities::iterator itr = _simpleKinematicEntities.find(entity); + if (itr == _simpleKinematicEntities.end()) { + _simpleKinematicEntities.insert(entity); + } } } else if (entity->isReadyToComputeShape()) { ShapeInfo shapeInfo; @@ -375,19 +392,21 @@ void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionSta } void PhysicalEntitySimulation::addOwnershipBid(EntityMotionState* motionState) { - if (!getEntityTree()->isServerlessMode()) { - motionState->initForBid(); - motionState->sendBid(_entityPacketSender, _physicsEngine->getNumSubsteps()); - _bids.push_back(motionState); - _nextBidExpiry = glm::min(_nextBidExpiry, motionState->getNextBidExpiry()); + if (getEntityTree()->isServerlessMode()) { + return; } + motionState->initForBid(); + motionState->sendBid(_entityPacketSender, _physicsEngine->getNumSubsteps()); + _bids.push_back(motionState); + _nextBidExpiry = glm::min(_nextBidExpiry, motionState->getNextBidExpiry()); } void PhysicalEntitySimulation::addOwnership(EntityMotionState* motionState) { - if (!getEntityTree()->isServerlessMode()) { - motionState->initForOwned(); - _owned.push_back(motionState); + if (getEntityTree()->isServerlessMode()) { + return; } + motionState->initForOwned(); + _owned.push_back(motionState); } void PhysicalEntitySimulation::sendOwnershipBids(uint32_t numSubsteps) { @@ -426,7 +445,9 @@ void PhysicalEntitySimulation::sendOwnershipBids(uint32_t numSubsteps) { } void PhysicalEntitySimulation::sendOwnedUpdates(uint32_t numSubsteps) { - bool serverlessMode = getEntityTree()->isServerlessMode(); + if (getEntityTree()->isServerlessMode()) { + return; + } PROFILE_RANGE_EX(simulation_physics, "Update", 0x00000000, (uint64_t)_owned.size()); uint32_t i = 0; while (i < _owned.size()) { @@ -438,7 +459,7 @@ void PhysicalEntitySimulation::sendOwnedUpdates(uint32_t numSubsteps) { } _owned.remove(i); } else { - if (!serverlessMode && _owned[i]->shouldSendUpdate(numSubsteps)) { + if (_owned[i]->shouldSendUpdate(numSubsteps)) { _owned[i]->sendUpdate(_entityPacketSender, numSubsteps); } ++i; diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp index 07d5ceb9ac..17a52f7cd9 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp @@ -162,7 +162,13 @@ void ThreadSafeDynamicsWorld::saveKinematicState(btScalar timeStep) { for (int i=0;iisKinematicObject() && body->getActivationState() != ISLAND_SLEEPING) { - body->saveKinematicState(timeStep); + if (body->getMotionState()) { + btMotionState* motionState = body->getMotionState(); + ObjectMotionState* objectMotionState = static_cast(motionState); + objectMotionState->saveKinematicState(timeStep); + } else { + body->saveKinematicState(timeStep); + } } } } diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index e9c084e132..94ce16cf00 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -40,9 +40,8 @@ void PluginManager::setInputPluginSettingsPersister(const InputPluginSettingsPer _inputSettingsPersister = persister; } -PluginManager* PluginManager::getInstance() { - static PluginManager _manager; - return &_manager; +PluginManagerPointer PluginManager::getInstance() { + return DependencyManager::get(); } QString getPluginNameFromMetaData(QJsonObject object) { @@ -136,9 +135,6 @@ const LoaderList& getLoadedPlugins() { return loadedPlugins; } -PluginManager::PluginManager() { -} - const CodecPluginList& PluginManager::getCodecPlugins() { static CodecPluginList codecPlugins; static std::once_flag once; diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h index f16ad7d09f..65a4012aed 100644 --- a/libraries/plugins/src/plugins/PluginManager.h +++ b/libraries/plugins/src/plugins/PluginManager.h @@ -9,12 +9,19 @@ #include +#include + #include "Forward.h" -class PluginManager : public QObject { + +class PluginManager; +using PluginManagerPointer = QSharedPointer; + +class PluginManager : public QObject, public Dependency { + SINGLETON_DEPENDENCY + public: - static PluginManager* getInstance(); - PluginManager(); + static PluginManagerPointer getInstance(); const DisplayPluginList& getDisplayPlugins(); const InputPluginList& getInputPlugins(); @@ -39,6 +46,8 @@ public: void setInputPluginSettingsPersister(const InputPluginSettingsPersister& persister); private: + PluginManager() = default; + DisplayPluginProvider _displayPluginProvider { []()->DisplayPluginList { return {}; } }; InputPluginProvider _inputPluginProvider { []()->InputPluginList { return {}; } }; CodecPluginProvider _codecPluginProvider { []()->CodecPluginList { return {}; } }; diff --git a/libraries/qml/src/qml/OffscreenSurface.cpp b/libraries/qml/src/qml/OffscreenSurface.cpp index f0c3dfdffd..ea6f1ce324 100644 --- a/libraries/qml/src/qml/OffscreenSurface.cpp +++ b/libraries/qml/src/qml/OffscreenSurface.cpp @@ -66,7 +66,7 @@ OffscreenSurface::OffscreenSurface() } OffscreenSurface::~OffscreenSurface() { - delete _sharedObject; + _sharedObject->deleteLater(); } bool OffscreenSurface::fetchTexture(TextureAndFence& textureAndFence) { @@ -393,9 +393,6 @@ void OffscreenSurface::finishQmlLoad(QQmlComponent* qmlComponent, _sharedObject->setRootItem(newItem); } - qmlComponent->completeCreate(); - qmlComponent->deleteLater(); - onItemCreated(qmlContext, newItem); if (!rootCreated) { @@ -405,6 +402,8 @@ void OffscreenSurface::finishQmlLoad(QQmlComponent* qmlComponent, // Call this callback after rootitem is set, otherwise VrMenu wont work callback(qmlContext, newItem); } + qmlComponent->completeCreate(); + qmlComponent->deleteLater(); } QQmlContext* OffscreenSurface::contextForUrl(const QUrl& qmlSource, QQuickItem* parent, bool forceNewContext) { diff --git a/libraries/render-utils/src/BloomApply.shared.slh b/libraries/render-utils/src/BloomApply.shared.slh new file mode 100644 index 0000000000..61f5a983b3 --- /dev/null +++ b/libraries/render-utils/src/BloomApply.shared.slh @@ -0,0 +1,16 @@ +// glsl / C++ compatible source as interface for BloomApply +#ifdef __cplusplus +# define BA_VEC3 glm::vec3 +#else +# define BA_VEC3 vec3 +#endif + +struct Parameters +{ + BA_VEC3 _intensities; +}; + + // <@if 1@> + // Trigger Scribe include + // <@endif@> +// diff --git a/libraries/render-utils/src/BloomApply.slf b/libraries/render-utils/src/BloomApply.slf index 961438888e..28415643a0 100644 --- a/libraries/render-utils/src/BloomApply.slf +++ b/libraries/render-utils/src/BloomApply.slf @@ -9,11 +9,15 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@include BloomApply.shared.slh@> uniform sampler2D blurMap0; uniform sampler2D blurMap1; uniform sampler2D blurMap2; -uniform vec3 intensity; + +layout(std140) uniform parametersBuffer { + Parameters parameters; +}; in vec2 varTexCoord0; out vec4 outFragColor; @@ -23,5 +27,5 @@ void main(void) { vec4 blur1 = texture(blurMap1, varTexCoord0); vec4 blur2 = texture(blurMap2, varTexCoord0); - outFragColor = vec4(blur0.rgb*intensity.x + blur1.rgb*intensity.y + blur2.rgb*intensity.z, 1.0f); + outFragColor = vec4(blur0.rgb*parameters._intensities.x + blur1.rgb*parameters._intensities.y + blur2.rgb*parameters._intensities.z, 1.0f); } diff --git a/libraries/render-utils/src/BloomEffect.cpp b/libraries/render-utils/src/BloomEffect.cpp index ee06e17578..0e95655370 100644 --- a/libraries/render-utils/src/BloomEffect.cpp +++ b/libraries/render-utils/src/BloomEffect.cpp @@ -21,13 +21,15 @@ #define BLOOM_BLUR_LEVEL_COUNT 3 -BloomThreshold::BloomThreshold(unsigned int downsamplingFactor) : - _downsamplingFactor(downsamplingFactor) { +BloomThreshold::BloomThreshold(unsigned int downsamplingFactor) { assert(downsamplingFactor > 0); + _parameters.edit()._sampleCount = downsamplingFactor; } void BloomThreshold::configure(const Config& config) { - _threshold = config.threshold; + if (_parameters.get()._threshold != config.threshold) { + _parameters.edit()._threshold = config.threshold; + } } void BloomThreshold::run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) { @@ -43,10 +45,11 @@ void BloomThreshold::run(const render::RenderContextPointer& renderContext, cons auto inputBuffer = inputFrameBuffer->getRenderBuffer(0); auto bufferSize = gpu::Vec2u(inputBuffer->getDimensions()); + const auto downSamplingFactor = _parameters.get()._sampleCount; // Downsample resolution - bufferSize.x /= _downsamplingFactor; - bufferSize.y /= _downsamplingFactor; + bufferSize.x /= downSamplingFactor; + bufferSize.y /= downSamplingFactor; if (!_outputBuffer || _outputBuffer->getSize() != bufferSize) { auto colorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(inputBuffer->getTexelFormat(), bufferSize.x, bufferSize.y, @@ -54,10 +57,12 @@ void BloomThreshold::run(const render::RenderContextPointer& renderContext, cons _outputBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("BloomThreshold")); _outputBuffer->setRenderBuffer(0, colorTexture); + + _parameters.edit()._deltaUV = { 1.0f / bufferSize.x, 1.0f / bufferSize.y }; } static const int COLOR_MAP_SLOT = 0; - static const int THRESHOLD_SLOT = 1; + static const int PARAMETERS_SLOT = 1; if (!_pipeline) { auto vs = gpu::StandardShaderLib::getDrawTransformUnitQuadVS(); @@ -66,7 +71,7 @@ void BloomThreshold::run(const render::RenderContextPointer& renderContext, cons gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding("colorMap", COLOR_MAP_SLOT)); - slotBindings.insert(gpu::Shader::Binding("threshold", THRESHOLD_SLOT)); + slotBindings.insert(gpu::Shader::Binding("parametersBuffer", PARAMETERS_SLOT)); gpu::Shader::makeProgram(*program, slotBindings); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -86,21 +91,26 @@ void BloomThreshold::run(const render::RenderContextPointer& renderContext, cons batch.setFramebuffer(_outputBuffer); batch.setResourceTexture(COLOR_MAP_SLOT, inputBuffer); - batch._glUniform1f(THRESHOLD_SLOT, _threshold); + batch.setUniformBuffer(PARAMETERS_SLOT, _parameters); batch.draw(gpu::TRIANGLE_STRIP, 4); }); outputs = _outputBuffer; } -BloomApply::BloomApply() : _intensities{ 1.0f, 1.0f, 1.0f } { +BloomApply::BloomApply() { } void BloomApply::configure(const Config& config) { - _intensities.x = config.intensity / 3.0f; - _intensities.y = _intensities.x; - _intensities.z = _intensities.x; + const auto newIntensity = config.intensity / 3.0f; + + if (_parameters.get()._intensities.x != newIntensity) { + auto& parameters = _parameters.edit(); + parameters._intensities.x = newIntensity; + parameters._intensities.y = newIntensity; + parameters._intensities.z = newIntensity; + } } void BloomApply::run(const render::RenderContextPointer& renderContext, const Inputs& inputs) { @@ -111,7 +121,7 @@ void BloomApply::run(const render::RenderContextPointer& renderContext, const In static const auto BLUR0_SLOT = 0; static const auto BLUR1_SLOT = 1; static const auto BLUR2_SLOT = 2; - static const auto INTENSITY_SLOT = 3; + static const auto PARAMETERS_SLOT = 0; if (!_pipeline) { auto vs = gpu::StandardShaderLib::getDrawTransformUnitQuadVS(); @@ -122,7 +132,7 @@ void BloomApply::run(const render::RenderContextPointer& renderContext, const In slotBindings.insert(gpu::Shader::Binding("blurMap0", BLUR0_SLOT)); slotBindings.insert(gpu::Shader::Binding("blurMap1", BLUR1_SLOT)); slotBindings.insert(gpu::Shader::Binding("blurMap2", BLUR2_SLOT)); - slotBindings.insert(gpu::Shader::Binding("intensity", INTENSITY_SLOT)); + slotBindings.insert(gpu::Shader::Binding("parametersBuffer", PARAMETERS_SLOT)); gpu::Shader::makeProgram(*program, slotBindings); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); @@ -151,7 +161,7 @@ void BloomApply::run(const render::RenderContextPointer& renderContext, const In batch.setResourceTexture(BLUR0_SLOT, blur0FB->getRenderBuffer(0)); batch.setResourceTexture(BLUR1_SLOT, blur1FB->getRenderBuffer(0)); batch.setResourceTexture(BLUR2_SLOT, blur2FB->getRenderBuffer(0)); - batch._glUniform3f(INTENSITY_SLOT, _intensities.x, _intensities.y, _intensities.z); + batch.setUniformBuffer(PARAMETERS_SLOT, _parameters); batch.draw(gpu::TRIANGLE_STRIP, 4); }); } diff --git a/libraries/render-utils/src/BloomEffect.h b/libraries/render-utils/src/BloomEffect.h index 2ff6bc35a7..04cb4a9474 100644 --- a/libraries/render-utils/src/BloomEffect.h +++ b/libraries/render-utils/src/BloomEffect.h @@ -61,10 +61,11 @@ public: private: +#include "BloomThreshold.shared.slh" + gpu::FramebufferPointer _outputBuffer; gpu::PipelinePointer _pipeline; - float _threshold; - unsigned int _downsamplingFactor; + gpu::StructBuffer _parameters; }; @@ -95,8 +96,10 @@ public: private: +#include "BloomApply.shared.slh" + gpu::PipelinePointer _pipeline; - glm::vec3 _intensities; + gpu::StructBuffer _parameters; }; class BloomDraw { diff --git a/libraries/render-utils/src/BloomThreshold.shared.slh b/libraries/render-utils/src/BloomThreshold.shared.slh new file mode 100644 index 0000000000..8aaf8ec311 --- /dev/null +++ b/libraries/render-utils/src/BloomThreshold.shared.slh @@ -0,0 +1,18 @@ +// glsl / C++ compatible source as interface for BloomThreshold +#ifdef __cplusplus +# define BT_VEC2 glm::vec2 +#else +# define BT_VEC2 vec2 +#endif + +struct Parameters +{ + BT_VEC2 _deltaUV; + float _threshold; + int _sampleCount; +}; + + // <@if 1@> + // Trigger Scribe include + // <@endif@> +// diff --git a/libraries/render-utils/src/BloomThreshold.slf b/libraries/render-utils/src/BloomThreshold.slf index e4b96618df..6eb75fba6e 100644 --- a/libraries/render-utils/src/BloomThreshold.slf +++ b/libraries/render-utils/src/BloomThreshold.slf @@ -9,37 +9,35 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@include BloomThreshold.shared.slh@> uniform sampler2D colorMap; -uniform float threshold; +layout(std140) uniform parametersBuffer { + Parameters parameters; +}; in vec2 varTexCoord0; out vec4 outFragColor; -#define DOWNSAMPLING_FACTOR 4 -#define SAMPLE_COUNT (DOWNSAMPLING_FACTOR/2) - void main(void) { - vec2 deltaX = dFdx(varTexCoord0) / SAMPLE_COUNT; - vec2 deltaY = dFdy(varTexCoord0) / SAMPLE_COUNT; vec2 startUv = varTexCoord0; vec4 maskedColor = vec4(0,0,0,0); - for (int y=0 ; y= 0; --i) {" " float depth = i / 255.0;" - " if (texture(shadowMap, vec3(uv, depth)) > 0.5) {" + " if (texture(shadowMaps, vec4(uv, parameters._shadowCascadeIndex, depth)) > 0.5) {" " return vec4(vec3(depth), 1.0);" " }" " }" @@ -323,7 +323,7 @@ std::string DebugDeferredBuffer::getShaderSourceCode(Mode mode, std::string cust case ShadowCascade1Mode: case ShadowCascade2Mode: case ShadowCascade3Mode: - return DEFAULT_SHADOW_SHADER; + return DEFAULT_SHADOW_DEPTH_SHADER; case ShadowCascadeIndicesMode: return DEFAULT_SHADOW_CASCADE_SHADER; case LinearDepthMode: @@ -396,6 +396,7 @@ const gpu::PipelinePointer& DebugDeferredBuffer::getPipeline(Mode mode, std::str slotBindings.insert(gpu::Shader::Binding("cameraCorrectionBuffer", CameraCorrection)); slotBindings.insert(gpu::Shader::Binding("deferredFrameTransformBuffer", DeferredFrameTransform)); slotBindings.insert(gpu::Shader::Binding("shadowTransformBuffer", ShadowTransform)); + slotBindings.insert(gpu::Shader::Binding("parametersBuffer", DebugParametersBuffer)); slotBindings.insert(gpu::Shader::Binding("albedoMap", Albedo)); slotBindings.insert(gpu::Shader::Binding("normalMap", Normal)); @@ -403,7 +404,7 @@ const gpu::PipelinePointer& DebugDeferredBuffer::getPipeline(Mode mode, std::str slotBindings.insert(gpu::Shader::Binding("depthMap", Depth)); slotBindings.insert(gpu::Shader::Binding("obscuranceMap", AmbientOcclusion)); slotBindings.insert(gpu::Shader::Binding("lightingMap", Lighting)); - slotBindings.insert(gpu::Shader::Binding("shadowMap", Shadow)); + slotBindings.insert(gpu::Shader::Binding("shadowMaps", Shadow)); slotBindings.insert(gpu::Shader::Binding("linearDepthMap", LinearDepth)); slotBindings.insert(gpu::Shader::Binding("halfLinearDepthMap", HalfLinearDepth)); slotBindings.insert(gpu::Shader::Binding("halfNormalMap", HalfNormal)); @@ -432,8 +433,11 @@ const gpu::PipelinePointer& DebugDeferredBuffer::getPipeline(Mode mode, std::str } void DebugDeferredBuffer::configure(const Config& config) { + auto& parameters = _parameters.edit(); + _mode = (Mode)config.mode; _size = config.size; + parameters._shadowCascadeIndex = glm::clamp(_mode - Mode::ShadowCascade0Mode, 0, (int)SHADOW_CASCADE_MAX_COUNT - 1); } void DebugDeferredBuffer::run(const RenderContextPointer& renderContext, const Inputs& inputs) { @@ -483,14 +487,15 @@ void DebugDeferredBuffer::run(const RenderContextPointer& renderContext, const I batch.setResourceTexture(Velocity, velocityFramebuffer->getVelocityTexture()); } + batch.setUniformBuffer(DebugParametersBuffer, _parameters); + auto lightStage = renderContext->_scene->getStage(); assert(lightStage); assert(lightStage->getNumLights() > 0); auto lightAndShadow = lightStage->getCurrentKeyLightAndShadow(); const auto& globalShadow = lightAndShadow.second; if (globalShadow) { - const auto cascadeIndex = glm::clamp(_mode - Mode::ShadowCascade0Mode, 0, (int)globalShadow->getCascadeCount() - 1); - batch.setResourceTexture(Shadow, globalShadow->getCascade(cascadeIndex).map); + batch.setResourceTexture(Shadow, globalShadow->map); batch.setUniformBuffer(ShadowTransform, globalShadow->getBuffer()); batch.setUniformBuffer(DeferredFrameTransform, frameTransform->getFrameTransformBuffer()); } diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index 5384a77b76..9daa8fd530 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -30,7 +30,7 @@ public: DebugDeferredBufferConfig() : render::Job::Config(false) {} void setMode(int newMode); - + int mode{ 0 }; glm::vec4 size{ 0.0f, -1.0f, 1.0f, 1.0f }; signals: @@ -39,20 +39,26 @@ signals: class DebugDeferredBuffer { public: - using Inputs = render::VaryingSet6; + using Inputs = render::VaryingSet6; using Config = DebugDeferredBufferConfig; using JobModel = render::Job::ModelI; - + DebugDeferredBuffer(); ~DebugDeferredBuffer(); void configure(const Config& config); void run(const render::RenderContextPointer& renderContext, const Inputs& inputs); - + protected: friend class DebugDeferredBufferConfig; - enum Mode : uint8_t { + enum Mode : uint8_t + { // Use Mode suffix to avoid collisions Off = 0, DepthMode, @@ -83,7 +89,7 @@ protected: AmbientOcclusionMode, AmbientOcclusionBlurredMode, VelocityMode, - CustomMode, // Needs to stay last + CustomMode, // Needs to stay last NumModes, }; @@ -92,20 +98,25 @@ private: Mode _mode{ Off }; glm::vec4 _size; +#include "debug_deferred_buffer_shared.slh" + + using ParametersBuffer = gpu::StructBuffer; + struct CustomPipeline { gpu::PipelinePointer pipeline; mutable QFileInfo info; }; using StandardPipelines = std::array; using CustomPipelines = std::unordered_map; - + bool pipelineNeedsUpdate(Mode mode, std::string customFile = std::string()) const; const gpu::PipelinePointer& getPipeline(Mode mode, std::string customFile = std::string()); std::string getShaderSourceCode(Mode mode, std::string customFile = std::string()); - + + ParametersBuffer _parameters; StandardPipelines _pipelines; CustomPipelines _customPipelines; - int _geometryId { 0 }; + int _geometryId{ 0 }; }; -#endif // hifi_DebugDeferredBuffer_h \ No newline at end of file +#endif // hifi_DebugDeferredBuffer_h \ No newline at end of file diff --git a/libraries/render-utils/src/DeferredGlobalLight.slh b/libraries/render-utils/src/DeferredGlobalLight.slh index c4894200a3..03ec18c321 100644 --- a/libraries/render-utils/src/DeferredGlobalLight.slh +++ b/libraries/render-utils/src/DeferredGlobalLight.slh @@ -198,7 +198,7 @@ vec3 evalGlobalLightingAlphaBlended(mat4 invViewMat, float shadowAttenuation, fl vec3 directionalSpecular; evalLightingDirectional(directionalDiffuse, directionalSpecular, lightDirection, lightIrradiance, surfaceWS, metallic, fresnel, albedo, shadowAttenuation); color += directionalDiffuse; - color += (ambientSpecular + directionalSpecular) / opacity; + color += evalSpecularWithOpacity(ambientSpecular + directionalSpecular, opacity); return color; } @@ -231,19 +231,18 @@ vec3 evalGlobalLightingAlphaBlendedWithHaze( vec3 directionalSpecular; evalLightingDirectional(directionalDiffuse, directionalSpecular, lightDirection, lightIrradiance, surfaceWS, metallic, fresnel, albedo, shadowAttenuation); color += directionalDiffuse; - color += (ambientSpecular + directionalSpecular) / opacity; + color += evalSpecularWithOpacity(ambientSpecular + directionalSpecular, opacity); // Haze if ((isHazeEnabled() > 0.0) && (hazeParams.hazeMode & HAZE_MODE_IS_ACTIVE) == HAZE_MODE_IS_ACTIVE) { - vec4 colorV4 = computeHazeColor( - vec4(color, 1.0), // fragment original color + vec4 hazeColor = computeHazeColor( positionES, // fragment position in eye coordinates fragPositionWS, // fragment position in world coordinates invViewMat[3].xyz, // eye position in world coordinates lightDirection // keylight direction vector in world coordinates ); - color = colorV4.rgb; + color = mix(color.rgb, hazeColor.rgb, hazeColor.a); } return color; @@ -269,19 +268,18 @@ vec3 evalGlobalLightingAlphaBlendedWithHaze( evalLightingDirectional(directionalDiffuse, directionalSpecular, lightDirection, lightIrradiance, surface, metallic, fresnel, albedo, shadowAttenuation); color += ambientDiffuse + directionalDiffuse; - color += (ambientSpecular + directionalSpecular) / opacity; + color += evalSpecularWithOpacity(ambientSpecular + directionalSpecular, opacity); // Haze if ((isHazeEnabled() > 0.0) && (hazeParams.hazeMode & HAZE_MODE_IS_ACTIVE) == HAZE_MODE_IS_ACTIVE) { - vec4 colorV4 = computeHazeColor( - vec4(color, 1.0), // fragment original color + vec4 hazeColor = computeHazeColor( positionES, // fragment position in eye coordinates positionWS, // fragment position in world coordinates invViewMat[3].xyz, // eye position in world coordinates lightDirection // keylight direction vector ); - color = colorV4.rgb; + color = mix(color.rgb, hazeColor.rgb, hazeColor.a); } return color; diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 9223e0fa03..62d8dffe3a 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -68,7 +68,7 @@ enum DeferredShader_MapSlot { SCATTERING_SPECULAR_UNIT = 9, SKYBOX_MAP_UNIT = render::ShapePipeline::Slot::LIGHT_AMBIENT_MAP, // unit = 10 SHADOW_MAP_UNIT = 11, - nextAvailableUnit = SHADOW_MAP_UNIT + SHADOW_CASCADE_MAX_COUNT + nextAvailableUnit = SHADOW_MAP_UNIT }; enum DeferredShader_BufferSlot { DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT = 0, @@ -393,34 +393,42 @@ graphics::MeshPointer DeferredLightingEffect::getSpotLightMesh() { return _spotLightMesh; } -void PreparePrimaryFramebuffer::run(const RenderContextPointer& renderContext, gpu::FramebufferPointer& primaryFramebuffer) { +gpu::FramebufferPointer PreparePrimaryFramebuffer::createFramebuffer(const char* name, const glm::uvec2& frameSize) { + gpu::FramebufferPointer framebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(name)); + auto colorFormat = gpu::Element::COLOR_SRGBA_32; + + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR); + auto primaryColorTexture = gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); + + framebuffer->setRenderBuffer(0, primaryColorTexture); + + auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format + auto primaryDepthTexture = gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); + + framebuffer->setDepthStencilBuffer(primaryDepthTexture, depthFormat); + + return framebuffer; +} + +void PreparePrimaryFramebuffer::configure(const Config& config) { + _resolutionScale = config.resolutionScale; +} + +void PreparePrimaryFramebuffer::run(const RenderContextPointer& renderContext, Output& primaryFramebuffer) { glm::uvec2 frameSize(renderContext->args->_viewport.z, renderContext->args->_viewport.w); + glm::uvec2 scaledFrameSize(glm::vec2(frameSize) * _resolutionScale); // Resizing framebuffers instead of re-building them seems to cause issues with threaded // rendering - if (_primaryFramebuffer && _primaryFramebuffer->getSize() != frameSize) { - _primaryFramebuffer.reset(); + if (!_primaryFramebuffer || _primaryFramebuffer->getSize() != scaledFrameSize) { + _primaryFramebuffer = createFramebuffer("deferredPrimary", scaledFrameSize); } - if (!_primaryFramebuffer) { - _primaryFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("deferredPrimary")); - auto colorFormat = gpu::Element::COLOR_SRGBA_32; - - auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - auto primaryColorTexture = gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); - - - _primaryFramebuffer->setRenderBuffer(0, primaryColorTexture); - - - auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - auto primaryDepthTexture = gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); - - _primaryFramebuffer->setDepthStencilBuffer(primaryDepthTexture, depthFormat); - } - - primaryFramebuffer = _primaryFramebuffer; + + // Set viewport for the rest of the scaled passes + renderContext->args->_viewport.z = scaledFrameSize.x; + renderContext->args->_viewport.w = scaledFrameSize.y; } void PrepareDeferred::run(const RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) { @@ -534,9 +542,7 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext, // Bind the shadow buffers if (globalShadow) { - for (unsigned int i = 0; i < globalShadow->getCascadeCount(); i++) { - batch.setResourceTexture(SHADOW_MAP_UNIT+i, globalShadow->getCascade(i).map); - } + batch.setResourceTexture(SHADOW_MAP_UNIT, globalShadow->map); } auto program = deferredLightingEffect->_directionalSkyboxLight; diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h index 9b55083ad7..5da2eb22f7 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.h +++ b/libraries/render-utils/src/DeferredLightingEffect.h @@ -93,13 +93,34 @@ private: friend class RenderDeferredCleanup; }; +class PreparePrimaryFramebufferConfig : public render::Job::Config { + Q_OBJECT + Q_PROPERTY(float resolutionScale MEMBER resolutionScale NOTIFY dirty) +public: + + float resolutionScale{ 1.0f }; + +signals: + void dirty(); +}; + class PreparePrimaryFramebuffer { public: - using JobModel = render::Job::ModelO; - void run(const render::RenderContextPointer& renderContext, gpu::FramebufferPointer& primaryFramebuffer); + using Output = gpu::FramebufferPointer; + using Config = PreparePrimaryFramebufferConfig; + using JobModel = render::Job::ModelO; + + PreparePrimaryFramebuffer(float resolutionScale = 1.0f) : _resolutionScale{resolutionScale} {} + void configure(const Config& config); + void run(const render::RenderContextPointer& renderContext, Output& primaryFramebuffer); gpu::FramebufferPointer _primaryFramebuffer; + float _resolutionScale{ 1.0f }; + +private: + + static gpu::FramebufferPointer createFramebuffer(const char* name, const glm::uvec2& size); }; class PrepareDeferred { diff --git a/libraries/render-utils/src/DrawHaze.cpp b/libraries/render-utils/src/DrawHaze.cpp index e6337d7099..94bac4e3ac 100644 --- a/libraries/render-utils/src/DrawHaze.cpp +++ b/libraries/render-utils/src/DrawHaze.cpp @@ -107,11 +107,11 @@ void MakeHaze::run(const render::RenderContextPointer& renderContext, graphics:: haze = _haze; } +// Buffer slots const int HazeEffect_ParamsSlot = 0; const int HazeEffect_TransformBufferSlot = 1; -const int HazeEffect_ColorMapSlot = 2; -const int HazeEffect_LinearDepthMapSlot = 3; -const int HazeEffect_LightingMapSlot = 4; +// Texture slots +const int HazeEffect_LinearDepthMapSlot = 0; void DrawHaze::configure(const Config& config) { } @@ -122,11 +122,10 @@ void DrawHaze::run(const render::RenderContextPointer& renderContext, const Inpu return; } - const auto inputBuffer = inputs.get1()->getRenderBuffer(0); + const auto outputBuffer = inputs.get1(); const auto framebuffer = inputs.get2(); const auto transformBuffer = inputs.get3(); - - auto outputBuffer = inputs.get4(); + const auto lightingModel = inputs.get4(); auto depthBuffer = framebuffer->getLinearDepthTexture(); @@ -139,6 +138,10 @@ void DrawHaze::run(const render::RenderContextPointer& renderContext, const Inpu gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + // Mask out haze on the tablet PrepareStencil::testMask(*state); @@ -148,15 +151,15 @@ void DrawHaze::run(const render::RenderContextPointer& renderContext, const Inpu gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("hazeBuffer"), HazeEffect_ParamsSlot)); slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), HazeEffect_TransformBufferSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("colorMap"), HazeEffect_ColorMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("lightingModelBuffer"), render::ShapePipeline::Slot::LIGHTING_MODEL)); slotBindings.insert(gpu::Shader::Binding(std::string("linearDepthMap"), HazeEffect_LinearDepthMapSlot)); - slotBindings.insert(gpu::Shader::Binding(std::string("keyLightBuffer"), HazeEffect_LightingMapSlot)); + slotBindings.insert(gpu::Shader::Binding(std::string("keyLightBuffer"), render::ShapePipeline::Slot::KEY_LIGHT)); gpu::Shader::makeProgram(*program, slotBindings); }); }); } - auto sourceFramebufferSize = glm::ivec2(inputBuffer->getDimensions()); + auto outputFramebufferSize = glm::ivec2(outputBuffer->getSize()); gpu::doInBatch("DrawHaze::run", args->_context, [&](gpu::Batch& batch) { batch.enableStereo(false); @@ -165,7 +168,7 @@ void DrawHaze::run(const render::RenderContextPointer& renderContext, const Inpu batch.setViewportTransform(args->_viewport); batch.setProjectionTransform(glm::mat4()); batch.resetViewTransform(); - batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(sourceFramebufferSize, args->_viewport)); + batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(outputFramebufferSize, args->_viewport)); batch.setPipeline(_hazePipeline); @@ -181,17 +184,17 @@ void DrawHaze::run(const render::RenderContextPointer& renderContext, const Inpu } batch.setUniformBuffer(HazeEffect_TransformBufferSlot, transformBuffer->getFrameTransformBuffer()); + batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); auto lightStage = args->_scene->getStage(); if (lightStage) { graphics::LightPointer keyLight; keyLight = lightStage->getCurrentKeyLight(); if (keyLight) { - batch.setUniformBuffer(HazeEffect_LightingMapSlot, keyLight->getLightSchemaBuffer()); + batch.setUniformBuffer(render::ShapePipeline::Slot::KEY_LIGHT, keyLight->getLightSchemaBuffer()); } } - batch.setResourceTexture(HazeEffect_ColorMapSlot, inputBuffer); batch.setResourceTexture(HazeEffect_LinearDepthMapSlot, depthBuffer); batch.draw(gpu::TRIANGLE_STRIP, 4); diff --git a/libraries/render-utils/src/DrawHaze.h b/libraries/render-utils/src/DrawHaze.h index e7d4e15d77..e30ce26dd4 100644 --- a/libraries/render-utils/src/DrawHaze.h +++ b/libraries/render-utils/src/DrawHaze.h @@ -22,6 +22,7 @@ #include #include "SurfaceGeometryPass.h" +#include "LightingModel.h" using LinearDepthFramebufferPointer = std::shared_ptr; @@ -159,7 +160,7 @@ public: class DrawHaze { public: - using Inputs = render::VaryingSet5; + using Inputs = render::VaryingSet5; using Config = HazeConfig; using JobModel = render::Job::ModelI; diff --git a/libraries/render-utils/src/Fade.slh b/libraries/render-utils/src/Fade.slh index a06c8c869e..a4e8fdf1f4 100644 --- a/libraries/render-utils/src/Fade.slh +++ b/libraries/render-utils/src/Fade.slh @@ -15,19 +15,20 @@ #define CATEGORY_COUNT 5 <@include Fade_shared.slh@> +<@include FadeObjectParams.shared.slh@> layout(std140) uniform fadeParametersBuffer { FadeParameters fadeParameters[CATEGORY_COUNT]; }; uniform sampler2D fadeMaskMap; -struct FadeObjectParams { - int category; - float threshold; - vec3 noiseOffset; - vec3 baseOffset; - vec3 baseInvSize; -}; +vec3 getNoiseInverseSize(int category) { + return fadeParameters[category]._noiseInvSizeAndLevel.xyz; +} + +float getNoiseLevel(int category) { + return fadeParameters[category]._noiseInvSizeAndLevel.w; +} vec2 hash2D(vec3 position) { return position.xy* vec2(0.1677, 0.221765) + position.z*0.561; @@ -40,7 +41,7 @@ float noise3D(vec3 position) { float evalFadeNoiseGradient(FadeObjectParams params, vec3 position) { // Do tri-linear interpolation - vec3 noisePosition = position * fadeParameters[params.category]._noiseInvSizeAndLevel.xyz + params.noiseOffset; + vec3 noisePosition = position * getNoiseInverseSize(params.category) + params.noiseOffset.xyz; vec3 noisePositionFloored = floor(noisePosition); vec3 noisePositionFraction = fract(noisePosition); @@ -61,11 +62,11 @@ float evalFadeNoiseGradient(FadeObjectParams params, vec3 position) { float noise = mix(maskY.x, maskY.y, noisePositionFraction.y); noise -= 0.5; // Center on value 0 - return noise * fadeParameters[params.category]._noiseInvSizeAndLevel.w; + return noise * getNoiseLevel(params.category); } float evalFadeBaseGradient(FadeObjectParams params, vec3 position) { - float gradient = length((position - params.baseOffset) * params.baseInvSize.xyz); + float gradient = length((position - params.baseOffset.xyz) * params.baseInvSize.xyz); gradient = gradient-0.5; // Center on value 0.5 gradient *= fadeParameters[params.category]._baseLevel; return gradient; @@ -112,20 +113,14 @@ void applyFade(FadeObjectParams params, vec3 position, out vec3 emissive) { <@func declareFadeFragmentUniform()@> -uniform int fadeCategory; -uniform vec3 fadeNoiseOffset; -uniform vec3 fadeBaseOffset; -uniform vec3 fadeBaseInvSize; -uniform float fadeThreshold; +layout(std140) uniform fadeObjectParametersBuffer { + FadeObjectParams fadeObjectParams; +}; <@endfunc@> <@func fetchFadeObjectParams(fadeParams)@> - <$fadeParams$>.category = fadeCategory; - <$fadeParams$>.threshold = fadeThreshold; - <$fadeParams$>.noiseOffset = fadeNoiseOffset; - <$fadeParams$>.baseOffset = fadeBaseOffset; - <$fadeParams$>.baseInvSize = fadeBaseInvSize; + <$fadeParams$> = fadeObjectParams; <@endfunc@> <@func declareFadeFragmentVertexInput()@> @@ -139,9 +134,9 @@ in vec4 _fadeData3; <@func fetchFadeObjectParamsInstanced(fadeParams)@> <$fadeParams$>.category = int(_fadeData1.w); <$fadeParams$>.threshold = _fadeData2.w; - <$fadeParams$>.noiseOffset = _fadeData1.xyz; - <$fadeParams$>.baseOffset = _fadeData2.xyz; - <$fadeParams$>.baseInvSize = _fadeData3.xyz; + <$fadeParams$>.noiseOffset = _fadeData1; + <$fadeParams$>.baseOffset = _fadeData2; + <$fadeParams$>.baseInvSize = _fadeData3; <@endfunc@> <@func declareFadeFragment()@> diff --git a/libraries/render-utils/src/FadeEffect.cpp b/libraries/render-utils/src/FadeEffect.cpp index c09aa99988..12531d4c9d 100644 --- a/libraries/render-utils/src/FadeEffect.cpp +++ b/libraries/render-utils/src/FadeEffect.cpp @@ -13,6 +13,8 @@ #include "render/TransitionStage.h" +#include "FadeObjectParams.shared.slh" + #include FadeEffect::FadeEffect() { @@ -31,15 +33,8 @@ void FadeEffect::build(render::Task::TaskConcept& task, const task::Varying& edi render::ShapePipeline::BatchSetter FadeEffect::getBatchSetter() const { return [this](const render::ShapePipeline& shapePipeline, gpu::Batch& batch, render::Args*) { - auto program = shapePipeline.pipeline->getProgram(); - auto maskMapLocation = program->getTextures().findLocation("fadeMaskMap"); - auto bufferLocation = program->getUniformBuffers().findLocation("fadeParametersBuffer"); - if (maskMapLocation != -1) { - batch.setResourceTexture(maskMapLocation, _maskMap); - } - if (bufferLocation != -1) { - batch.setUniformBuffer(bufferLocation, _configurations); - } + batch.setResourceTexture(render::ShapePipeline::Slot::FADE_MASK, _maskMap); + batch.setUniformBuffer(render::ShapePipeline::Slot::FADE_PARAMETERS, _configurations); }; } @@ -50,23 +45,29 @@ render::ShapePipeline::ItemSetter FadeEffect::getItemUniformSetter() const { auto batch = args->_batch; auto transitionStage = scene->getStage(render::TransitionStage::getName()); auto& transitionState = transitionStage->getTransition(item.getTransitionId()); - auto program = shapePipeline.pipeline->getProgram(); - auto& uniforms = program->getUniforms(); - auto fadeNoiseOffsetLocation = uniforms.findLocation("fadeNoiseOffset"); - auto fadeBaseOffsetLocation = uniforms.findLocation("fadeBaseOffset"); - auto fadeBaseInvSizeLocation = uniforms.findLocation("fadeBaseInvSize"); - auto fadeThresholdLocation = uniforms.findLocation("fadeThreshold"); - auto fadeCategoryLocation = uniforms.findLocation("fadeCategory"); - if (fadeNoiseOffsetLocation >= 0 || fadeBaseInvSizeLocation >= 0 || fadeBaseOffsetLocation >= 0 || fadeThresholdLocation >= 0 || fadeCategoryLocation >= 0) { - const auto fadeCategory = FadeJob::transitionToCategory[transitionState.eventType]; - - batch->_glUniform1i(fadeCategoryLocation, fadeCategory); - batch->_glUniform1f(fadeThresholdLocation, transitionState.threshold); - batch->_glUniform3f(fadeNoiseOffsetLocation, transitionState.noiseOffset.x, transitionState.noiseOffset.y, transitionState.noiseOffset.z); - batch->_glUniform3f(fadeBaseOffsetLocation, transitionState.baseOffset.x, transitionState.baseOffset.y, transitionState.baseOffset.z); - batch->_glUniform3f(fadeBaseInvSizeLocation, transitionState.baseInvSize.x, transitionState.baseInvSize.y, transitionState.baseInvSize.z); + if (transitionState.paramsBuffer._size != sizeof(gpu::StructBuffer)) { + static_assert(sizeof(transitionState.paramsBuffer) == sizeof(gpu::StructBuffer), "Assuming gpu::StructBuffer is a helper class for gpu::BufferView"); + transitionState.paramsBuffer = gpu::StructBuffer(); } + + const auto fadeCategory = FadeJob::transitionToCategory[transitionState.eventType]; + auto& paramsConst = static_cast&>(transitionState.paramsBuffer).get(); + + if (paramsConst.category != fadeCategory + || paramsConst.threshold != transitionState.threshold + || glm::vec3(paramsConst.baseOffset) != transitionState.baseOffset + || glm::vec3(paramsConst.noiseOffset) != transitionState.noiseOffset + || glm::vec3(paramsConst.baseInvSize) != transitionState.baseInvSize) { + auto& params = static_cast&>(transitionState.paramsBuffer).edit(); + + params.category = fadeCategory; + params.threshold = transitionState.threshold; + params.baseInvSize = glm::vec4(transitionState.baseInvSize, 0.0f); + params.noiseOffset = glm::vec4(transitionState.noiseOffset, 0.0f); + params.baseOffset = glm::vec4(transitionState.baseOffset, 0.0f); + } + batch->setUniformBuffer(render::ShapePipeline::Slot::FADE_OBJECT_PARAMETERS, transitionState.paramsBuffer); } }; } diff --git a/libraries/render-utils/src/FadeObjectParams.shared.slh b/libraries/render-utils/src/FadeObjectParams.shared.slh new file mode 100644 index 0000000000..e97acaf0b0 --- /dev/null +++ b/libraries/render-utils/src/FadeObjectParams.shared.slh @@ -0,0 +1,25 @@ +// glsl / C++ compatible source as interface for FadeObjectParams +#ifdef __cplusplus +# define FOP_VEC4 glm::vec4 +# define FOP_VEC2 glm::vec2 +# define FOP_FLOAT32 glm::float32 +# define FOP_INT32 glm::int32 +#else +# define FOP_VEC4 vec4 +# define FOP_VEC2 vec2 +# define FOP_FLOAT32 float +# define FOP_INT32 int +#endif + +struct FadeObjectParams { + FOP_VEC4 noiseOffset; + FOP_VEC4 baseOffset; + FOP_VEC4 baseInvSize; + FOP_INT32 category; + FOP_FLOAT32 threshold; +}; + + // <@if 1@> + // Trigger Scribe include + // <@endif@> +// diff --git a/libraries/render-utils/src/ForwardGlobalLight.slh b/libraries/render-utils/src/ForwardGlobalLight.slh index 84999f347b..cf5f070c55 100644 --- a/libraries/render-utils/src/ForwardGlobalLight.slh +++ b/libraries/render-utils/src/ForwardGlobalLight.slh @@ -197,7 +197,7 @@ vec3 evalGlobalLightingAlphaBlended(mat4 invViewMat, float shadowAttenuation, fl vec3 directionalSpecular; evalLightingDirectional(directionalDiffuse, directionalSpecular, lightDirection, lightIrradiance, surfaceWS, metallic, fresnel, albedo, shadowAttenuation); color += directionalDiffuse; - color += (ambientSpecular + directionalSpecular) / opacity; + color += evalSpecularWithOpacity(ambientSpecular + directionalSpecular, opacity); return color; } @@ -223,20 +223,19 @@ vec3 evalGlobalLightingAlphaBlendedWithHaze( vec3 directionalSpecular; evalLightingDirectional(directionalDiffuse, directionalSpecular, lightDirection, lightIrradiance, surfaceWS, metallic, fresnel, albedo, shadowAttenuation); color += directionalDiffuse; - color += (ambientSpecular + directionalSpecular) / opacity; + color += evalSpecularWithOpacity(ambientSpecular + directionalSpecular, opacity); // Haze // FIXME - temporarily removed until we support it for forward... /* if ((hazeParams.hazeMode & HAZE_MODE_IS_ACTIVE) == HAZE_MODE_IS_ACTIVE) { - vec4 colorV4 = computeHazeColor( - vec4(color, 1.0), // fragment original color + vec4 hazeColor = computeHazeColor( positionES, // fragment position in eye coordinates fragPositionWS, // fragment position in world coordinates invViewMat[3].xyz, // eye position in world coordinates lightDirection // keylight direction vector ); - color = colorV4.rgb; + color = mix(color.rgb, hazeColor.rgb, hazeColor.a); }*/ return color; diff --git a/libraries/render-utils/src/Haze.slf b/libraries/render-utils/src/Haze.slf index 6b45a72768..93b66d99ed 100644 --- a/libraries/render-utils/src/Haze.slf +++ b/libraries/render-utils/src/Haze.slf @@ -22,7 +22,6 @@ <@include Haze.slh@> -uniform sampler2D colorMap; uniform sampler2D linearDepthMap; vec4 unpackPositionFromZeye(vec2 texcoord) { @@ -46,7 +45,6 @@ void main(void) { discard; } - vec4 fragColor = texture(colorMap, varTexCoord0); vec4 fragPositionES = unpackPositionFromZeye(varTexCoord0); mat4 viewInverse = getViewInverse(); @@ -56,5 +54,8 @@ void main(void) { Light light = getKeyLight(); vec3 lightDirectionWS = getLightDirection(light); - outFragColor = computeHazeColor(fragColor, fragPositionES.xyz, fragPositionWS.xyz, eyePositionWS.xyz, lightDirectionWS); + outFragColor = computeHazeColor(fragPositionES.xyz, fragPositionWS.xyz, eyePositionWS.xyz, lightDirectionWS); + if (outFragColor.a < 1e-4) { + discard; + } } diff --git a/libraries/render-utils/src/Haze.slh b/libraries/render-utils/src/Haze.slh index ab973ba752..7854ad08ca 100644 --- a/libraries/render-utils/src/Haze.slh +++ b/libraries/render-utils/src/Haze.slh @@ -92,22 +92,21 @@ vec3 computeHazeColorKeyLightAttenuation(vec3 color, vec3 lightDirectionWS, vec3 } // Input: -// fragColor - fragment original color // fragPositionES - fragment position in eye coordinates // fragPositionWS - fragment position in world coordinates // eyePositionWS - eye position in world coordinates // Output: -// fragment colour after haze effect +// haze colour and alpha contains haze blend factor // // General algorithm taken from http://www.iquilezles.org/www/articles/fog/fog.htm, with permission // -vec4 computeHazeColor(vec4 fragColor, vec3 fragPositionES, vec3 fragPositionWS, vec3 eyePositionWS, vec3 lightDirectionWS) { +vec4 computeHazeColor(vec3 fragPositionES, vec3 fragPositionWS, vec3 eyePositionWS, vec3 lightDirectionWS) { // Distance to fragment float distance = length(fragPositionES); float eyeWorldHeight = eyePositionWS.y; // Convert haze colour from uniform into a vec4 - vec4 hazeColor = vec4(hazeParams.hazeColor, 1.0); + vec4 hazeColor = vec4(hazeParams.hazeColor, 1.0); // Use the haze colour for the glare colour, if blend is not enabled vec4 blendedHazeColor; @@ -149,13 +148,13 @@ vec4 computeHazeColor(vec4 fragColor, vec3 fragPositionES, vec3 fragPositionWS, vec3 hazeAmount = 1.0 - exp(-hazeIntegral); // Compute color after haze effect - potentialFragColor = mix(fragColor, vec4(1.0, 1.0, 1.0, 1.0), vec4(hazeAmount, 1.0)); + potentialFragColor = vec4(1.0, 1.0, 1.0, hazeAmount); } else if ((hazeParams.hazeMode & HAZE_MODE_IS_ALTITUDE_BASED) != HAZE_MODE_IS_ALTITUDE_BASED) { // Haze is based only on range float hazeAmount = 1.0 - exp(-distance * hazeParams.hazeRangeFactor); // Compute color after haze effect - potentialFragColor = mix(fragColor, blendedHazeColor, hazeAmount); + potentialFragColor = vec4(blendedHazeColor.rgb, hazeAmount); } else { // Haze is based on both range and altitude // Taken from www.crytek.com/download/GDC2007_RealtimeAtmoFxInGamesRev.ppt @@ -181,16 +180,14 @@ vec4 computeHazeColor(vec4 fragColor, vec3 fragPositionES, vec3 fragPositionWS, float hazeAmount = 1.0 - exp(-hazeIntegral); // Compute color after haze effect - potentialFragColor = mix(fragColor, blendedHazeColor, hazeAmount); + potentialFragColor = vec4(blendedHazeColor.rgb, hazeAmount); } // Mix with background at far range const float BLEND_DISTANCE = 27000.0f; - vec4 outFragColor; + vec4 outFragColor = potentialFragColor; if (distance > BLEND_DISTANCE) { - outFragColor = mix(potentialFragColor, fragColor, hazeParams.backgroundBlend); - } else { - outFragColor = potentialFragColor; + outFragColor.a *= hazeParams.backgroundBlend; } return outFragColor; diff --git a/libraries/render-utils/src/HighlightEffect.cpp b/libraries/render-utils/src/HighlightEffect.cpp index 20d0cc39be..6c8a90da81 100644 --- a/libraries/render-utils/src/HighlightEffect.cpp +++ b/libraries/render-utils/src/HighlightEffect.cpp @@ -117,6 +117,9 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c assert(renderContext->args->hasViewFrustum()); auto& inShapes = inputs.get0(); + const int BOUNDS_SLOT = 0; + const int PARAMETERS_SLOT = 1; + if (!_stencilMaskPipeline || !_stencilMaskFillPipeline) { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); state->setDepthTest(true, false, gpu::LESS_EQUAL); @@ -135,6 +138,8 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("ssbo0Buffer"), BOUNDS_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("parametersBuffer"), PARAMETERS_SLOT)); gpu::Shader::makeProgram(*program, slotBindings); _stencilMaskPipeline = gpu::Pipeline::create(program, state); @@ -214,6 +219,15 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c _boundsBuffer->setData(itemBounds.size() * sizeof(render::ItemBound), (const gpu::Byte*) itemBounds.data()); + const auto securityMargin = 2.0f; + const float blurPixelWidth = 2.0f * securityMargin * HighlightSharedParameters::getBlurPixelWidth(highlight._style, args->_viewport.w); + const auto framebufferSize = ressources->getSourceFrameSize(); + const glm::vec2 highlightWidth = { blurPixelWidth / framebufferSize.x, blurPixelWidth / framebufferSize.y }; + + if (highlightWidth != _outlineWidth.get()) { + _outlineWidth.edit() = highlightWidth; + } + gpu::doInBatch("DrawHighlightMask::run::end", args->_context, [&](gpu::Batch& batch) { // Setup camera, projection and viewport for all items batch.setViewportTransform(args->_viewport); @@ -221,15 +235,10 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c batch.setViewTransform(viewMat); // Draw stencil mask with object bounding boxes - const auto highlightWidthLoc = _stencilMaskPipeline->getProgram()->getUniforms().findLocation("outlineWidth"); - const auto securityMargin = 2.0f; - const float blurPixelWidth = 2.0f * securityMargin * HighlightSharedParameters::getBlurPixelWidth(highlight._style, args->_viewport.w); - const auto framebufferSize = ressources->getSourceFrameSize(); - auto stencilPipeline = highlight._style.isFilled() ? _stencilMaskFillPipeline : _stencilMaskPipeline; batch.setPipeline(stencilPipeline); - batch.setResourceBuffer(0, _boundsBuffer); - batch._glUniform2f(highlightWidthLoc, blurPixelWidth / framebufferSize.x, blurPixelWidth / framebufferSize.y); + batch.setResourceBuffer(BOUNDS_SLOT, _boundsBuffer); + batch.setUniformBuffer(PARAMETERS_SLOT, _outlineWidth); static const int NUM_VERTICES_PER_CUBE = 36; batch.draw(gpu::TRIANGLES, NUM_VERTICES_PER_CUBE * (gpu::uint32) itemBounds.size(), 0); }); diff --git a/libraries/render-utils/src/HighlightEffect.h b/libraries/render-utils/src/HighlightEffect.h index 8af11da237..eee1c29cb7 100644 --- a/libraries/render-utils/src/HighlightEffect.h +++ b/libraries/render-utils/src/HighlightEffect.h @@ -127,6 +127,7 @@ protected: render::ShapePlumberPointer _shapePlumber; HighlightSharedParametersPointer _sharedParameters; gpu::BufferPointer _boundsBuffer; + gpu::StructBuffer _outlineWidth; static gpu::PipelinePointer _stencilMaskPipeline; static gpu::PipelinePointer _stencilMaskFillPipeline; diff --git a/libraries/render-utils/src/Highlight_aabox.slv b/libraries/render-utils/src/Highlight_aabox.slv index 4927db9610..2a87e00f94 100644 --- a/libraries/render-utils/src/Highlight_aabox.slv +++ b/libraries/render-utils/src/Highlight_aabox.slv @@ -40,7 +40,9 @@ ItemBound getItemBound(int i) { } #endif -uniform vec2 outlineWidth; +uniform parametersBuffer { + vec2 outlineWidth; +}; void main(void) { const vec3 UNIT_BOX_VERTICES[8] = vec3[8]( diff --git a/libraries/render-utils/src/LightLocal.slh b/libraries/render-utils/src/LightLocal.slh index 515a065d2e..06f8871e24 100644 --- a/libraries/render-utils/src/LightLocal.slh +++ b/libraries/render-utils/src/LightLocal.slh @@ -143,6 +143,6 @@ vec4 evalLocalLighting(ivec3 cluster, int numLights, vec3 fragWorldPos, SurfaceD fragSpecular *= isSpecularEnabled(); fragColor.rgb += fragDiffuse; - fragColor.rgb += fragSpecular / opacity; + fragColor.rgb += evalSpecularWithOpacity(fragSpecular, opacity); return fragColor; } \ No newline at end of file diff --git a/libraries/render-utils/src/LightStage.cpp b/libraries/render-utils/src/LightStage.cpp index ceac4ae3c8..369c62c197 100644 --- a/libraries/render-utils/src/LightStage.cpp +++ b/libraries/render-utils/src/LightStage.cpp @@ -74,8 +74,6 @@ LightStage::Shadow::Cascade::Cascade() : _frustum{ std::make_shared() }, _minDistance{ 0.0f }, _maxDistance{ 20.0f } { - framebuffer = gpu::FramebufferPointer(gpu::Framebuffer::createShadowmap(MAP_SIZE)); - map = framebuffer->getDepthStencilBuffer(); } const glm::mat4& LightStage::Shadow::Cascade::getView() const { @@ -127,8 +125,29 @@ LightStage::Shadow::Shadow(graphics::LightPointer light, float maxDistance, unsi Schema schema; schema.cascadeCount = cascadeCount; _schemaBuffer = std::make_shared(sizeof(Schema), (const gpu::Byte*) &schema); + + // Create shadow cascade texture array + auto depthFormat = gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::DEPTH); // Depth32 texel format + map = gpu::TexturePointer(gpu::Texture::createRenderBufferArray(depthFormat, MAP_SIZE, MAP_SIZE, cascadeCount)); + gpu::Sampler::Desc samplerDesc; + samplerDesc._borderColor = glm::vec4(1.0f); + samplerDesc._wrapModeU = gpu::Sampler::WRAP_BORDER; + samplerDesc._wrapModeV = gpu::Sampler::WRAP_BORDER; + samplerDesc._filter = gpu::Sampler::FILTER_MIN_MAG_LINEAR; + samplerDesc._comparisonFunc = gpu::LESS; + + map->setSampler(gpu::Sampler(samplerDesc)); + _cascades.resize(cascadeCount); + for (uint cascadeIndex=0; cascadeIndex < cascadeCount; cascadeIndex++) { + auto& cascade = _cascades[cascadeIndex]; + std::string name = "Shadowmap Cascade "; + name += '0' + cascadeIndex; + cascade.framebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(name)); + cascade.framebuffer->setDepthBuffer(map, depthFormat, cascadeIndex); + } + setMaxDistance(maxDistance); } diff --git a/libraries/render-utils/src/LightStage.h b/libraries/render-utils/src/LightStage.h index 9812426fa6..b8a49d81bb 100644 --- a/libraries/render-utils/src/LightStage.h +++ b/libraries/render-utils/src/LightStage.h @@ -53,7 +53,6 @@ public: Cascade(); gpu::FramebufferPointer framebuffer; - gpu::TexturePointer map; const std::shared_ptr& getFrustum() const { return _frustum; } @@ -93,6 +92,8 @@ public: const graphics::LightPointer& getLight() const { return _light; } + gpu::TexturePointer map; + protected: #include "Shadows_shared.slh" diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index 3b1c94f949..fe2d684c32 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -314,6 +314,9 @@ void evalFragShadingGloss(out vec3 diffuse, out vec3 specular, specular = shading.xyz; } +vec3 evalSpecularWithOpacity(vec3 specular, float opacity) { + return specular / opacity; +} <@if not GETFRESNEL0@> <@def GETFRESNEL0@> diff --git a/libraries/render-utils/src/MaterialTextures.slh b/libraries/render-utils/src/MaterialTextures.slh index 4bcd74eefe..0b83fd1334 100644 --- a/libraries/render-utils/src/MaterialTextures.slh +++ b/libraries/render-utils/src/MaterialTextures.slh @@ -267,7 +267,7 @@ vec3 fetchLightmapMap(vec2 uv) { <@func discardTransparent(opacity)@> { - if (<$opacity$> < 1.0) { + if (<$opacity$> < 1e-6) { discard; } } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 3cd4501c4b..71312f3bb3 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1675,13 +1675,12 @@ void Model::removeMaterial(graphics::MaterialPointer material, const std::string for (auto shapeID : shapeIDs) { if (shapeID < _modelMeshRenderItemIDs.size()) { auto itemID = _modelMeshRenderItemIDs[shapeID]; - bool visible = isVisible(); auto renderItemsKey = _renderItemKeyGlobalFlags; bool wireframe = isWireframe(); auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); bool useDualQuaternionSkinning = _useDualQuaternionSkinning; - transaction.updateItem(itemID, [material, visible, renderItemsKey, + transaction.updateItem(itemID, [material, renderItemsKey, invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning](ModelMeshPartPayload& data) { data.removeMaterial(material); // if the material changed, we might need to update our item key or shape key diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp index 63d07af1c2..c2181b7613 100644 --- a/libraries/render-utils/src/RenderCommonTask.cpp +++ b/libraries/render-utils/src/RenderCommonTask.cpp @@ -51,19 +51,19 @@ void DrawOverlay3D::run(const RenderContextPointer& renderContext, const Inputs& config->setNumDrawn((int)inItems.size()); emit config->numDrawnChanged(); + RenderArgs* args = renderContext->args; + + // Clear the framebuffer without stereo + // Needs to be distinct from the other batch because using the clear call + // while stereo is enabled triggers a warning + if (_opaquePass) { + gpu::doInBatch("DrawOverlay3D::run::clear", args->_context, [&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.clearFramebuffer(gpu::Framebuffer::BUFFER_DEPTH, glm::vec4(), 1.f, 0, false); + }); + } + if (!inItems.empty()) { - RenderArgs* args = renderContext->args; - - // Clear the framebuffer without stereo - // Needs to be distinct from the other batch because using the clear call - // while stereo is enabled triggers a warning - if (_opaquePass) { - gpu::doInBatch("DrawOverlay3D::run::clear", args->_context, [&](gpu::Batch& batch){ - batch.enableStereo(false); - batch.clearFramebuffer(gpu::Framebuffer::BUFFER_DEPTHSTENCIL, glm::vec4(), 1.f, 0, false); - }); - } - // Render the items gpu::doInBatch("DrawOverlay3D::main", args->_context, [&](gpu::Batch& batch) { args->_batch = &batch; diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 32bdad280c..0b05977265 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include "RenderHifi.h" #include "RenderCommonTask.h" @@ -59,8 +60,14 @@ RenderDeferredTask::RenderDeferredTask() { } -void RenderDeferredTask::configure(const Config& config) -{ +void RenderDeferredTask::configure(const Config& config) { + // Propagate resolution scale to sub jobs who need it + auto preparePrimaryBufferConfig = config.getConfig("PreparePrimaryBuffer"); + auto upsamplePrimaryBufferConfig = config.getConfig("PrimaryBufferUpscale"); + assert(preparePrimaryBufferConfig); + assert(upsamplePrimaryBufferConfig); + preparePrimaryBufferConfig->setProperty("resolutionScale", config.resolutionScale); + upsamplePrimaryBufferConfig->setProperty("factor", 1.0f / config.resolutionScale); } const render::Varying RenderDeferredTask::addSelectItemJobs(JobModel& task, const char* selectionName, @@ -97,23 +104,22 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren const auto jitter = task.addJob("JitterCam"); - // Prepare deferred, generate the shared Deferred Frame Transform + // GPU jobs: Start preparing the primary, deferred and lighting buffer + const auto scaledPrimaryFramebuffer = task.addJob("PreparePrimaryBuffer"); + + // Prepare deferred, generate the shared Deferred Frame Transform. Only valid with the scaled frame buffer const auto deferredFrameTransform = task.addJob("DeferredFrameTransform", jitter); const auto lightingModel = task.addJob("LightingModel"); - - - // GPU jobs: Start preparing the primary, deferred and lighting buffer - const auto primaryFramebuffer = task.addJob("PreparePrimaryBuffer"); const auto opaqueRangeTimer = task.addJob("BeginOpaqueRangeTimer", "DrawOpaques"); - const auto prepareDeferredInputs = PrepareDeferred::Inputs(primaryFramebuffer, lightingModel).asVarying(); + const auto prepareDeferredInputs = PrepareDeferred::Inputs(scaledPrimaryFramebuffer, lightingModel).asVarying(); const auto prepareDeferredOutputs = task.addJob("PrepareDeferred", prepareDeferredInputs); const auto deferredFramebuffer = prepareDeferredOutputs.getN(0); const auto lightingFramebuffer = prepareDeferredOutputs.getN(1); // draw a stencil mask in hidden regions of the framebuffer. - task.addJob("PrepareStencil", primaryFramebuffer); + task.addJob("PrepareStencil", scaledPrimaryFramebuffer); // Render opaque objects in DeferredBuffer const auto opaqueInputs = DrawStateSortDeferred::Inputs(opaques, lightingModel, jitter).asVarying(); @@ -174,7 +180,7 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren // Similar to light stage, background stage has been filled by several potential render items and resolved for the frame in this job task.addJob("DrawBackgroundDeferred", lightingModel); - const auto drawHazeInputs = render::Varying(DrawHaze::Inputs(hazeModel, lightingFramebuffer, linearDepthTarget, deferredFrameTransform, lightingFramebuffer)); + const auto drawHazeInputs = render::Varying(DrawHaze::Inputs(hazeModel, lightingFramebuffer, linearDepthTarget, deferredFrameTransform, lightingModel)); task.addJob("DrawHazeDeferred", drawHazeInputs); // Render transparent objects forward in LightingBuffer @@ -223,7 +229,7 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren task.addJob("Bloom", bloomInputs); // Lighting Buffer ready for tone mapping - const auto toneMappingInputs = ToneMappingDeferred::Inputs(lightingFramebuffer, primaryFramebuffer).asVarying(); + const auto toneMappingInputs = ToneMappingDeferred::Inputs(lightingFramebuffer, scaledPrimaryFramebuffer).asVarying(); task.addJob("ToneMapping", toneMappingInputs); { // Debug the bounds of the rendered items, still look at the zbuffer @@ -284,6 +290,9 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren task.addJob("DrawZoneStack", deferredFrameTransform); } + // Upscale to finale resolution + const auto primaryFramebuffer = task.addJob("PrimaryBufferUpscale", scaledPrimaryFramebuffer); + // Composite the HUD and HUD overlays task.addJob("HUD"); diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index ab6ab177d2..1ce1682cf1 100644 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -105,11 +105,13 @@ class RenderDeferredTaskConfig : public render::Task::Config { Q_OBJECT Q_PROPERTY(float fadeScale MEMBER fadeScale NOTIFY dirty) Q_PROPERTY(float fadeDuration MEMBER fadeDuration NOTIFY dirty) + Q_PROPERTY(float resolutionScale MEMBER resolutionScale NOTIFY dirty) Q_PROPERTY(bool debugFade MEMBER debugFade NOTIFY dirty) Q_PROPERTY(float debugFadePercent MEMBER debugFadePercent NOTIFY dirty) public: float fadeScale{ 0.5f }; float fadeDuration{ 3.0f }; + float resolutionScale{ 1.f }; float debugFadePercent{ 0.f }; bool debugFade{ false }; diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index fbb4bba263..91eb777199 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -227,7 +227,7 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende } const auto setupOutput = task.addJob("ShadowSetup"); - const auto queryResolution = setupOutput.getN(2); + const auto queryResolution = setupOutput.getN(1); // Fetch and cull the items from the scene static const auto shadowCasterReceiverFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask); @@ -248,10 +248,12 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende const auto sortedShapes = task.addJob("DepthSortShadow", sortedPipelines, true); render::Varying cascadeFrustums[SHADOW_CASCADE_MAX_COUNT] = { - ViewFrustumPointer(), - ViewFrustumPointer(), + ViewFrustumPointer() +#if SHADOW_CASCADE_MAX_COUNT>1 + ,ViewFrustumPointer(), ViewFrustumPointer(), ViewFrustumPointer() +#endif }; for (auto i = 0; i < SHADOW_CASCADE_MAX_COUNT; i++) { @@ -293,13 +295,15 @@ RenderShadowSetup::RenderShadowSetup() : void RenderShadowSetup::configure(const Config& configuration) { setConstantBias(0, configuration.constantBias0); - setConstantBias(1, configuration.constantBias1); - setConstantBias(2, configuration.constantBias2); - setConstantBias(3, configuration.constantBias3); setSlopeBias(0, configuration.slopeBias0); +#if SHADOW_CASCADE_MAX_COUNT>1 + setConstantBias(1, configuration.constantBias1); setSlopeBias(1, configuration.slopeBias1); + setConstantBias(2, configuration.constantBias2); setSlopeBias(2, configuration.slopeBias2); + setConstantBias(3, configuration.constantBias3); setSlopeBias(3, configuration.slopeBias3); +#endif } void RenderShadowSetup::setConstantBias(int cascadeIndex, float value) { diff --git a/libraries/render-utils/src/Shadow.slh b/libraries/render-utils/src/Shadow.slh index 36eb35c757..235ea519ab 100644 --- a/libraries/render-utils/src/Shadow.slh +++ b/libraries/render-utils/src/Shadow.slh @@ -17,11 +17,11 @@ #define SHADOW_SCREEN_SPACE_DITHER 1 // the shadow texture -uniform sampler2DShadow shadowMaps[SHADOW_CASCADE_MAX_COUNT]; +uniform sampler2DArrayShadow shadowMaps; // Sample the shadowMap with PCF (built-in) float fetchShadow(int cascadeIndex, vec3 shadowTexcoord) { - return texture(shadowMaps[cascadeIndex], shadowTexcoord); + return texture(shadowMaps, vec4(shadowTexcoord.xy, cascadeIndex, shadowTexcoord.z)); } vec2 PCFkernel[4] = vec2[4]( diff --git a/libraries/render-utils/src/debug_deferred_buffer.slf b/libraries/render-utils/src/debug_deferred_buffer.slf index fded04ca87..5f974acfeb 100644 --- a/libraries/render-utils/src/debug_deferred_buffer.slf +++ b/libraries/render-utils/src/debug_deferred_buffer.slf @@ -23,11 +23,18 @@ uniform sampler2D occlusionMap; uniform sampler2D occlusionBlurredMap; uniform sampler2D scatteringMap; uniform sampler2D velocityMap; +uniform sampler2DArrayShadow shadowMaps; <@include ShadowCore.slh@> <$declareDeferredCurvature()$> +<@include debug_deferred_buffer_shared.slh@> + +layout(std140) uniform parametersBuffer { + DebugParameters parameters; +}; + float curvatureAO(float k) { return 1.0f - (0.0022f * k * k) + (0.0776f * k) + 0.7369f; } diff --git a/libraries/render-utils/src/debug_deferred_buffer_shared.slh b/libraries/render-utils/src/debug_deferred_buffer_shared.slh new file mode 100644 index 0000000000..2d11a66d61 --- /dev/null +++ b/libraries/render-utils/src/debug_deferred_buffer_shared.slh @@ -0,0 +1,17 @@ +// glsl / C++ compatible source as interface for FadeEffect +#ifdef __cplusplus +# define INT32 glm::int32 +#else +# define INT32 int +#endif + +struct DebugParameters +{ + INT32 _shadowCascadeIndex; +}; + + // <@if 1@> + // Trigger Scribe include + // <@endif@> +// + diff --git a/libraries/render-utils/src/forward_model_translucent.slf b/libraries/render-utils/src/forward_model_translucent.slf index b8d43f15f1..70a3233737 100644 --- a/libraries/render-utils/src/forward_model_translucent.slf +++ b/libraries/render-utils/src/forward_model_translucent.slf @@ -41,6 +41,7 @@ void main(void) { float opacity = getMaterialOpacity(mat) * _alpha; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + <$discardTransparent(opacity)$>; vec3 albedo = getMaterialAlbedo(mat); <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; diff --git a/libraries/render-utils/src/model_translucent.slf b/libraries/render-utils/src/model_translucent.slf index 71f76c8a8d..b808ca4bab 100644 --- a/libraries/render-utils/src/model_translucent.slf +++ b/libraries/render-utils/src/model_translucent.slf @@ -44,6 +44,7 @@ void main(void) { float opacity = getMaterialOpacity(mat) * _alpha; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + <$discardTransparent(opacity)$>; vec3 albedo = getMaterialAlbedo(mat); <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; diff --git a/libraries/render-utils/src/model_translucent_fade.slf b/libraries/render-utils/src/model_translucent_fade.slf index 8b40186448..a93adee96b 100644 --- a/libraries/render-utils/src/model_translucent_fade.slf +++ b/libraries/render-utils/src/model_translucent_fade.slf @@ -46,6 +46,7 @@ void main(void) { float opacity = getMaterialOpacity(mat) * _alpha; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + <$discardTransparent(opacity)$>; vec3 albedo = getMaterialAlbedo(mat); <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; diff --git a/libraries/render-utils/src/model_translucent_normal_map.slf b/libraries/render-utils/src/model_translucent_normal_map.slf index 320e883bb0..750149dc1b 100644 --- a/libraries/render-utils/src/model_translucent_normal_map.slf +++ b/libraries/render-utils/src/model_translucent_normal_map.slf @@ -45,6 +45,7 @@ void main(void) { float opacity = getMaterialOpacity(mat) * _alpha; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + <$discardTransparent(opacity)$>; vec3 albedo = getMaterialAlbedo(mat); <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; diff --git a/libraries/render-utils/src/model_translucent_normal_map_fade.slf b/libraries/render-utils/src/model_translucent_normal_map_fade.slf index 0e114f7fdd..c7615626ce 100644 --- a/libraries/render-utils/src/model_translucent_normal_map_fade.slf +++ b/libraries/render-utils/src/model_translucent_normal_map_fade.slf @@ -54,6 +54,7 @@ void main(void) { float opacity = getMaterialOpacity(mat) * _alpha; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + <$discardTransparent(opacity)$>; vec3 albedo = getMaterialAlbedo(mat); <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; diff --git a/libraries/render-utils/src/model_translucent_unlit.slf b/libraries/render-utils/src/model_translucent_unlit.slf index ebe9901616..e5507dd2e0 100644 --- a/libraries/render-utils/src/model_translucent_unlit.slf +++ b/libraries/render-utils/src/model_translucent_unlit.slf @@ -31,6 +31,7 @@ void main(void) { float opacity = getMaterialOpacity(mat) * _alpha; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + <$discardTransparent(opacity)$>; vec3 albedo = getMaterialAlbedo(mat); <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; diff --git a/libraries/render-utils/src/model_translucent_unlit_fade.slf b/libraries/render-utils/src/model_translucent_unlit_fade.slf index 0f7c3366bb..016db4639f 100644 --- a/libraries/render-utils/src/model_translucent_unlit_fade.slf +++ b/libraries/render-utils/src/model_translucent_unlit_fade.slf @@ -41,6 +41,7 @@ void main(void) { float opacity = getMaterialOpacity(mat) * _alpha; <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + <$discardTransparent(opacity)$>; vec3 albedo = getMaterialAlbedo(mat); <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; diff --git a/libraries/render/src/render/CullTask.cpp b/libraries/render/src/render/CullTask.cpp index 3f55e6dedc..8cfe7683ce 100644 --- a/libraries/render/src/render/CullTask.cpp +++ b/libraries/render/src/render/CullTask.cpp @@ -370,10 +370,13 @@ void CullShapeBounds::run(const RenderContextPointer& renderContext, const Input const auto& inShapes = inputs.get0(); const auto& cullFilter = inputs.get1(); const auto& boundsFilter = inputs.get2(); - const auto& antiFrustum = inputs.get3(); + ViewFrustumPointer antiFrustum; auto& outShapes = outputs.edit0(); auto& outBounds = outputs.edit1(); + if (!inputs[3].isNull()) { + antiFrustum = inputs.get3(); + } outShapes.clear(); outBounds = AABox(); diff --git a/libraries/render/src/render/ResampleTask.cpp b/libraries/render/src/render/ResampleTask.cpp index 07f7367582..008234b437 100644 --- a/libraries/render/src/render/ResampleTask.cpp +++ b/libraries/render/src/render/ResampleTask.cpp @@ -81,3 +81,69 @@ void HalfDownsample::run(const RenderContextPointer& renderContext, const gpu::F batch.draw(gpu::TRIANGLE_STRIP, 4); }); } + +gpu::PipelinePointer Upsample::_pipeline; + +void Upsample::configure(const Config& config) { + _factor = config.factor; +} + +gpu::FramebufferPointer Upsample::getResampledFrameBuffer(const gpu::FramebufferPointer& sourceFramebuffer) { + if (_factor == 1.0f) { + return sourceFramebuffer; + } + + auto resampledFramebufferSize = glm::uvec2(glm::vec2(sourceFramebuffer->getSize()) * _factor); + + if (!_destinationFrameBuffer || resampledFramebufferSize != _destinationFrameBuffer->getSize()) { + _destinationFrameBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("UpsampledOutput")); + + auto sampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR); + auto target = gpu::Texture::createRenderBuffer(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), resampledFramebufferSize.x, resampledFramebufferSize.y, gpu::Texture::SINGLE_MIP, sampler); + _destinationFrameBuffer->setRenderBuffer(0, target); + } + return _destinationFrameBuffer; +} + +void Upsample::run(const RenderContextPointer& renderContext, const gpu::FramebufferPointer& sourceFramebuffer, gpu::FramebufferPointer& resampledFrameBuffer) { + assert(renderContext->args); + assert(renderContext->args->hasViewFrustum()); + RenderArgs* args = renderContext->args; + + resampledFrameBuffer = getResampledFrameBuffer(sourceFramebuffer); + if (resampledFrameBuffer != sourceFramebuffer) { + if (!_pipeline) { + auto vs = gpu::StandardShaderLib::getDrawTransformUnitQuadVS(); + auto ps = gpu::StandardShaderLib::getDrawTextureOpaquePS(); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + + gpu::Shader::BindingSet slotBindings; + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + state->setDepthTest(gpu::State::DepthTest(false, false)); + _pipeline = gpu::Pipeline::create(program, state); + } + + const auto bufferSize = resampledFrameBuffer->getSize(); + glm::ivec4 viewport{ 0, 0, bufferSize.x, bufferSize.y }; + + gpu::doInBatch("Upsample::run", args->_context, [&](gpu::Batch& batch) { + batch.enableStereo(false); + + batch.setFramebuffer(resampledFrameBuffer); + + batch.setViewportTransform(viewport); + batch.setProjectionTransform(glm::mat4()); + batch.resetViewTransform(); + batch.setPipeline(_pipeline); + + batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(bufferSize, viewport)); + batch.setResourceTexture(0, sourceFramebuffer->getRenderBuffer(0)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + + // Set full final viewport + args->_viewport = viewport; + } +} diff --git a/libraries/render/src/render/ResampleTask.h b/libraries/render/src/render/ResampleTask.h index da2b7b3537..25f9c6a3e9 100644 --- a/libraries/render/src/render/ResampleTask.h +++ b/libraries/render/src/render/ResampleTask.h @@ -36,6 +36,37 @@ namespace render { gpu::FramebufferPointer getResampledFrameBuffer(const gpu::FramebufferPointer& sourceFramebuffer); }; + + class UpsampleConfig : public render::Job::Config { + Q_OBJECT + Q_PROPERTY(float factor MEMBER factor NOTIFY dirty) + public: + + float factor{ 1.0f }; + + signals: + void dirty(); + }; + + class Upsample { + public: + using Config = UpsampleConfig; + using JobModel = Job::ModelIO; + + Upsample(float factor = 2.0f) : _factor{ factor } {} + + void configure(const Config& config); + void run(const RenderContextPointer& renderContext, const gpu::FramebufferPointer& sourceFramebuffer, gpu::FramebufferPointer& resampledFrameBuffer); + + protected: + + static gpu::PipelinePointer _pipeline; + + gpu::FramebufferPointer _destinationFrameBuffer; + float _factor{ 2.0f }; + + gpu::FramebufferPointer getResampledFrameBuffer(const gpu::FramebufferPointer& sourceFramebuffer); + }; } #endif // hifi_render_ResampleTask_h diff --git a/libraries/render/src/render/ShapePipeline.cpp b/libraries/render/src/render/ShapePipeline.cpp index 1ce58c49ae..8cd04f8067 100644 --- a/libraries/render/src/render/ShapePipeline.cpp +++ b/libraries/render/src/render/ShapePipeline.cpp @@ -95,6 +95,7 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), Slot::MAP::LIGHT_AMBIENT_MAP)); slotBindings.insert(gpu::Shader::Binding(std::string("fadeMaskMap"), Slot::MAP::FADE_MASK)); slotBindings.insert(gpu::Shader::Binding(std::string("fadeParametersBuffer"), Slot::BUFFER::FADE_PARAMETERS)); + slotBindings.insert(gpu::Shader::Binding(std::string("fadeObjectParametersBuffer"), Slot::BUFFER::FADE_OBJECT_PARAMETERS)); slotBindings.insert(gpu::Shader::Binding(std::string("hazeBuffer"), Slot::BUFFER::HAZE_MODEL)); if (key.isTranslucent()) { @@ -124,6 +125,7 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p locations->lightAmbientMapUnit = program->getTextures().findLocation("skyboxMap"); locations->fadeMaskTextureUnit = program->getTextures().findLocation("fadeMaskMap"); locations->fadeParameterBufferUnit = program->getUniformBuffers().findLocation("fadeParametersBuffer"); + locations->fadeObjectParameterBufferUnit = program->getUniformBuffers().findLocation("fadeObjectParametersBuffer"); locations->hazeParameterBufferUnit = program->getUniformBuffers().findLocation("hazeBuffer"); if (key.isTranslucent()) { locations->lightClusterGridBufferUnit = program->getUniformBuffers().findLocation("clusterGridBuffer"); diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index 7d87d98deb..10f1b757cc 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -240,6 +240,7 @@ public: LIGHT_AMBIENT_BUFFER, HAZE_MODEL, FADE_PARAMETERS, + FADE_OBJECT_PARAMETERS, LIGHT_CLUSTER_GRID_FRUSTUM_GRID_SLOT, LIGHT_CLUSTER_GRID_CLUSTER_GRID_SLOT, LIGHT_CLUSTER_GRID_CLUSTER_CONTENT_SLOT, @@ -254,9 +255,9 @@ public: ROUGHNESS, OCCLUSION, SCATTERING, - FADE_MASK, LIGHT_AMBIENT_MAP = 10, + FADE_MASK, }; }; @@ -278,6 +279,7 @@ public: int lightAmbientMapUnit; int fadeMaskTextureUnit; int fadeParameterBufferUnit; + int fadeObjectParameterBufferUnit; int hazeParameterBufferUnit; int lightClusterGridBufferUnit; int lightClusterContentBufferUnit; diff --git a/libraries/render/src/render/Transition.h b/libraries/render/src/render/Transition.h index 622e6f69ce..30bda8aa2a 100644 --- a/libraries/render/src/render/Transition.h +++ b/libraries/render/src/render/Transition.h @@ -42,6 +42,8 @@ namespace render { glm::vec3 baseInvSize{ 1.f, 1.f, 1.f }; float threshold{ 0.f }; uint8_t isFinished{ 0 }; + + mutable gpu::BufferView paramsBuffer; }; typedef std::shared_ptr TransitionPointer; diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index f248c20d41..72918e33f6 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -23,6 +23,21 @@ void registerAudioMetaTypes(QScriptEngine* engine) { qScriptRegisterMetaType(engine, soundSharedPointerToScriptValue, soundSharedPointerFromScriptValue); } + +void AudioScriptingInterface::setLocalAudioInterface(AbstractAudioInterface* audioInterface) { + if (_localAudioInterface) { + disconnect(_localAudioInterface, &AbstractAudioInterface::isStereoInputChanged, + this, &AudioScriptingInterface::isStereoInputChanged); + } + + _localAudioInterface = audioInterface; + + if (_localAudioInterface) { + connect(_localAudioInterface, &AbstractAudioInterface::isStereoInputChanged, + this, &AudioScriptingInterface::isStereoInputChanged); + } +} + ScriptAudioInjector* AudioScriptingInterface::playSystemSound(SharedSoundPointer sound, const QVector3D& position) { AudioInjectorOptions options; options.position = glm::vec3(position.x(), position.y(), position.z()); @@ -60,12 +75,10 @@ ScriptAudioInjector* AudioScriptingInterface::playSound(SharedSoundPointer sound } } -bool AudioScriptingInterface::setStereoInput(bool stereo) { - bool stereoInputChanged = false; +void AudioScriptingInterface::setStereoInput(bool stereo) { if (_localAudioInterface) { - stereoInputChanged = _localAudioInterface->setIsStereoInput(stereo); + QMetaObject::invokeMethod(_localAudioInterface, "setIsStereoInput", Q_ARG(bool, stereo)); } - return stereoInputChanged; } bool AudioScriptingInterface::isStereoInput() { diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index 36fe29243d..20ca977da1 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -23,9 +23,11 @@ class AudioScriptingInterface : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY + Q_PROPERTY(bool isStereoInput READ isStereoInput WRITE setStereoInput NOTIFY isStereoInputChanged) + public: virtual ~AudioScriptingInterface() {} - void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } + void setLocalAudioInterface(AbstractAudioInterface* audioInterface); protected: AudioScriptingInterface() {} @@ -52,9 +54,8 @@ protected: /**jsdoc * @function Audio.setStereoInput * @param {boolean} stereo - * @returns {boolean} */ - Q_INVOKABLE bool setStereoInput(bool stereo); + Q_INVOKABLE void setStereoInput(bool stereo); /**jsdoc * @function Audio.isStereoInput @@ -114,6 +115,13 @@ signals: */ void inputReceived(const QByteArray& inputSamples); + /**jsdoc + * @function Audio.isStereoInputChanged + * @param {boolean} isStereo + * @returns {Signal} + */ + void isStereoInputChanged(bool isStereo); + private: AbstractAudioInterface* _localAudioInterface { nullptr }; }; diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index f8c99b192f..99c02ba1f6 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -1639,22 +1639,24 @@ QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const boo loader->start(MAX_RETRIES); if (!loader->isFinished()) { - QTimer monitor; - QEventLoop loop; - QObject::connect(loader, &BatchLoader::finished, this, [&monitor, &loop]{ - monitor.stop(); - loop.quit(); + // This lambda can get called AFTER this local scope has completed. + // This is why we pass smart ptrs to the lambda instead of references to local variables. + auto monitor = std::make_shared(); + auto loop = std::make_shared(); + QObject::connect(loader, &BatchLoader::finished, this, [monitor, loop] { + monitor->stop(); + loop->quit(); }); // this helps detect the case where stop() is invoked during the download // but not seen in time to abort processing in onload()... - connect(&monitor, &QTimer::timeout, this, [this, &loop]{ + connect(monitor.get(), &QTimer::timeout, this, [this, loop] { if (isStopping()) { - loop.exit(-1); + loop->exit(-1); } }); - monitor.start(500); - loop.exec(); + monitor->start(500); + loop->exec(); } loader->deleteLater(); return req; diff --git a/libraries/shared/src/AvatarConstants.h b/libraries/shared/src/AvatarConstants.h index 58cbff6669..3dc344dc6f 100644 --- a/libraries/shared/src/AvatarConstants.h +++ b/libraries/shared/src/AvatarConstants.h @@ -24,6 +24,17 @@ const float DEFAULT_AVATAR_SUPPORT_BASE_LEFT = -0.25f; const float DEFAULT_AVATAR_SUPPORT_BASE_RIGHT = 0.25f; const float DEFAULT_AVATAR_SUPPORT_BASE_FRONT = -0.20f; const float DEFAULT_AVATAR_SUPPORT_BASE_BACK = 0.10f; +const float DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD = 0.10f; +const float DEFAULT_AVATAR_ANTERIOR_STEPPING_THRESHOLD = 0.04f; +const float DEFAULT_AVATAR_POSTERIOR_STEPPING_THRESHOLD = 0.07f; +const float DEFAULT_AVATAR_HEAD_ANGULAR_VELOCITY_STEPPING_THRESHOLD = 0.3f; +const float DEFAULT_AVATAR_MODE_HEIGHT_STEPPING_THRESHOLD = -0.02f; +const float DEFAULT_HANDS_VELOCITY_DIRECTION_STEPPING_THRESHOLD = 0.4f; +const float DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD = 3.3f; +const float DEFAULT_HEAD_VELOCITY_STEPPING_THRESHOLD = 0.18f; +const float DEFAULT_HEAD_PITCH_STEPPING_TOLERANCE = 7.0f; +const float DEFAULT_HEAD_ROLL_STEPPING_TOLERANCE = 7.0f; +const float DEFAULT_AVATAR_SPINE_STRETCH_LIMIT = 0.07f; const float DEFAULT_AVATAR_FORWARD_DAMPENING_FACTOR = 0.5f; const float DEFAULT_AVATAR_LATERAL_DAMPENING_FACTOR = 2.0f; const float DEFAULT_AVATAR_HIPS_MASS = 40.0f; diff --git a/libraries/shared/src/FaceshiftConstants.cpp b/libraries/shared/src/FaceshiftConstants.cpp index e6c929409a..0d6f718e49 100644 --- a/libraries/shared/src/FaceshiftConstants.cpp +++ b/libraries/shared/src/FaceshiftConstants.cpp @@ -64,3 +64,18 @@ const char* FACESHIFT_BLENDSHAPES[] = { }; const int NUM_FACESHIFT_BLENDSHAPES = sizeof(FACESHIFT_BLENDSHAPES) / sizeof(char*); + +const int EYE_BLINK_L_INDEX = 0; +const int EYE_BLINK_R_INDEX = 1; +const int EYE_SQUINT_L_INDEX = 2; +const int EYE_SQUINT_R_INDEX = 3; +const int EYE_OPEN_L_INDEX = 8; +const int EYE_OPEN_R_INDEX = 9; +const int BROWS_U_L_INDEX = 17; +const int BROWS_U_R_INDEX = 18; + + +const int EYE_BLINK_INDICES[] = { EYE_BLINK_L_INDEX, EYE_BLINK_R_INDEX }; +const int EYE_SQUINT_INDICES[] = { EYE_SQUINT_L_INDEX, EYE_SQUINT_R_INDEX }; +const int EYE_OPEN_INDICES[] = { EYE_OPEN_L_INDEX, EYE_OPEN_R_INDEX }; +const int BROWS_U_INDICES[] = { BROWS_U_L_INDEX, BROWS_U_R_INDEX }; diff --git a/libraries/shared/src/FaceshiftConstants.h b/libraries/shared/src/FaceshiftConstants.h index ee6e43fdbc..4349a3a21e 100644 --- a/libraries/shared/src/FaceshiftConstants.h +++ b/libraries/shared/src/FaceshiftConstants.h @@ -16,5 +16,10 @@ extern const char* FACESHIFT_BLENDSHAPES[]; /// The size of FACESHIFT_BLENDSHAPES extern const int NUM_FACESHIFT_BLENDSHAPES; +// Eyes and Brows indices +extern const int EYE_BLINK_INDICES[]; +extern const int EYE_OPEN_INDICES[]; +extern const int BROWS_U_INDICES[]; +extern const int EYE_SQUINT_INDICES[]; -#endif // hifi_FaceshiftConstants_h \ No newline at end of file +#endif // hifi_FaceshiftConstants_h diff --git a/libraries/shared/src/PhysicsHelpers.cpp b/libraries/shared/src/PhysicsHelpers.cpp index b43d55020e..988af98c46 100644 --- a/libraries/shared/src/PhysicsHelpers.cpp +++ b/libraries/shared/src/PhysicsHelpers.cpp @@ -42,22 +42,27 @@ glm::quat computeBulletRotationStep(const glm::vec3& angularVelocity, float time // Exponential map // google for "Practical Parameterization of Rotations Using the Exponential Map", F. Sebastian Grassia - float speed = glm::length(angularVelocity); + glm::vec3 axis = angularVelocity; + float angle = glm::length(axis) * timeStep; // limit the angular motion because the exponential approximation fails for large steps const float ANGULAR_MOTION_THRESHOLD = 0.5f * PI_OVER_TWO; - if (speed * timeStep > ANGULAR_MOTION_THRESHOLD) { - speed = ANGULAR_MOTION_THRESHOLD / timeStep; + if (angle > ANGULAR_MOTION_THRESHOLD) { + angle = ANGULAR_MOTION_THRESHOLD; } - glm::vec3 axis = angularVelocity; - if (speed < 0.001f) { - // use Taylor's expansions of sync function - axis *= (0.5f * timeStep - (timeStep * timeStep * timeStep) * (0.020833333333f * speed * speed)); + const float MIN_ANGLE = 0.001f; + if (angle < MIN_ANGLE) { + // for small angles use Taylor's expansion of sin(x): + // sin(x) = x - (x^3)/(3!) + ... + // where: x = angle/2 + // sin(angle/2) = angle/2 - (angle*angle*angle)/48 + // but (angle = speed * timeStep) and we want to normalize the axis by dividing by speed + // which gives us: + axis *= timeStep * (0.5f - 0.020833333333f * angle * angle); } else { - // sync(speed) = sin(c * speed)/t - axis *= (sinf(0.5f * speed * timeStep) / speed ); + axis *= (sinf(0.5f * angle) * timeStep / angle); } - return glm::quat(cosf(0.5f * speed * timeStep), axis.x, axis.y, axis.z); + return glm::quat(cosf(0.5f * angle), axis.x, axis.y, axis.z); } /* end Bullet code derivation*/ diff --git a/libraries/shared/src/Preferences.h b/libraries/shared/src/Preferences.h index 931508e825..27bcf7a71b 100644 --- a/libraries/shared/src/Preferences.h +++ b/libraries/shared/src/Preferences.h @@ -15,6 +15,7 @@ #include #include #include +#include #include "DependencyManager.h" @@ -80,7 +81,6 @@ public: } void setEnabler(BoolPreference* enabler, bool inverse = false); - virtual Type getType() { return Invalid; }; Q_INVOKABLE virtual void load() {}; diff --git a/libraries/ui/src/InteractiveWindow.cpp b/libraries/ui/src/InteractiveWindow.cpp new file mode 100644 index 0000000000..5078fcb602 --- /dev/null +++ b/libraries/ui/src/InteractiveWindow.cpp @@ -0,0 +1,291 @@ +// +// InteractiveWindow.cpp +// libraries/ui/src +// +// Created by Thijs Wenker on 2018-06-25 +// Copyright 2018 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 "InteractiveWindow.h" + +#include +#include + +#include +#include + +#include "OffscreenUi.h" +#include "shared/QtHelpers.h" + +static auto CONTENT_WINDOW_QML = QUrl("InteractiveWindow.qml"); + +static const char* const FLAGS_PROPERTY = "flags"; +static const char* const SOURCE_PROPERTY = "source"; +static const char* const TITLE_PROPERTY = "title"; +static const char* const POSITION_PROPERTY = "position"; +static const char* const INTERACTIVE_WINDOW_POSITION_PROPERTY = "interactiveWindowPosition"; +static const char* const SIZE_PROPERTY = "size"; +static const char* const INTERACTIVE_WINDOW_SIZE_PROPERTY = "interactiveWindowSize"; +static const char* const VISIBLE_PROPERTY = "visible"; +static const char* const INTERACTIVE_WINDOW_VISIBLE_PROPERTY = "interactiveWindowVisible"; +static const char* const EVENT_BRIDGE_PROPERTY = "eventBridge"; +static const char* const PRESENTATION_MODE_PROPERTY = "presentationMode"; + +static const QStringList KNOWN_SCHEMES = QStringList() << "http" << "https" << "file" << "about" << "atp" << "qrc"; + +void registerInteractiveWindowMetaType(QScriptEngine* engine) { + qScriptRegisterMetaType(engine, interactiveWindowPointerToScriptValue, interactiveWindowPointerFromScriptValue); +} + +QScriptValue interactiveWindowPointerToScriptValue(QScriptEngine* engine, const InteractiveWindowPointer& in) { + return engine->newQObject(in, QScriptEngine::ScriptOwnership); +} + +void interactiveWindowPointerFromScriptValue(const QScriptValue& object, InteractiveWindowPointer& out) { + if (const auto interactiveWindow = qobject_cast(object.toQObject())) { + out = interactiveWindow; + } +} + +InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap& properties) { + auto offscreenUi = DependencyManager::get(); + + // Build the event bridge and wrapper on the main thread + offscreenUi->loadInNewContext(CONTENT_WINDOW_QML, [&](QQmlContext* context, QObject* object) { + _qmlWindow = object; + context->setContextProperty(EVENT_BRIDGE_PROPERTY, this); + if (properties.contains(FLAGS_PROPERTY)) { + object->setProperty(FLAGS_PROPERTY, properties[FLAGS_PROPERTY].toUInt()); + } + if (properties.contains(PRESENTATION_MODE_PROPERTY)) { + object->setProperty(PRESENTATION_MODE_PROPERTY, properties[PRESENTATION_MODE_PROPERTY].toInt()); + } + if (properties.contains(TITLE_PROPERTY)) { + object->setProperty(TITLE_PROPERTY, properties[TITLE_PROPERTY].toString()); + } + if (properties.contains(SIZE_PROPERTY)) { + const auto size = vec2FromVariant(properties[SIZE_PROPERTY]); + object->setProperty(INTERACTIVE_WINDOW_SIZE_PROPERTY, QSize(size.x, size.y)); + } + if (properties.contains(POSITION_PROPERTY)) { + const auto position = vec2FromVariant(properties[POSITION_PROPERTY]); + object->setProperty(INTERACTIVE_WINDOW_POSITION_PROPERTY, QPointF(position.x, position.y)); + } + if (properties.contains(VISIBLE_PROPERTY)) { + object->setProperty(VISIBLE_PROPERTY, properties[INTERACTIVE_WINDOW_VISIBLE_PROPERTY].toBool()); + } + + connect(object, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), Qt::QueuedConnection); + connect(object, SIGNAL(interactiveWindowPositionChanged()), this, SIGNAL(positionChanged()), Qt::QueuedConnection); + connect(object, SIGNAL(interactiveWindowSizeChanged()), this, SIGNAL(sizeChanged()), Qt::QueuedConnection); + connect(object, SIGNAL(interactiveWindowVisibleChanged()), this, SIGNAL(visibleChanged()), Qt::QueuedConnection); + connect(object, SIGNAL(presentationModeChanged()), this, SIGNAL(presentationModeChanged()), Qt::QueuedConnection); + connect(object, SIGNAL(titleChanged()), this, SIGNAL(titleChanged()), Qt::QueuedConnection); + connect(object, SIGNAL(windowClosed()), this, SIGNAL(closed()), Qt::QueuedConnection); + connect(object, SIGNAL(selfDestruct()), this, SLOT(close()), Qt::QueuedConnection); + + QUrl sourceURL{ sourceUrl }; + // If the passed URL doesn't correspond to a known scheme, assume it's a local file path + if (!KNOWN_SCHEMES.contains(sourceURL.scheme(), Qt::CaseInsensitive)) { + sourceURL = QUrl::fromLocalFile(sourceURL.toString()).toString(); + } + object->setProperty(SOURCE_PROPERTY, sourceURL); + }); +} + +InteractiveWindow::~InteractiveWindow() { + close(); +} + +void InteractiveWindow::sendToQml(const QVariant& message) { + // Forward messages received from the script on to QML + QMetaObject::invokeMethod(_qmlWindow, "fromScript", Qt::QueuedConnection, Q_ARG(QVariant, message)); +} + +void InteractiveWindow::emitScriptEvent(const QVariant& scriptMessage) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, scriptMessage)); + } else { + emit scriptEventReceived(scriptMessage); + } +} + +void InteractiveWindow::emitWebEvent(const QVariant& webMessage) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "emitWebEvent", Qt::QueuedConnection, Q_ARG(QVariant, webMessage)); + } else { + emit webEventReceived(webMessage); + } +} + +void InteractiveWindow::close() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "close"); + return; + } + + if (_qmlWindow) { + _qmlWindow->deleteLater(); + } + _qmlWindow = nullptr; +} + +void InteractiveWindow::show() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "show"); + return; + } + + if (_qmlWindow) { + QMetaObject::invokeMethod(_qmlWindow, "show", Qt::DirectConnection); + } +} + +void InteractiveWindow::raise() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "raise"); + return; + } + + if (_qmlWindow) { + QMetaObject::invokeMethod(_qmlWindow, "raiseWindow", Qt::DirectConnection); + } +} + +void InteractiveWindow::qmlToScript(const QVariant& message) { + if (message.canConvert()) { + emit fromQml(qvariant_cast(message).toVariant()); + } else if (message.canConvert()) { + emit fromQml(message.toString()); + } else { + qWarning() << "Unsupported message type " << message; + } +} + +void InteractiveWindow::setVisible(bool visible) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setVisible", Q_ARG(bool, visible)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(INTERACTIVE_WINDOW_VISIBLE_PROPERTY, visible); + } +} + +bool InteractiveWindow::isVisible() const { + if (QThread::currentThread() != thread()) { + bool result = false; + BLOCKING_INVOKE_METHOD(const_cast(this), "isVisible", Q_RETURN_ARG(bool, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return false; + } + + return _qmlWindow->property(INTERACTIVE_WINDOW_VISIBLE_PROPERTY).toBool(); +} + +glm::vec2 InteractiveWindow::getPosition() const { + if (QThread::currentThread() != thread()) { + glm::vec2 result; + BLOCKING_INVOKE_METHOD(const_cast(this), "getPosition", Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return {}; + } + + return toGlm(_qmlWindow->property(INTERACTIVE_WINDOW_POSITION_PROPERTY).toPointF()); +} + +void InteractiveWindow::setPosition(const glm::vec2& position) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPosition", Q_ARG(const glm::vec2&, position)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(INTERACTIVE_WINDOW_POSITION_PROPERTY, QPointF(position.x, position.y)); + QMetaObject::invokeMethod(_qmlWindow, "updateInteractiveWindowPositionForMode", Qt::DirectConnection); + } +} + +glm::vec2 InteractiveWindow::getSize() const { + if (QThread::currentThread() != thread()) { + glm::vec2 result; + BLOCKING_INVOKE_METHOD(const_cast(this), "getSize", Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return {}; + } + return toGlm(_qmlWindow->property(INTERACTIVE_WINDOW_SIZE_PROPERTY).toSize()); +} + +void InteractiveWindow::setSize(const glm::vec2& size) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setSize", Q_ARG(const glm::vec2&, size)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(INTERACTIVE_WINDOW_SIZE_PROPERTY, QSize(size.x, size.y)); + QMetaObject::invokeMethod(_qmlWindow, "updateInteractiveWindowSizeForMode", Qt::DirectConnection); + } +} + +QString InteractiveWindow::getTitle() const { + if (QThread::currentThread() != thread()) { + QString result; + BLOCKING_INVOKE_METHOD(const_cast(this), "getTitle", Q_RETURN_ARG(QString, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return QString(); + } + return _qmlWindow->property(TITLE_PROPERTY).toString(); +} + +void InteractiveWindow::setTitle(const QString& title) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setTitle", Q_ARG(const QString&, title)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(TITLE_PROPERTY, title); + } +} + +int InteractiveWindow::getPresentationMode() const { + if (QThread::currentThread() != thread()) { + int result; + BLOCKING_INVOKE_METHOD(const_cast(this), "getPresentationMode", + Q_RETURN_ARG(int, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return Virtual; + } + return _qmlWindow->property(PRESENTATION_MODE_PROPERTY).toInt(); +} + +void InteractiveWindow::setPresentationMode(int presentationMode) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPresentationMode", Q_ARG(int, presentationMode)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(PRESENTATION_MODE_PROPERTY, presentationMode); + } +} diff --git a/libraries/ui/src/InteractiveWindow.h b/libraries/ui/src/InteractiveWindow.h new file mode 100644 index 0000000000..bf832550b5 --- /dev/null +++ b/libraries/ui/src/InteractiveWindow.h @@ -0,0 +1,206 @@ +// +// InteractiveWindow.h +// libraries/ui/src +// +// Created by Thijs Wenker on 2018-06-25 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#ifndef hifi_InteractiveWindow_h +#define hifi_InteractiveWindow_h + +#include +#include +#include +#include + +#include +#include + +namespace InteractiveWindowEnums { + Q_NAMESPACE + + enum InteractiveWindowFlags : uint8_t { + AlwaysOnTop = 1 << 0, + CloseButtonHides = 1 << 1 + }; + Q_ENUM_NS(InteractiveWindowFlags); + + enum InteractiveWindowPresentationMode { + Virtual, + Native + }; + Q_ENUM_NS(InteractiveWindowPresentationMode); +} + +using namespace InteractiveWindowEnums; + +/**jsdoc + * @class InteractiveWindow + * + * @hifi-interface + * @hifi-client-en + * + * @property {string} title + * @property {Vec2} position + * @property {Vec2} size + * @property {boolean} visible + * @property {Desktop.PresentationMode} presentationMode + * + */ +class InteractiveWindow : public QObject { + Q_OBJECT + + Q_PROPERTY(QString title READ getTitle WRITE setTitle) + Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition) + Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize) + Q_PROPERTY(bool visible READ isVisible WRITE setVisible) + Q_PROPERTY(int presentationMode READ getPresentationMode WRITE setPresentationMode) + +public: + InteractiveWindow(const QString& sourceUrl, const QVariantMap& properties); + + ~InteractiveWindow(); + +private: + // define property getters and setters as private to not expose them to the JS API + Q_INVOKABLE QString getTitle() const; + Q_INVOKABLE void setTitle(const QString& title); + + Q_INVOKABLE glm::vec2 getPosition() const; + Q_INVOKABLE void setPosition(const glm::vec2& position); + + Q_INVOKABLE glm::vec2 getSize() const; + Q_INVOKABLE void setSize(const glm::vec2& size); + + Q_INVOKABLE void setVisible(bool visible); + Q_INVOKABLE bool isVisible() const; + + Q_INVOKABLE void setPresentationMode(int presentationMode); + Q_INVOKABLE int getPresentationMode() const; + +public slots: + + /**jsdoc + * @function InteractiveWindow.sendToQml + * @param {object} message + */ + // Scripts can use this to send a message to the QML object + void sendToQml(const QVariant& message); + + /**jsdoc + * @function InteractiveWindow.emitScriptEvent + * @param {object} message + */ + // QmlWindow content may include WebView requiring EventBridge. + void emitScriptEvent(const QVariant& scriptMessage); + + /**jsdoc + * @function InteractiveWindow.emitWebEvent + * @param {object} message + */ + void emitWebEvent(const QVariant& webMessage); + + /**jsdoc + * @function InteractiveWindow.close + */ + Q_INVOKABLE void close(); + + /**jsdoc + * @function InteractiveWindow.show + */ + Q_INVOKABLE void show(); + + /**jsdoc + * @function InteractiveWindow.raise + */ + Q_INVOKABLE void raise(); + +signals: + + /**jsdoc + * @function InteractiveWindow.visibleChanged + * @returns {Signal} + */ + void visibleChanged(); + + /**jsdoc + * @function InteractiveWindow.positionChanged + * @returns {Signal} + */ + void positionChanged(); + + /**jsdoc + * @function InteractiveWindow.sizeChanged + * @returns {Signal} + */ + void sizeChanged(); + + /**jsdoc + * @function InteractiveWindow.presentationModeChanged + * @returns {Signal} + */ + void presentationModeChanged(); + + /**jsdoc + * @function InteractiveWindow.titleChanged + * @returns {Signal} + */ + void titleChanged(); + + /**jsdoc + * @function InteractiveWindow.closed + * @returns {Signal} + */ + void closed(); + + /**jsdoc + * @function InteractiveWindow.fromQml + * @param {object} message + * @returns {Signal} + */ + // Scripts can connect to this signal to receive messages from the QML object + void fromQml(const QVariant& message); + + /**jsdoc + * @function InteractiveWindow.scriptEventReceived + * @param {object} message + * @returns {Signal} + */ + // InteractiveWindow content may include WebView requiring EventBridge. + void scriptEventReceived(const QVariant& message); + + /**jsdoc + * @function InteractiveWindow.webEventReceived + * @param {object} message + * @returns {Signal} + */ + void webEventReceived(const QVariant& message); + +protected slots: + /**jsdoc + * @function InteractiveWindow.qmlToScript + * @param {object} message + * @returns {Signal} + */ + void qmlToScript(const QVariant& message); + +private: + QPointer _qmlWindow; +}; + +typedef InteractiveWindow* InteractiveWindowPointer; + +QScriptValue interactiveWindowPointerToScriptValue(QScriptEngine* engine, const InteractiveWindowPointer& in); +void interactiveWindowPointerFromScriptValue(const QScriptValue& object, InteractiveWindowPointer& out); + +void registerInteractiveWindowMetaType(QScriptEngine* engine); + +Q_DECLARE_METATYPE(InteractiveWindowPointer) + +#endif // hifi_InteractiveWindow_h diff --git a/libraries/ui/src/Tooltip.cpp b/libraries/ui/src/Tooltip.cpp index c0c015e72f..bd2c4e6d8f 100644 --- a/libraries/ui/src/Tooltip.cpp +++ b/libraries/ui/src/Tooltip.cpp @@ -83,7 +83,7 @@ void Tooltip::requestHyperlinkImage() { auto accountManager = DependencyManager::get(); JSONCallbackParameters callbackParams; - callbackParams.jsonCallbackReceiver = this; + callbackParams.callbackReceiver = this; callbackParams.jsonCallbackMethod = "handleAPIResponse"; accountManager->sendRequest(GET_PLACE.arg(_title), @@ -94,9 +94,9 @@ void Tooltip::requestHyperlinkImage() { } } -void Tooltip::handleAPIResponse(QNetworkReply& requestReply) { +void Tooltip::handleAPIResponse(QNetworkReply* requestReply) { // did a preview image come back? - QJsonObject responseObject = QJsonDocument::fromJson(requestReply.readAll()).object(); + QJsonObject responseObject = QJsonDocument::fromJson(requestReply->readAll()).object(); QJsonObject dataObject = responseObject["data"].toObject(); const QString PLACE_KEY = "place"; diff --git a/libraries/ui/src/Tooltip.h b/libraries/ui/src/Tooltip.h index 5e884a7aea..b1bf7b7f3e 100644 --- a/libraries/ui/src/Tooltip.h +++ b/libraries/ui/src/Tooltip.h @@ -49,7 +49,7 @@ signals: void imageURLChanged(); private slots: - void handleAPIResponse(QNetworkReply& requestReply); + void handleAPIResponse(QNetworkReply* requestReply); private: void requestHyperlinkImage(); diff --git a/libraries/workload/src/workload/Space.cpp b/libraries/workload/src/workload/Space.cpp index 27a8639f3a..747df5f6c4 100644 --- a/libraries/workload/src/workload/Space.cpp +++ b/libraries/workload/src/workload/Space.cpp @@ -44,6 +44,11 @@ void Space::processResets(const Transaction::Resets& transactions) { for (auto& reset : transactions) { // Access the true item auto proxyID = std::get<0>(reset); + + // Guard against proxyID being past the end of the list. + if (!_IDAllocator.checkIndex(proxyID)) { + continue; + } auto& item = _proxies[proxyID]; // Reset the item with a new payload @@ -56,6 +61,9 @@ void Space::processResets(const Transaction::Resets& transactions) { void Space::processRemoves(const Transaction::Removes& transactions) { for (auto removedID : transactions) { + if (!_IDAllocator.checkIndex(removedID)) { + continue; + } _IDAllocator.freeIndex(removedID); // Access the true item @@ -70,7 +78,7 @@ void Space::processRemoves(const Transaction::Removes& transactions) { void Space::processUpdates(const Transaction::Updates& transactions) { for (auto& update : transactions) { auto updateID = std::get<0>(update); - if (updateID == INVALID_PROXY_ID) { + if (!_IDAllocator.checkIndex(updateID)) { continue; } @@ -136,6 +144,7 @@ uint8_t Space::getRegion(int32_t proxyID) const { } void Space::clear() { + Collection::clear(); std::unique_lock lock(_proxiesMutex); _IDAllocator.clear(); _proxies.clear(); diff --git a/libraries/workload/src/workload/Space.h b/libraries/workload/src/workload/Space.h index 960c905f7c..310955f4c6 100644 --- a/libraries/workload/src/workload/Space.h +++ b/libraries/workload/src/workload/Space.h @@ -42,7 +42,7 @@ public: uint32_t getNumViews() const { return (uint32_t)(_views.size()); } void copyViews(std::vector& copy) const; - uint32_t getNumObjects() const { return _IDAllocator.getNumLiveIndices(); } // (uint32_t)(_proxies.size() - _freeIndices.size()); } + uint32_t getNumObjects() const { return _IDAllocator.getNumLiveIndices(); } uint32_t getNumAllocatedProxies() const { return (uint32_t)(_IDAllocator.getNumAllocatedIndices()); } void categorizeAndGetChanges(std::vector& changes); @@ -51,7 +51,7 @@ public: const Owner getOwner(int32_t proxyID) const; uint8_t getRegion(int32_t proxyID) const; - void clear(); + void clear() override; private: void processTransactionFrame(const Transaction& transaction) override; diff --git a/libraries/workload/src/workload/Transaction.cpp b/libraries/workload/src/workload/Transaction.cpp index 31c28cdf22..14984e9c77 100644 --- a/libraries/workload/src/workload/Transaction.cpp +++ b/libraries/workload/src/workload/Transaction.cpp @@ -111,6 +111,12 @@ Collection::Collection() { Collection::~Collection() { } +void Collection::clear() { + std::unique_lock lock(_transactionQueueMutex); + _transactionQueue.clear(); + _transactionFrames.clear(); +} + ProxyID Collection::allocateID() { // Just increment and return the previous value initialized at 0 return _IDAllocator.allocateIndex(); diff --git a/libraries/workload/src/workload/Transaction.h b/libraries/workload/src/workload/Transaction.h index aa2a427127..22328cf4b1 100644 --- a/libraries/workload/src/workload/Transaction.h +++ b/libraries/workload/src/workload/Transaction.h @@ -137,6 +137,8 @@ public: Collection(); ~Collection(); + virtual void clear(); + // This call is thread safe, can be called from anywhere to allocate a new ID ProxyID allocateID(); diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 635d9d0529..0deb4c75e7 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -48,7 +48,6 @@ const quint64 CALIBRATION_TIMELAPSE = 1 * USECS_PER_SECOND; static const char* MENU_PARENT = "Avatar"; static const char* MENU_NAME = "Vive Controllers"; static const char* MENU_PATH = "Avatar" ">" "Vive Controllers"; -static const char* RENDER_CONTROLLERS = "Render Hand Controllers"; static const int MIN_HEAD = 1; static const int MIN_PUCK_COUNT = 2; @@ -205,11 +204,6 @@ bool ViveControllerManager::activate() { return false; } - _container->addMenu(MENU_PATH); - _container->addMenuItem(PluginType::INPUT_PLUGIN, MENU_PATH, RENDER_CONTROLLERS, - [this](bool clicked) { this->setRenderControllers(clicked); }, - true, true); - enableOpenVrKeyboard(_container); // register with UserInputMapper @@ -224,9 +218,6 @@ void ViveControllerManager::deactivate() { disableOpenVrKeyboard(); - _container->removeMenuItem(MENU_NAME, RENDER_CONTROLLERS); - _container->removeMenu(MENU_PATH); - if (_system) { _container->makeRenderingContextCurrent(); releaseOpenVrSystem(); diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index f3631ece9d..30f8590062 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -57,8 +57,6 @@ public: void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; - void setRenderControllers(bool renderControllers) { _renderControllers = renderControllers; } - virtual void saveSettings() const override; virtual void loadSettings() override; @@ -219,7 +217,6 @@ private: int _leftHandRenderID { 0 }; int _rightHandRenderID { 0 }; - bool _renderControllers { false }; vr::IVRSystem* _system { nullptr }; std::shared_ptr _inputDevice { std::make_shared(_system) }; diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index ddbeaaeea9..b275660c0f 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -21,6 +21,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/bubble.js", "system/snapshot.js", "system/pal.js", // "system/mod.js", // older UX, if you prefer + "system/avatarapp.js", "system/makeUserConnection.js", "system/tablet-goto.js", "system/marketplaces/marketplaces.js", diff --git a/scripts/developer/tests/interactiveWindowTest.js b/scripts/developer/tests/interactiveWindowTest.js new file mode 100644 index 0000000000..c17deba617 --- /dev/null +++ b/scripts/developer/tests/interactiveWindowTest.js @@ -0,0 +1,34 @@ +// +// interactiveWindowTest.js +// +// Created by Thijs Wenker on 2018-07-03 +// Copyright 2018 High Fidelity, Inc. +// +// An example of an interactive window that toggles presentation mode when toggling HMD on/off +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +function getPreferredPresentationMode() { + return HMD.active ? Desktop.PresentationMode.VIRTUAL : Desktop.PresentationMode.NATIVE; +} + +function getPreferredTitle() { + return HMD.active ? 'Virtual Desktop Window' : 'Native Desktop Window'; +} + +var virtualWindow = Desktop.createWindow(Script.resourcesPath() + 'qml/OverlayWindowTest.qml', { + title: getPreferredTitle(), + flags: Desktop.ALWAYS_ON_TOP, + presentationMode: getPreferredPresentationMode(), + size: {x: 500, y: 400} +}); + +HMD.displayModeChanged.connect(function() { + virtualWindow.presentationMode = getPreferredPresentationMode(); + virtualWindow.title = getPreferredTitle(); +}); + +Script.scriptEnding.connect(function() { + virtualWindow.close(); +}); diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js new file mode 100644 index 0000000000..db81af3755 --- /dev/null +++ b/scripts/modules/appUi.js @@ -0,0 +1,187 @@ +"use strict"; +/*global Tablet, Script*/ +// +// libraries/appUi.js +// +// Created by Howard Stearns on 3/20/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +function AppUi(properties) { + /* Example development order: + 1. var AppUi = Script.require('appUi'); + 2. Put appname-i.svg, appname-a.svg in graphicsDirectory (where non-default graphicsDirectory can be added in #3). + 3. ui = new AppUi({buttonName: "APPNAME", home: "qml-or-html-path"}); + (And if converting an existing app, + define var tablet = ui.tablet, button = ui.button; as needed. + remove button.clicked.[dis]connect and tablet.remove(button).) + 4. Define onOpened and onClosed behavior in #3, if any. + (And if converting an existing app, remove screenChanged.[dis]connect.) + 5. Define onMessage and sendMessage in #3, if any. onMessage is wired/unwired on open/close. If you + want a handler to be "always on", connect it yourself at script startup. + (And if converting an existing app, remove code that [un]wires that message handling such as + fromQml/sendToQml or webEventReceived/emitScriptEvent.) + 6. (If converting an existing app, cleanup stuff that is no longer necessary, like references to button, tablet, + and use isOpen, open(), and close() as needed.) + 7. lint! + */ + var that = this; + function defaultButton(name, suffix) { + var base = that[name] || (that.buttonPrefix + suffix); + that[name] = (base.indexOf('/') >= 0) ? base : (that.graphicsDirectory + base); //poor man's merge + } + + // Defaults: + that.tabletName = "com.highfidelity.interface.tablet.system"; + that.inject = ""; + that.graphicsDirectory = "icons/tablet-icons/"; // Where to look for button svgs. See below. + that.checkIsOpen = function checkIsOpen(type, tabletUrl) { // Are we active? Value used to set isOpen. + return (type === that.type) && that.currentUrl && (tabletUrl.indexOf(that.currentUrl) >= 0); // Actual url may have prefix or suffix. + }; + that.setCurrentData = function setCurrentData(url) { + that.currentUrl = url; + that.type = /.qml$/.test(url) ? 'QML' : 'Web'; + } + that.open = function open(optionalUrl) { // How to open the app. + var url = optionalUrl || that.home; + that.setCurrentData(url); + if (that.isQML()) { + that.tablet.loadQMLSource(url); + } else { + that.tablet.gotoWebScreen(url, that.inject); + } + }; + that.close = function close() { // How to close the app. + that.currentUrl = ""; + // for toolbar-mode: go back to home screen, this will close the window. + that.tablet.gotoHomeScreen(); + }; + that.buttonActive = function buttonActive(isActive) { // How to make the button active (white). + that.button.editProperties({isActive: isActive}); + }; + that.messagesWaiting = function messagesWaiting(isWaiting) { // How to indicate a message light on button. + // Note that waitingButton doesn't have to exist unless someone explicitly calls this with isWaiting true. + that.button.editProperties({ + icon: isWaiting ? that.normalMessagesButton : that.normalButton, + activeIcon: isWaiting ? that.activeMessagesButton : that.activeButton + }); + }; + that.isQML = function isQML() { // We set type property in onClick. + return that.type === 'QML'; + }; + that.eventSignal = function eventSignal() { // What signal to hook onMessage to. + return that.isQML() ? that.tablet.fromQml : that.tablet.webEventReceived; + }; + + // Overwrite with the given properties: + Object.keys(properties).forEach(function (key) { that[key] = properties[key]; }); + + // Properties: + that.tablet = Tablet.getTablet(that.tabletName); + // Must be after we gather properties. + that.buttonPrefix = that.buttonPrefix || that.buttonName.toLowerCase() + "-"; + defaultButton('normalButton', 'i.svg'); + defaultButton('activeButton', 'a.svg'); + defaultButton('normalMessagesButton', 'i-msg.svg'); + defaultButton('activeMessagesButton', 'a-msg.svg'); + that.button = that.tablet.addButton({ + icon: that.normalButton, + activeIcon: that.activeButton, + text: that.buttonName, + sortOrder: that.sortOrder + }); + that.ignore = function ignore() { }; + + // Handlers + that.onScreenChanged = function onScreenChanged(type, url) { + // Set isOpen, wireEventBridge, set buttonActive as appropriate, + // and finally call onOpened() or onClosed() IFF defined. + console.debug(that.buttonName, 'onScreenChanged', type, url, that.isOpen); + if (that.checkIsOpen(type, url)) { + if (!that.isOpen) { + that.wireEventBridge(true); + that.buttonActive(true); + if (that.onOpened) { + that.onOpened(); + } + that.isOpen = true; + } + + } else { // Not us. Should we do something for type Home, Menu, and particularly Closed (meaning tablet hidden? + if (that.isOpen) { + that.wireEventBridge(false); + that.buttonActive(false); + if (that.onClosed) { + that.onClosed(); + } + that.isOpen = false; + } + } + }; + that.hasEventBridge = false; + // HTML event bridge uses strings, not objects. Here we abstract over that. + // (Although injected javascript still has to use JSON.stringify/JSON.parse.) + that.sendToHtml = function (messageObject) { that.tablet.emitScriptEvent(JSON.stringify(messageObject)); }; + that.fromHtml = function (messageString) { that.onMessage(JSON.parse(messageString)); }; + that.wireEventBridge = function wireEventBridge(on) { + // Uniquivocally sets that.sendMessage(messageObject) to do the right thing. + // Sets hasEventBridge and wires onMessage to eventSignal as appropriate, IFF onMessage defined. + var handler, isQml = that.isQML(); + // Outbound (always, regardless of whether there is an inbound handler). + if (on) { + that.sendMessage = isQml ? that.tablet.sendToQml : that.sendToHtml; + } else { + that.sendMessage = that.ignore; + } + + if (!that.onMessage) { return; } + + // Inbound + handler = isQml ? that.onMessage : that.fromHtml; + if (on) { + if (!that.hasEventBridge) { + console.debug(that.buttonName, 'connecting', that.eventSignal()); + that.eventSignal().connect(handler); + that.hasEventBridge = true; + } + } else { + if (that.hasEventBridge) { + console.debug(that.buttonName, 'disconnecting', that.eventSignal()); + that.eventSignal().disconnect(handler); + that.hasEventBridge = false; + } + } + }; + that.isOpen = false; + // To facilitate incremental development, only wire onClicked to do something when "home" is defined in properties. + that.onClicked = that.home + ? function onClicked() { + // Call open() or close(), and reset type based on current home property. + if (that.isOpen) { + that.close(); + } else { + that.open(); + } + } : that.ignore; + that.onScriptEnding = function onScriptEnding() { + // Close if necessary, clean up any remaining handlers, and remove the button. + if (that.isOpen) { + that.close(); + } + that.tablet.screenChanged.disconnect(that.onScreenChanged); + if (that.button) { + if (that.onClicked) { + that.button.clicked.disconnect(that.onClicked); + } + that.tablet.removeButton(that.button); + } + }; + // Set up the handlers. + that.tablet.screenChanged.connect(that.onScreenChanged); + that.button.clicked.connect(that.onClicked); + Script.scriptEnding.connect(that.onScriptEnding); +} +module.exports = AppUi; diff --git a/scripts/system/+android/clickWeb.js b/scripts/system/+android/clickWeb.js new file mode 100644 index 0000000000..dc75a58327 --- /dev/null +++ b/scripts/system/+android/clickWeb.js @@ -0,0 +1,106 @@ +"use strict"; +// +// clickWeb.js +// scripts/system/+android +// +// Created by Gabriel Calero & Cristian Duarte on Jun 22, 2018 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { // BEGIN LOCAL_SCOPE + +var logEnabled = false; +var touchOverlayID; +var touchEntityID; + +function printd(str) { + if (logEnabled) + print("[clickWeb.js] " + str); +} + +function intersectsWebOverlay(intersection) { + return intersection && intersection.intersects && intersection.overlayID && + Overlays.getOverlayType(intersection.overlayID) == "web3d"; +} + +function intersectsWebEntity(intersection) { + if (intersection && intersection.intersects && intersection.entityID) { + var properties = Entities.getEntityProperties(intersection.entityID, ["type", "sourceUrl"]); + return properties.type && properties.type == "Web" && properties.sourceUrl; + } + return false; +} + +function findRayIntersection(pickRay) { + // Check 3D overlays and entities. Argument is an object with origin and direction. + var overlayRayIntersection = Overlays.findRayIntersection(pickRay); + var entityRayIntersection = Entities.findRayIntersection(pickRay, true); + var isOverlayInters = intersectsWebOverlay(overlayRayIntersection); + var isEntityInters = intersectsWebEntity(entityRayIntersection); + if (isOverlayInters && + (!isEntityInters || + overlayRayIntersection.distance < entityRayIntersection.distance)) { + return { type: 'overlay', obj: overlayRayIntersection }; + } else if (isEntityInters && + (!isOverlayInters || + entityRayIntersection.distance < overlayRayIntersection.distance)) { + return { type: 'entity', obj: entityRayIntersection }; + } + return false; +} + +function touchBegin(event) { + var intersection = findRayIntersection(Camera.computePickRay(event.x, event.y)); + if (intersection && intersection.type == 'overlay') { + touchOverlayID = intersection.obj.overlayID; + touchEntityID = null; + } else if (intersection && intersection.type == 'entity') { + touchEntityID = intersection.obj.entityID; + touchOverlayID = null; + } +} + +function touchEnd(event) { + var intersection = findRayIntersection(Camera.computePickRay(event.x, event.y)); + if (intersection && intersection.type == 'overlay' && touchOverlayID == intersection.obj.overlayID) { + var propertiesToGet = {}; + propertiesToGet[overlayID] = ['url']; + var properties = Overlays.getOverlaysProperties(propertiesToGet); + if (properties[overlayID].url) { + Window.openUrl(properties[overlayID].url); + } + } else if (intersection && intersection.type == 'entity' && touchEntityID == intersection.obj.entityID) { + var properties = Entities.getEntityProperties(touchEntityID, ["sourceUrl"]); + if (properties.sourceUrl) { + Window.openUrl(properties.sourceUrl); + } + } + + touchOverlayID = null; + touchEntityID = null; +} + +function ending() { + Controller.touchBeginEvent.disconnect(touchBegin); + Controller.touchEndEvent.disconnect(touchEnd); +} + +function init() { + Controller.touchBeginEvent.connect(touchBegin); + Controller.touchEndEvent.connect(touchEnd); + + Script.scriptEnding.connect(function () { + ending(); + }); + +} + +module.exports = { + init: init, + ending: ending +} + +}()); // END LOCAL_SCOPE diff --git a/scripts/system/+android/modes.js b/scripts/system/+android/modes.js index f0dfb64677..f495af3bba 100644 --- a/scripts/system/+android/modes.js +++ b/scripts/system/+android/modes.js @@ -29,6 +29,7 @@ var logEnabled = false; var radar = Script.require('./radar.js'); var uniqueColor = Script.require('./uniqueColor.js'); var displayNames = Script.require('./displayNames.js'); +var clickWeb = Script.require('./clickWeb.js'); function printd(str) { if (logEnabled) { @@ -97,9 +98,11 @@ function switchToMode(newMode) { if (currentMode == MODE_RADAR) { radar.startRadarMode(); displayNames.ending(); + clickWeb.ending(); } else if (currentMode == MODE_MY_VIEW) { // nothing to do yet displayNames.init(); + clickWeb.init(); } else { printd("Unknown view mode " + currentMode); } diff --git a/scripts/system/assets/animations/Cheer.fbx b/scripts/system/assets/animations/Cheering.fbx similarity index 100% rename from scripts/system/assets/animations/Cheer.fbx rename to scripts/system/assets/animations/Cheering.fbx diff --git a/scripts/system/assets/animations/Clap.fbx b/scripts/system/assets/animations/Clapping.fbx similarity index 100% rename from scripts/system/assets/animations/Clap.fbx rename to scripts/system/assets/animations/Clapping.fbx diff --git a/scripts/system/assets/animations/Cry.fbx b/scripts/system/assets/animations/Crying.fbx similarity index 67% rename from scripts/system/assets/animations/Cry.fbx rename to scripts/system/assets/animations/Crying.fbx index 7ecd6415a5..2e60ba2450 100644 Binary files a/scripts/system/assets/animations/Cry.fbx and b/scripts/system/assets/animations/Crying.fbx differ diff --git a/scripts/system/assets/animations/Dance.fbx b/scripts/system/assets/animations/Dancing.fbx similarity index 100% rename from scripts/system/assets/animations/Dance.fbx rename to scripts/system/assets/animations/Dancing.fbx diff --git a/scripts/system/assets/animations/Point.fbx b/scripts/system/assets/animations/Pointing.fbx similarity index 100% rename from scripts/system/assets/animations/Point.fbx rename to scripts/system/assets/animations/Pointing.fbx diff --git a/scripts/system/assets/animations/Wave.fbx b/scripts/system/assets/animations/Waving.fbx similarity index 100% rename from scripts/system/assets/animations/Wave.fbx rename to scripts/system/assets/animations/Waving.fbx diff --git a/scripts/system/assets/models/teleport-destination.fbm/Teleportation-Destination-Texture2.png b/scripts/system/assets/models/teleport-destination.fbm/Teleportation-Destination-Texture2.png new file mode 100644 index 0000000000..eb9addcfca Binary files /dev/null and b/scripts/system/assets/models/teleport-destination.fbm/Teleportation-Destination-Texture2.png differ diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js new file mode 100644 index 0000000000..f692128fa3 --- /dev/null +++ b/scripts/system/avatarapp.js @@ -0,0 +1,553 @@ +"use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// avatarapp.js +// +// Created by Alexander Ivash on April 30, 2018 +// Copyright 2016 High Fidelity, Inc +// +// Distributed under the Apache License, Version 2.0 +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { // BEGIN LOCAL_SCOPE + +var request = Script.require('request').request; +var AVATARAPP_QML_SOURCE = "hifi/AvatarApp.qml"; +Script.include("/~/system/libraries/controllers.js"); + +// constants from AvatarBookmarks.h +var ENTRY_AVATAR_URL = "avatarUrl"; +var ENTRY_AVATAR_ATTACHMENTS = "attachments"; +var ENTRY_AVATAR_ENTITIES = "avatarEntites"; +var ENTRY_AVATAR_SCALE = "avatarScale"; +var ENTRY_VERSION = "version"; + +function executeLater(callback) { + Script.setTimeout(callback, 300); +} + +function getMyAvatarWearables() { + var wearablesArray = MyAvatar.getAvatarEntitiesVariant(); + + for(var i = 0; i < wearablesArray.length; ++i) { + var wearable = wearablesArray[i]; + var localRotation = wearable.properties.localRotation; + wearable.properties.localRotationAngles = Quat.safeEulerAngles(localRotation) + } + + return wearablesArray; +} + +function getMyAvatar() { + var avatar = {} + avatar[ENTRY_AVATAR_URL] = MyAvatar.skeletonModelURL; + avatar[ENTRY_AVATAR_SCALE] = MyAvatar.getAvatarScale(); + avatar[ENTRY_AVATAR_ATTACHMENTS] = MyAvatar.getAttachmentsVariant(); + avatar[ENTRY_AVATAR_ENTITIES] = getMyAvatarWearables(); + return avatar; +} + +function getMyAvatarSettings() { + return { + dominantHand: MyAvatar.getDominantHand(), + collisionsEnabled : MyAvatar.getCollisionsEnabled(), + collisionSoundUrl : MyAvatar.collisionSoundURL, + animGraphUrl : MyAvatar.getAnimGraphUrl(), + } +} + +function updateAvatarWearables(avatar, bookmarkAvatarName) { + executeLater(function() { + var wearables = getMyAvatarWearables(); + avatar[ENTRY_AVATAR_ENTITIES] = wearables; + + sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables, 'avatarName' : bookmarkAvatarName}) + }); +} + +var adjustWearables = { + opened : false, + cameraMode : '', + setOpened : function(value) { + if(this.opened !== value) { + if(value) { + this.cameraMode = Camera.mode; + + if(!HMD.active) { + Camera.mode = 'mirror'; + } + } else { + Camera.mode = this.cameraMode; + } + + this.opened = value; + } + } +} + +var currentAvatarWearablesBackup = null; +var currentAvatar = null; +var currentAvatarSettings = getMyAvatarSettings(); + +var notifyScaleChanged = true; +function onTargetScaleChanged() { + if(currentAvatar.scale !== MyAvatar.getAvatarScale()) { + currentAvatar.scale = MyAvatar.getAvatarScale(); + if(notifyScaleChanged) { + sendToQml({'method' : 'scaleChanged', 'value' : currentAvatar.scale}) + } + } +} + +function onSkeletonModelURLChanged() { + if(currentAvatar || (currentAvatar.skeletonModelURL !== MyAvatar.skeletonModelURL)) { + fromQml({'method' : 'getAvatars'}); + } +} + +function onDominantHandChanged(dominantHand) { + if(currentAvatarSettings.dominantHand !== dominantHand) { + currentAvatarSettings.dominantHand = dominantHand; + sendToQml({'method' : 'settingChanged', 'name' : 'dominantHand', 'value' : dominantHand}) + } +} + +function onCollisionsEnabledChanged(enabled) { + if(currentAvatarSettings.collisionsEnabled !== enabled) { + currentAvatarSettings.collisionsEnabled = enabled; + sendToQml({'method' : 'settingChanged', 'name' : 'collisionsEnabled', 'value' : enabled}) + } +} + +function onNewCollisionSoundUrl(url) { + if(currentAvatarSettings.collisionSoundUrl !== url) { + currentAvatarSettings.collisionSoundUrl = url; + sendToQml({'method' : 'settingChanged', 'name' : 'collisionSoundUrl', 'value' : url}) + } +} + +function onAnimGraphUrlChanged(url) { + if(currentAvatarSettings.animGraphUrl !== url) { + currentAvatarSettings.animGraphUrl = url; + sendToQml({'method' : 'settingChanged', 'name' : 'animGraphUrl', 'value' : url}) + } +} + +var selectedAvatarEntityGrabbable = false; +var selectedAvatarEntityID = null; +var grabbedAvatarEntityChangeNotifier = null; + +var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; +var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("html/js/marketplacesInject.js"); + +function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. + switch (message.method) { + case 'getAvatars': + currentAvatar = getMyAvatar(); + currentAvatarSettings = getMyAvatarSettings(); + + message.data = { + 'bookmarks' : AvatarBookmarks.getBookmarks(), + 'displayName' : MyAvatar.displayName, + 'currentAvatar' : currentAvatar, + 'currentAvatarSettings' : currentAvatarSettings + }; + + for(var bookmarkName in message.data.bookmarks) { + var bookmark = message.data.bookmarks[bookmarkName]; + if (!bookmark.avatarEntites) { // ensure avatarEntites always exist + bookmark.avatarEntites = []; + } + + bookmark.avatarEntites.forEach(function(avatarEntity) { + avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation) + }) + } + + sendToQml(message) + break; + case 'selectAvatar': + AvatarBookmarks.loadBookmark(message.name); + break; + case 'deleteAvatar': + AvatarBookmarks.removeBookmark(message.name); + break; + case 'addAvatar': + AvatarBookmarks.addBookmark(message.name); + break; + case 'adjustWearable': + if(message.properties.localRotationAngles) { + message.properties.localRotation = Quat.fromVec3Degrees(message.properties.localRotationAngles) + } + + Entities.editEntity(message.entityID, message.properties); + message.properties = Entities.getEntityProperties(message.entityID, Object.keys(message.properties)); + + if(message.properties.localRotation) { + message.properties.localRotationAngles = Quat.safeEulerAngles(message.properties.localRotation); + } + + sendToQml({'method' : 'wearableUpdated', 'entityID' : message.entityID, wearableIndex : message.wearableIndex, properties : message.properties, updateUI : false}) + break; + case 'adjustWearablesOpened': + currentAvatarWearablesBackup = getMyAvatarWearables(); + adjustWearables.setOpened(true); + + Entities.mousePressOnEntity.connect(onSelectedEntity); + Messages.subscribe('Hifi-Object-Manipulation'); + Messages.messageReceived.connect(handleWearableMessages); + break; + case 'adjustWearablesClosed': + if(!message.save) { + // revert changes using snapshot of wearables + if(currentAvatarWearablesBackup !== null) { + AvatarBookmarks.updateAvatarEntities(currentAvatarWearablesBackup); + updateAvatarWearables(currentAvatar, message.avatarName); + } + } else { + sendToQml({'method' : 'updateAvatarInBookmarks'}); + } + + adjustWearables.setOpened(false); + ensureWearableSelected(null); + Entities.mousePressOnEntity.disconnect(onSelectedEntity); + Messages.messageReceived.disconnect(handleWearableMessages); + Messages.unsubscribe('Hifi-Object-Manipulation'); + break; + case 'selectWearable': + ensureWearableSelected(message.entityID); + break; + case 'deleteWearable': + Entities.deleteEntity(message.entityID); + updateAvatarWearables(currentAvatar, message.avatarName); + break; + case 'changeDisplayName': + if (MyAvatar.displayName !== message.displayName) { + MyAvatar.displayName = message.displayName; + UserActivityLogger.palAction("display_name_change", message.displayName); + } + break; + case 'applyExternalAvatar': + var currentAvatarURL = MyAvatar.getFullAvatarURLFromPreferences(); + if(currentAvatarURL !== message.avatarURL) { + MyAvatar.useFullAvatarURL(message.avatarURL); + sendToQml({'method' : 'externalAvatarApplied', 'avatarURL' : message.avatarURL}) + } + break; + case 'navigate': + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system") + if(message.url.indexOf('app://') === 0) { + if(message.url === 'app://marketplace') { + tablet.gotoWebScreen(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); + } else if(message.url === 'app://purchases') { + tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); + } + + } else if(message.url.indexOf('hifi://') === 0) { + AddressManager.handleLookupString(message.url, false); + } else if(message.url.indexOf('https://') === 0 || message.url.indexOf('http://') === 0) { + tablet.gotoWebScreen(message.url, MARKETPLACES_INJECT_SCRIPT_URL); + } + + break; + case 'setScale': + notifyScaleChanged = false; + MyAvatar.setAvatarScale(message.avatarScale); + currentAvatar.avatarScale = message.avatarScale; + notifyScaleChanged = true; + break; + case 'revertScale': + MyAvatar.setAvatarScale(message.avatarScale); + currentAvatar.avatarScale = message.avatarScale; + break; + case 'saveSettings': + MyAvatar.setAvatarScale(message.avatarScale); + currentAvatar.avatarScale = message.avatarScale; + + MyAvatar.setDominantHand(message.settings.dominantHand); + MyAvatar.setCollisionsEnabled(message.settings.collisionsEnabled); + MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl; + MyAvatar.setAnimGraphUrl(message.settings.animGraphUrl); + + settings = getMyAvatarSettings(); + break; + default: + print('Unrecognized message from AvatarApp.qml:', JSON.stringify(message)); + } +} + +function isGrabbable(entityID) { + if(entityID === null) { + return false; + } + + var properties = Entities.getEntityProperties(entityID, ['clientOnly', 'userData']); + if (properties.clientOnly) { + var userData; + try { + userData = JSON.parse(properties.userData); + } catch (e) { + userData = {}; + } + + return userData.grabbableKey && userData.grabbableKey.grabbable; + } + + return false; +} + +function setGrabbable(entityID, grabbable) { + var properties = Entities.getEntityProperties(entityID, ['clientOnly', 'userData']); + if (properties.clientOnly) { + var userData; + try { + userData = JSON.parse(properties.userData); + } catch (e) { + userData = {}; + } + + if (userData.grabbableKey === undefined) { + userData.grabbableKey = {}; + } + userData.grabbableKey.grabbable = grabbable; + Entities.editEntity(entityID, {userData: JSON.stringify(userData)}); + } +} + +function ensureWearableSelected(entityID) { + if(selectedAvatarEntityID !== entityID) { + if(grabbedAvatarEntityChangeNotifier !== null) { + Script.clearInterval(grabbedAvatarEntityChangeNotifier); + grabbedAvatarEntityChangeNotifier = null; + } + + if(selectedAvatarEntityID !== null) { + setGrabbable(selectedAvatarEntityID, selectedAvatarEntityGrabbable); + } + + selectedAvatarEntityID = entityID; + selectedAvatarEntityGrabbable = isGrabbable(entityID); + + if(selectedAvatarEntityID !== null) { + setGrabbable(selectedAvatarEntityID, true); + } + + return true; + } + + return false; +} + +function isEntityBeingWorn(entityID) { + return Entities.getEntityProperties(entityID, 'parentID').parentID === MyAvatar.sessionUUID; +}; + +function onSelectedEntity(entityID, pointerEvent) { + if(selectedAvatarEntityID !== entityID && isEntityBeingWorn(entityID)) + { + if(ensureWearableSelected(entityID)) { + sendToQml({'method' : 'selectAvatarEntity', 'entityID' : selectedAvatarEntityID}); + } + } +} + +function handleWearableMessages(channel, message, sender) { + if (channel !== 'Hifi-Object-Manipulation') { + return; + } + + var parsedMessage = null; + + try { + parsedMessage = JSON.parse(message); + } catch (e) { + return; + } + + var entityID = parsedMessage.grabbedEntity; + if(parsedMessage.action === 'grab') { + if(selectedAvatarEntityID !== entityID) { + ensureWearableSelected(entityID); + sendToQml({'method' : 'selectAvatarEntity', 'entityID' : selectedAvatarEntityID}); + } + + grabbedAvatarEntityChangeNotifier = Script.setInterval(function() { + // for some reasons Entities.getEntityProperties returns more than was asked.. + var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions']; + var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames); + var properties = {} + + propertyNames.forEach(function(propertyName) { + properties[propertyName] = entityProperties[propertyName]; + }) + + properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation); + sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, 'wearableIndex' : -1, 'properties' : properties, updateUI : true}) + + }, 1000); + } else if(parsedMessage.action === 'release') { + if(grabbedAvatarEntityChangeNotifier !== null) { + Script.clearInterval(grabbedAvatarEntityChangeNotifier); + grabbedAvatarEntityChangeNotifier = null; + } + } +} + +function sendToQml(message) { + tablet.sendToQml(message); +} + +function onBookmarkLoaded(bookmarkName) { + executeLater(function() { + currentAvatar = getMyAvatar(); + sendToQml({'method' : 'bookmarkLoaded', 'data' : {'name' : bookmarkName, 'currentAvatar' : currentAvatar} }); + }); +} + +function onBookmarkDeleted(bookmarkName) { + sendToQml({'method' : 'bookmarkDeleted', 'name' : bookmarkName}); +} + +function onBookmarkAdded(bookmarkName) { + var bookmark = AvatarBookmarks.getBookmark(bookmarkName); + bookmark.avatarEntites.forEach(function(avatarEntity) { + avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation) + }) + + sendToQml({ 'method': 'bookmarkAdded', 'bookmarkName': bookmarkName, 'bookmark': bookmark }); +} + +// +// Manage the connection between the button and the window. +// +var button; +var buttonName = "AVATAR"; +var tablet = null; + +function startup() { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + text: buttonName, + icon: "icons/tablet-icons/avatar-i.svg", + activeIcon: "icons/tablet-icons/avatar-a.svg", + sortOrder: 7 + }); + button.clicked.connect(onTabletButtonClicked); + tablet.screenChanged.connect(onTabletScreenChanged); +} + +startup(); + +var isWired = false; +function off() { + if (isWired) { // It is not ok to disconnect these twice, hence guard. + isWired = false; + } + + if(adjustWearables.opened) { + adjustWearables.setOpened(false); + ensureWearableSelected(null); + Entities.mousePressOnEntity.disconnect(onSelectedEntity); + + Messages.messageReceived.disconnect(handleWearableMessages); + Messages.unsubscribe('Hifi-Object-Manipulation'); + } + + AvatarBookmarks.bookmarkLoaded.disconnect(onBookmarkLoaded); + AvatarBookmarks.bookmarkDeleted.disconnect(onBookmarkDeleted); + AvatarBookmarks.bookmarkAdded.disconnect(onBookmarkAdded); + + MyAvatar.skeletonModelURLChanged.disconnect(onSkeletonModelURLChanged); + MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged); + MyAvatar.collisionsEnabledChanged.disconnect(onCollisionsEnabledChanged); + MyAvatar.newCollisionSoundURL.disconnect(onNewCollisionSoundUrl); + MyAvatar.animGraphUrlChanged.disconnect(onAnimGraphUrlChanged); + MyAvatar.targetScaleChanged.disconnect(onTargetScaleChanged); +} + +function on() { + AvatarBookmarks.bookmarkLoaded.connect(onBookmarkLoaded); + AvatarBookmarks.bookmarkDeleted.connect(onBookmarkDeleted); + AvatarBookmarks.bookmarkAdded.connect(onBookmarkAdded); + + MyAvatar.skeletonModelURLChanged.connect(onSkeletonModelURLChanged); + MyAvatar.dominantHandChanged.connect(onDominantHandChanged); + MyAvatar.collisionsEnabledChanged.connect(onCollisionsEnabledChanged); + MyAvatar.newCollisionSoundURL.connect(onNewCollisionSoundUrl); + MyAvatar.animGraphUrlChanged.connect(onAnimGraphUrlChanged); + MyAvatar.targetScaleChanged.connect(onTargetScaleChanged); +} + +function onTabletButtonClicked() { + if (onAvatarAppScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + ContextOverlay.enabled = false; + tablet.loadQMLSource(AVATARAPP_QML_SOURCE); + isWired = true; + } +} +var hasEventBridge = false; +function wireEventBridge(on) { + if (on) { + if (!hasEventBridge) { + tablet.fromQml.connect(fromQml); + hasEventBridge = true; + } + } else { + if (hasEventBridge) { + tablet.fromQml.disconnect(fromQml); + hasEventBridge = false; + } + } +} + +var onAvatarAppScreen = false; +function onTabletScreenChanged(type, url) { + var onAvatarAppScreenNow = (type === "QML" && url === AVATARAPP_QML_SOURCE); + wireEventBridge(onAvatarAppScreenNow); + // for toolbar mode: change button to active when window is first openend, false otherwise. + button.editProperties({isActive: onAvatarAppScreenNow}); + + if (!onAvatarAppScreen && onAvatarAppScreenNow) { + on(); + } else if(onAvatarAppScreen && !onAvatarAppScreenNow) { + off(); + } + + onAvatarAppScreen = onAvatarAppScreenNow; + + if(onAvatarAppScreenNow) { + var message = { + 'method' : 'initialize', + 'data' : { + 'jointNames' : MyAvatar.getJointNames() + } + }; + + sendToQml(message) + } +} + +function shutdown() { + if (onAvatarAppScreen) { + tablet.gotoHomeScreen(); + } + button.clicked.disconnect(onTabletButtonClicked); + tablet.removeButton(button); + tablet.screenChanged.disconnect(onTabletScreenChanged); + + off(); +} + +// +// Cleanup. +// +Script.scriptEnding.connect(shutdown); + +}()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/controllerDisplay.js b/scripts/system/controllers/controllerDisplay.js index dc7c9b37bd..8aa0393357 100644 --- a/scripts/system/controllers/controllerDisplay.js +++ b/scripts/system/controllers/controllerDisplay.js @@ -108,14 +108,13 @@ createControllerDisplay = function(config) { for (var partName in controller.parts) { overlayID = this.overlays[i++]; var part = controller.parts[partName]; - localPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, part.naturalPosition)); + localPosition = Vec3.subtract(part.naturalPosition, controller.naturalPosition); var localRotation; var value = this.partValues[partName]; var offset, rotation; if (value !== undefined) { if (part.type === "linear") { - var axis = Vec3.multiplyQbyV(controller.rotation, part.axis); - offset = Vec3.multiply(part.maxTranslation * value, axis); + offset = Vec3.multiply(part.maxTranslation * value, part.axis); localPosition = Vec3.sum(localPosition, offset); localRotation = undefined; } else if (part.type === "joystick") { @@ -126,8 +125,8 @@ createControllerDisplay = function(config) { } else { offset = { x: 0, y: 0, z: 0 }; } - localPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, Vec3.sum(offset, part.naturalPosition))); - localRotation = Quat.multiply(controller.rotation, rotation); + localPosition = Vec3.sum(offset, localPosition); + localRotation = rotation; } else if (part.type === "rotational") { value = clamp(value, part.minValue, part.maxValue); var pct = (value - part.minValue) / part.maxValue; @@ -139,8 +138,8 @@ createControllerDisplay = function(config) { } else { offset = { x: 0, y: 0, z: 0 }; } - localPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, Vec3.sum(offset, part.naturalPosition))); - localRotation = Quat.multiply(controller.rotation, rotation); + localPosition = Vec3.sum(offset, localPosition); + localRotation = rotation; } } if (localRotation !== undefined) { @@ -169,9 +168,11 @@ createControllerDisplay = function(config) { if (controller.naturalPosition) { position = Vec3.sum(Vec3.multiplyQbyV(controller.rotation, controller.naturalPosition), position); + } else { + controller.naturalPosition = { x: 0, y: 0, z: 0 }; } - var overlayID = Overlays.addOverlay("model", { + var baseOverlayID = Overlays.addOverlay("model", { url: controller.modelURL, dimensions: Vec3.multiply(sensorScaleFactor, controller.dimensions), localRotation: controller.rotation, @@ -181,23 +182,21 @@ createControllerDisplay = function(config) { ignoreRayIntersection: true }); - controllerDisplay.overlays.push(overlayID); - overlayID = null; + controllerDisplay.overlays.push(baseOverlayID); if (controller.parts) { for (var partName in controller.parts) { var part = controller.parts[partName]; - var partPosition = Vec3.sum(controller.position, Vec3.multiplyQbyV(controller.rotation, part.naturalPosition)); - var innerRotation = controller.rotation; + var localPosition = Vec3.subtract(part.naturalPosition, controller.naturalPosition); + var localRotation = { x: 0, y: 0, z: 0, w: 1 } controllerDisplay.parts[partName] = controller.parts[partName]; var properties = { url: part.modelURL, - localPosition: partPosition, - localRotation: innerRotation, - parentID: MyAvatar.SELF_ID, - parentJointIndex: controller.jointIndex, + localPosition: localPosition, + localRotation: localRotation, + parentID: baseOverlayID, ignoreRayIntersection: true }; @@ -207,11 +206,10 @@ createControllerDisplay = function(config) { properties['textures'] = textures; } - overlayID = Overlays.addOverlay("model", properties); + var overlayID = Overlays.addOverlay("model", properties); if (part.type === "rotational") { var input = resolveHardware(part.input); - print("Mapping to: ", part.input, input); mapping.from([input]).peek().to(function(partName) { return function(value) { // insert the most recent controller value into controllerDisplay.partValues. diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js index 00d7ad0491..38334f5523 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -11,7 +11,8 @@ TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS, findGroupParent, Vec3, cloneEntity, entityIsCloneable, propsAreCloneDynamic, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, findHandChildEntities, TEAR_AWAY_DISTANCE, MSECS_PER_SEC, TEAR_AWAY_CHECK_TIME, - TEAR_AWAY_COUNT, distanceBetweenPointAndEntityBoundingBox, print, Uuid, highlightTargetEntity, unhighlightTargetEntity + TEAR_AWAY_COUNT, distanceBetweenPointAndEntityBoundingBox, print, Uuid, highlightTargetEntity, unhighlightTargetEntity, + distanceBetweenEntityLocalPositionAndBoundingBox */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -172,12 +173,9 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); if (now - this.lastUnequipCheckTime > MSECS_PER_SEC * TEAR_AWAY_CHECK_TIME) { this.lastUnequipCheckTime = now; if (props.parentID === MyAvatar.SELF_ID) { - var sensorScaleFactor = MyAvatar.sensorToWorldScale; - var handPosition = controllerData.controllerLocations[this.hand].position; - var dist = distanceBetweenPointAndEntityBoundingBox(handPosition, props); - var distance = Vec3.distance(props.position, handPosition); - if ((dist > TEAR_AWAY_DISTANCE) || - (distance > NEAR_GRAB_RADIUS * sensorScaleFactor)) { + var tearAwayDistance = TEAR_AWAY_DISTANCE * MyAvatar.sensorToWorldScale; + var distance = distanceBetweenEntityLocalPositionAndBoundingBox(props); + if (distance > tearAwayDistance) { this.autoUnequipCounter++; } else { this.autoUnequipCounter = 0; diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js index a8e30c51a8..a2fe0bfcd4 100644 --- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js +++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -50,19 +50,19 @@ Script.include("/~/system/libraries/controllers.js"); return this.hand === RIGHT_HAND ? leftOverlayLaserInput : rightOverlayLaserInput; }; - this.isPointingAtTabletOrWeb = function(controllerData, triggerPressed) { + this.isPointingAtTriggerable = function(controllerData, triggerPressed) { + // allow pointing at tablet, unlocked web entities, or web overlays automatically without pressing trigger, + // but for pointing at locked web entities or non-web overlays user must be pressing trigger var intersection = controllerData.rayPicks[this.hand]; if (intersection.type === Picks.INTERSECTED_OVERLAY) { var objectID = intersection.objectID; if ((HMD.tabletID && objectID === HMD.tabletID) || - (HMD.tabletScreenID && objectID === HMD.tabletScreenID) || + (HMD.tabletScreenID && objectID === HMD.tabletScreenID) || (HMD.homeButtonID && objectID === HMD.homeButtonID)) { return true; } else { var overlayType = Overlays.getOverlayType(objectID); - if (overlayType === "web3d") { - return true; - } + return overlayType === "web3d" || triggerPressed; } } else if (intersection.type === Picks.INTERSECTED_ENTITY) { var entityProperty = Entities.getEntityProperties(intersection.objectID); @@ -103,7 +103,7 @@ Script.include("/~/system/libraries/controllers.js"); var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE && controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE; var allowThisModule = !otherModuleRunning || isTriggerPressed; - if (allowThisModule && this.isPointingAtTabletOrWeb(controllerData, isTriggerPressed)) { + if (allowThisModule && this.isPointingAtTriggerable(controllerData, isTriggerPressed)) { this.updateAllwaysOn(); if (isTriggerPressed) { this.dominantHandOverride = true; // Override dominant hand. @@ -124,7 +124,7 @@ Script.include("/~/system/libraries/controllers.js"); var allowThisModule = !otherModuleRunning && !grabModuleNeedsToRun; var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE; var laserOn = isTriggerPressed || this.parameters.handLaser.allwaysOn; - if (allowThisModule && (laserOn && this.isPointingAtTabletOrWeb(controllerData, isTriggerPressed))) { + if (allowThisModule && (laserOn && this.isPointingAtTriggerable(controllerData, isTriggerPressed))) { this.running = true; return makeRunningValues(true, [], []); } diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index 975ec3fc22..ce93c6a010 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -29,7 +29,6 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/disableOtherModule.js", "controllerModules/farTrigger.js", "controllerModules/teleport.js", - "controllerModules/scaleAvatar.js", "controllerModules/hudOverlayPointer.js", "controllerModules/mouseHMD.js", "controllerModules/scaleEntity.js", diff --git a/scripts/system/controllers/handTouch.js b/scripts/system/controllers/handTouch.js index c20b86b775..db79aa4a77 100644 --- a/scripts/system/controllers/handTouch.js +++ b/scripts/system/controllers/handTouch.js @@ -14,64 +14,64 @@ /* global Script, Overlays, Controller, Vec3, MyAvatar, Entities */ -(function() { - - var MSECONDS_AFTER_LOAD = 2000; +(function () { + var handTouchEnabled = true; + var MSECONDS_AFTER_LOAD = 2000; var updateFingerWithIndex = 0; + var untouchableEntities = []; - - // Keys to access finger data + // Keys to access finger data var fingerKeys = ["pinky", "ring", "middle", "index", "thumb"]; - - // Additionally close the hands to achieve a grabbing effect + + // Additionally close the hands to achieve a grabbing effect var grabPercent = { left: 0, right: 0 }; - + var Palm = function() { this.position = {x: 0, y: 0, z: 0}; this.perpendicular = {x: 0, y: 0, z: 0}; this.distance = 0; this.fingers = { - pinky: {x: 0, y: 0, z: 0}, - middle: {x: 0, y: 0, z: 0}, - ring: {x: 0, y: 0, z: 0}, - thumb: {x: 0, y: 0, z: 0}, + pinky: {x: 0, y: 0, z: 0}, + middle: {x: 0, y: 0, z: 0}, + ring: {x: 0, y: 0, z: 0}, + thumb: {x: 0, y: 0, z: 0}, index: {x: 0, y: 0, z: 0} }; this.set = false; }; - + var palmData = { left: new Palm(), right: new Palm() }; var handJointNames = {left: "LeftHand", right: "RightHand"}; - - // Store which fingers are touching - if all false restate the default poses + + // Store which fingers are touching - if all false restate the default poses var isTouching = { left: { - pinky: false, - middle: false, - ring: false, - thumb: false, - index: false + pinky: false, + middle: false, + ring: false, + thumb: false, + index: false }, right: { - pinky: false, - middle: false, - ring: false, - thumb: false, + pinky: false, + middle: false, + ring: false, + thumb: false, index: false } }; - + // frame count for transition to default pose - + var countToDefault = { left: 0, right: 0 }; - + // joint data for open pose var dataOpen = { left: { @@ -128,7 +128,7 @@ ] } }; - + // joint data for close pose var dataClose = { left: { @@ -185,78 +185,78 @@ ] } }; - + // snapshot for the default pose var dataDefault = { left: { - pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], set: false }, right: { - pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], set: false } }; - + // joint data for the current frame var dataCurrent = { left: { - pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}] }, right: { - pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}] } }; - - // interpolated values on joint data to smooth movement + + // interpolated values on joint data to smooth movement var dataDelta = { left: { - pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}] }, right: { - pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], - thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}] } }; - + // Acquire an updated value per hand every 5 frames when finger is touching (faster in) var touchAnimationSteps = 5; - - // Acquire an updated value per hand every 20 frames when finger is returning to default position (slower out) + + // Acquire an updated value per hand every 20 frames when finger is returning to default position (slower out) var defaultAnimationSteps = 10; - + // Debugging info var showSphere = false; var showLines = false; - + // This get setup on creation var linesCreated = false; var sphereCreated = false; - - // Register object with API Debugger + + // Register object with API Debugger var varsToDebug = { scriptLoaded: false, toggleDebugSphere: function() { @@ -275,17 +275,17 @@ }, fingerPercent: { left: { - pinky: 0.38, - middle: 0.38, - ring: 0.38, - thumb: 0.38, + pinky: 0.38, + middle: 0.38, + ring: 0.38, + thumb: 0.38, index: 0.38 - } , + } , right: { - pinky: 0.38, - middle: 0.38, - ring: 0.38, - thumb: 0.38, + pinky: 0.38, + middle: 0.38, + ring: 0.38, + thumb: 0.38, index: 0.38 } }, @@ -300,12 +300,11 @@ palmData: { left: new Palm(), right: new Palm() - }, + }, offset: {x: 0, y: 0, z: 0}, avatarLoaded: false }; - - + // Add/Subtract the joint data - per finger joint function addVals(val1, val2, sign) { var val = []; @@ -321,7 +320,7 @@ } return val; } - + // Multiply/Divide the joint data - per finger joint function multiplyValsBy(val1, num) { var val = []; @@ -334,7 +333,7 @@ } return val; } - + // Calculate the finger lengths by adding its joint lengths function getJointDistances(jointNamesArray) { var result = {distances: [], totalDistance: 0}; @@ -349,13 +348,12 @@ } return result; } - - function dataRelativeToWorld(side, dataIn, dataOut) { + function dataRelativeToWorld(side, dataIn, dataOut) { var handJoint = handJointNames[side]; var jointIndex = MyAvatar.getJointIndex(handJoint); var worldPosHand = MyAvatar.jointToWorldPoint({x: 0, y: 0, z: 0}, jointIndex); - + dataOut.position = MyAvatar.jointToWorldPoint(dataIn.position, jointIndex); var localPerpendicular = side === "right" ? {x: 0.2, y: 0, z: 1} : {x: -0.2, y: 0, z: 1}; dataOut.perpendicular = Vec3.normalize( @@ -365,15 +363,14 @@ for (var i = 0; i < fingerKeys.length; i++) { var finger = fingerKeys[i]; dataOut.fingers[finger] = MyAvatar.jointToWorldPoint(dataIn.fingers[finger], jointIndex); - } + } } - - function dataRelativeToHandJoint(side, dataIn, dataOut) { + function dataRelativeToHandJoint(side, dataIn, dataOut) { var handJoint = handJointNames[side]; var jointIndex = MyAvatar.getJointIndex(handJoint); var worldPosHand = MyAvatar.jointToWorldPoint({x: 0, y: 0, z: 0}, jointIndex); - + dataOut.position = MyAvatar.worldToJointPoint(dataIn.position, jointIndex); dataOut.perpendicular = MyAvatar.worldToJointPoint(Vec3.sum(worldPosHand, dataIn.perpendicular), jointIndex); dataOut.distance = dataIn.distance; @@ -382,46 +379,44 @@ dataOut.fingers[finger] = MyAvatar.worldToJointPoint(dataIn.fingers[finger], jointIndex); } } - - // Calculate touch field; Sphere at the center of the palm, + + // Calculate touch field; Sphere at the center of the palm, // perpendicular vector from the palm plane and origin of the the finger rays - function estimatePalmData(side) { // Return data object - var data = new Palm(); - - var jointOffset = { x: 0, y: 0, z: 0 }; - + var data = new Palm(); + + var jointOffset = { x: 0, y: 0, z: 0 }; + var upperSide = side[0].toUpperCase() + side.substring(1); var jointIndexHand = MyAvatar.getJointIndex(upperSide + "Hand"); - + // Store position of the hand joint var worldPosHand = MyAvatar.jointToWorldPoint(jointOffset, jointIndexHand); var minusWorldPosHand = {x: -worldPosHand.x, y: -worldPosHand.y, z: -worldPosHand.z}; - + // Data for finger rays var directions = {pinky: undefined, middle: undefined, ring: undefined, thumb: undefined, index: undefined}; var positions = {pinky: undefined, middle: undefined, ring: undefined, thumb: undefined, index: undefined}; - + var thumbLength = 0; var weightCount = 0; - + // Calculate palm center - var handJointWeight = 1; var fingerJointWeight = 2; - + var palmCenter = {x: 0, y: 0, z: 0}; palmCenter = Vec3.sum(worldPosHand, palmCenter); - + weightCount += handJointWeight; - + for (var i = 0; i < fingerKeys.length; i++) { var finger = fingerKeys[i]; var jointSuffixes = 4; // Get 4 joint names with suffix numbers (0, 1, 2, 3) var jointNames = getJointNames(side, finger, jointSuffixes); var fingerLength = getJointDistances(jointNames).totalDistance; - + var jointIndex = MyAvatar.getJointIndex(jointNames[0]); positions[finger] = MyAvatar.jointToWorldPoint(jointOffset, jointIndex); directions[finger] = Vec3.normalize(Vec3.sum(positions[finger], minusWorldPosHand)); @@ -429,66 +424,63 @@ if (finger !== "thumb") { // finger joints have double the weight than the hand joint // This would better position the palm estimation - - palmCenter = Vec3.sum(Vec3.multiply(fingerJointWeight, positions[finger]), palmCenter); + + palmCenter = Vec3.sum(Vec3.multiply(fingerJointWeight, positions[finger]), palmCenter); weightCount += fingerJointWeight; } else { thumbLength = fingerLength; } } - + // perpendicular change direction depending on the side - data.perpendicular = (side === "right") ? - Vec3.normalize(Vec3.cross(directions.index, directions.pinky)): + data.perpendicular = (side === "right") ? + Vec3.normalize(Vec3.cross(directions.index, directions.pinky)): Vec3.normalize(Vec3.cross(directions.pinky, directions.index)); - + data.position = Vec3.multiply(1.0/weightCount, palmCenter); - + if (side === "right") { varsToDebug.offset = MyAvatar.worldToJointPoint(worldPosHand, jointIndexHand); } - + var palmDistanceMultiplier = 1.55; // 1.55 based on test/error for the sphere radius that best fits the hand - data.distance = palmDistanceMultiplier*Vec3.distance(data.position, positions.index); + data.distance = palmDistanceMultiplier*Vec3.distance(data.position, positions.index); // move back thumb ray origin var thumbBackMultiplier = 0.2; data.fingers.thumb = Vec3.sum( data.fingers.thumb, Vec3.multiply( -thumbBackMultiplier * thumbLength, data.perpendicular)); - + // return getDataRelativeToHandJoint(side, data); dataRelativeToHandJoint(side, data, palmData[side]); palmData[side].set = true; - // return palmData[side]; } - + // Register GlobalDebugger for API Debugger Script.registerValue("GlobalDebugger", varsToDebug); // store the rays for the fingers - only for debug purposes - var fingerRays = { + var fingerRays = { left: { - pinky: undefined, - middle: undefined, - ring: undefined, - thumb: undefined, + pinky: undefined, + middle: undefined, + ring: undefined, + thumb: undefined, index: undefined - }, + }, right: { - pinky: undefined, - middle: undefined, - ring: undefined, - thumb: undefined, + pinky: undefined, + middle: undefined, + ring: undefined, + thumb: undefined, index: undefined } }; - + // Create debug overlays - finger rays + palm rays + spheres - var palmRay, sphereHand; - + function createDebugLines() { - for (var i = 0; i < fingerKeys.length; i++) { fingerRays.left[fingerKeys[i]] = Overlays.addOverlay("line3d", { color: { red: 0, green: 0, blue: 255 }, @@ -503,7 +495,7 @@ visible: showLines }); } - + palmRay = { left: Overlays.addOverlay("line3d", { color: { red: 255, green: 0, blue: 0 }, @@ -520,9 +512,8 @@ }; linesCreated = true; } - + function createDebugSphere() { - sphereHand = { right: Overlays.addOverlay("sphere", { position: MyAvatar.position, @@ -536,10 +527,10 @@ scale: { x: 0.01, y: 0.01, z: 0.01 }, visible: showSphere }) - }; + }; sphereCreated = true; } - + function acquireDefaultPose(side) { for (var i = 0; i < fingerKeys.length; i++) { var finger = fingerKeys[i]; @@ -553,87 +544,87 @@ } dataDefault[side].set = true; } - - var rayPicks = { - left: { - pinky: undefined, - middle: undefined, - ring: undefined, - thumb: undefined, + + var rayPicks = { + left: { + pinky: undefined, + middle: undefined, + ring: undefined, + thumb: undefined, index: undefined }, - right: { - pinky: undefined, - middle: undefined, - ring: undefined, - thumb: undefined, + right: { + pinky: undefined, + middle: undefined, + ring: undefined, + thumb: undefined, index: undefined } }; - - var dataFailed = { - left: { - pinky: 0, - middle: 0, - ring: 0, - thumb: 0, + + var dataFailed = { + left: { + pinky: 0, + middle: 0, + ring: 0, + thumb: 0, index: 0 }, - right: { - pinky: 0, - middle: 0, - ring: 0, - thumb: 0, + right: { + pinky: 0, + middle: 0, + ring: 0, + thumb: 0, index: 0 } }; - + function clearRayPicks(side) { for (var i = 0; i < fingerKeys.length; i++) { var finger = fingerKeys[i]; if (rayPicks[side][finger] !== undefined) { RayPick.removeRayPick(rayPicks[side][finger]); rayPicks[side][finger] = undefined; - } + } } } - + function createRayPicks(side) { var data = palmData[side]; clearRayPicks(side); for (var i = 0; i < fingerKeys.length; i++) { - var finger = fingerKeys[i]; + var finger = fingerKeys[i]; var LOOKUP_DISTANCE_MULTIPLIER = 1.5; var dist = LOOKUP_DISTANCE_MULTIPLIER*data.distance; - console.log("distance: " + dist); - var checkOffset = { - x: data.perpendicular.x * dist, - y: data.perpendicular.y * dist, - z: data.perpendicular.z * dist + var checkOffset = { + x: data.perpendicular.x * dist, + y: data.perpendicular.y * dist, + z: data.perpendicular.z * dist }; - + var checkPoint = Vec3.sum(data.position, Vec3.multiply(2, checkOffset)); var sensorToWorldScale = MyAvatar.getSensorToWorldScale(); - + var origin = data.fingers[finger]; - + var direction = Vec3.normalize(Vec3.subtract(checkPoint, origin)); - + origin = Vec3.multiply(1/sensorToWorldScale, origin); - + rayPicks[side][finger] = RayPick.createRayPick( - { - "enabled": false, + { + "enabled": false, "joint": handJointNames[side], "posOffset": origin, "dirOffset": direction, "filter": RayPick.PICK_ENTITIES } - ); - - RayPick.setPrecisionPicking(rayPicks[side][finger], true); - } + ); + + RayPick.setPrecisionPicking(rayPicks[side][finger], true); + } } + function activateNextRay(side, index) { var nextIndex = (index < fingerKeys.length-1) ? index + 1 : 0; for (var i = 0; i < fingerKeys.length; i++) { @@ -642,46 +633,44 @@ RayPick.enableRayPick(rayPicks[side][finger]); } else { RayPick.disableRayPick(rayPicks[side][finger]); - } + } } } - - function updateSphereHand(side) { + function updateSphereHand(side) { var data = new Palm(); dataRelativeToWorld(side, palmData[side], data); varsToDebug.palmData[side] = palmData[side]; - + var palmPoint = data.position; var LOOKUP_DISTANCE_MULTIPLIER = 1.5; var dist = LOOKUP_DISTANCE_MULTIPLIER*data.distance; - - // Situate the debugging overlays - - var checkOffset = { - x: data.perpendicular.x * dist, - y: data.perpendicular.y * dist, - z: data.perpendicular.z * dist + + // Situate the debugging overlays + var checkOffset = { + x: data.perpendicular.x * dist, + y: data.perpendicular.y * dist, + z: data.perpendicular.z * dist }; - + var spherePos = Vec3.sum(palmPoint, checkOffset); var checkPoint = Vec3.sum(palmPoint, Vec3.multiply(2, checkOffset)); - + if (showLines) { Overlays.editOverlay(palmRay[side], { start: palmPoint, end: checkPoint, visible: showLines - }); + }); for (var i = 0; i < fingerKeys.length; i++) { Overlays.editOverlay(fingerRays[side][fingerKeys[i]], { start: data.fingers[fingerKeys[i]], end: checkPoint, visible: showLines }); - } + } } - + if (showSphere) { Overlays.editOverlay(sphereHand[side], { position: spherePos, @@ -691,16 +680,16 @@ z: 2*dist }, visible: showSphere - }); + }); } - + // Update the intersection of only one finger at a time - - var finger = fingerKeys[updateFingerWithIndex]; - - - var grabbables = Entities.findEntities(spherePos, dist); - + var finger = fingerKeys[updateFingerWithIndex]; + var nearbyEntities = Entities.findEntities(spherePos, dist); + // Filter the entities that are allowed to be touched + var touchableEntities = nearbyEntities.filter(function (id) { + return untouchableEntities.indexOf(id) == -1; + }); var intersection; if (rayPicks[side][finger] !== undefined) { intersection = RayPick.getPrevRayPickResult(rayPicks[side][finger]); @@ -709,11 +698,10 @@ var animationSteps = defaultAnimationSteps; var newFingerData = dataDefault[side][finger]; var isAbleToGrab = false; - if (grabbables.length > 0) { - - RayPick.setIncludeItems(rayPicks[side][finger], grabbables); + if (touchableEntities.length > 0) { + RayPick.setIncludeItems(rayPicks[side][finger], touchableEntities); - if (intersection === undefined) { + if (intersection === undefined) { return; } @@ -726,28 +714,27 @@ // Store if this finger is touching something isTouching[side][finger] = isAbleToGrab; if (isAbleToGrab) { - // update the open/close percentage for this finger - + // update the open/close percentage for this finger var FINGER_REACT_MULTIPLIER = 2.8; - + percent = intersection.distance/(FINGER_REACT_MULTIPLIER*dist); - + var THUMB_FACTOR = 0.2; var FINGER_FACTOR = 0.05; - + // Amount of grab coefficient added to the fingers - thumb is higher - var grabMultiplier = finger === "thumb" ? THUMB_FACTOR : FINGER_FACTOR; + var grabMultiplier = finger === "thumb" ? THUMB_FACTOR : FINGER_FACTOR; percent += grabMultiplier * grabPercent[side]; - + // Calculate new interpolation data var totalDistance = addVals(dataClose[side][finger], dataOpen[side][finger], -1); // Assign close/open ratio to finger to simulate touch - newFingerData = addVals(dataOpen[side][finger], multiplyValsBy(totalDistance, percent), 1); + newFingerData = addVals(dataOpen[side][finger], multiplyValsBy(totalDistance, percent), 1); animationSteps = touchAnimationSteps; - } + } varsToDebug.fingerPercent[side][finger] = percent; - - } + + } if (!isAbleToGrab) { dataFailed[side][finger] = dataFailed[side][finger] === 0 ? 1 : 2; } else { @@ -756,13 +743,12 @@ // If it only fails once it will not update increments if (dataFailed[side][finger] !== 1) { // Calculate animation increments - dataDelta[side][finger] = - multiplyValsBy(addVals(newFingerData, dataCurrent[side][finger], -1), 1.0/animationSteps); + dataDelta[side][finger] = + multiplyValsBy(addVals(newFingerData, dataCurrent[side][finger], -1), 1.0/animationSteps); } } - + // Recreate the finger joint names - function getJointNames(side, finger, count) { var names = []; for (var i = 1; i < count+1; i++) { @@ -773,30 +759,34 @@ } // Capture the controller values - var leftTriggerPress = function (value) { varsToDebug.triggerValues.leftTriggerValue = value; - // the value for the trigger increments the hand-close percentage + // the value for the trigger increments the hand-close percentage grabPercent.left = value; }; + var leftTriggerClick = function (value) { varsToDebug.triggerValues.leftTriggerClicked = value; }; + var rightTriggerPress = function (value) { varsToDebug.triggerValues.rightTriggerValue = value; - // the value for the trigger increments the hand-close percentage + // the value for the trigger increments the hand-close percentage grabPercent.right = value; }; + var rightTriggerClick = function (value) { varsToDebug.triggerValues.rightTriggerClicked = value; }; + var leftSecondaryPress = function (value) { varsToDebug.triggerValues.leftSecondaryValue = value; }; + var rightSecondaryPress = function (value) { varsToDebug.triggerValues.rightSecondaryValue = value; }; - + var MAPPING_NAME = "com.highfidelity.handTouch"; var mapping = Controller.newMapping(MAPPING_NAME); mapping.from([Controller.Standard.RT]).peek().to(rightTriggerPress); @@ -810,16 +800,17 @@ mapping.from([Controller.Standard.RightGrip]).peek().to(rightSecondaryPress); Controller.enableMapping(MAPPING_NAME); - + if (showLines && !linesCreated) { createDebugLines(); linesCreated = true; } + if (showSphere && !sphereCreated) { createDebugSphere(); sphereCreated = true; } - + function getTouching(side) { var animating = false; for (var i = 0; i < fingerKeys.length; i++) { @@ -828,19 +819,70 @@ } return animating; // return false only if none of the fingers are touching } - + function reEstimatePalmData() { ["right", "left"].forEach(function(side) { estimatePalmData(side); }); } - + function recreateRayPicks() { ["right", "left"].forEach(function(side) { createRayPicks(side); }); } - + + function cleanUp() { + ["right", "left"].forEach(function (side) { + if (linesCreated) { + Overlays.deleteOverlay(palmRay[side]); + } + if (sphereCreated) { + Overlays.deleteOverlay(sphereHand[side]); + } + clearRayPicks(side); + for (var i = 0; i < fingerKeys.length; i++) { + var finger = fingerKeys[i]; + var jointSuffixes = 3; // We need to clear the joints 0, 1 and 2 joints + var names = getJointNames(side, finger, jointSuffixes); + for (var j = 0; j < names.length; j++) { + var index = MyAvatar.getJointIndex(names[j]); + MyAvatar.clearJointData(index); + } + if (linesCreated) { + Overlays.deleteOverlay(fingerRays[side][finger]); + } + } + }); + } + + MyAvatar.shouldDisableHandTouchChanged.connect(function (shouldDisable) { + if (shouldDisable) { + if (handTouchEnabled) { + cleanUp(); + } + } else { + if (!handTouchEnabled) { + reEstimatePalmData(); + recreateRayPicks(); + } + } + handTouchEnabled = !shouldDisable; + }); + + MyAvatar.disableHandTouchForIDChanged.connect(function (entityID, disable) { + var entityIndex = untouchableEntities.indexOf(entityID); + if (disable) { + if (entityIndex == -1) { + untouchableEntities.push(entityID); + } + } else { + if (entityIndex != -1) { + untouchableEntities.splice(entityIndex, 1); + } + } + }); + MyAvatar.onLoadComplete.connect(function () { // Sometimes the rig is not ready when this signal is trigger console.log("avatar loaded"); @@ -849,78 +891,55 @@ recreateRayPicks(); }, MSECONDS_AFTER_LOAD); }); - + MyAvatar.sensorToWorldScaleChanged.connect(function() { reEstimatePalmData(); }); - - Script.scriptEnding.connect(function () { - ["right", "left"].forEach(function(side) { - if (linesCreated) { - Overlays.deleteOverlay(palmRay[side]); - } - if (sphereCreated) { - Overlays.deleteOverlay(sphereHand[side]); - } - clearRayPicks(side); - for (var i = 0; i < fingerKeys.length; i++) { - var finger = fingerKeys[i]; - var jointSuffixes = 3; // We need to clear the joints 0, 1 and 2 joints - var names = getJointNames(side, finger, jointSuffixes); - - for (var j = 0; j < names.length; j++) { - var index = MyAvatar.getJointIndex(names[j]); - MyAvatar.clearJointData(index); - } - - if (linesCreated) { - Overlays.deleteOverlay(fingerRays[side][finger]); - } - } - }); + Script.scriptEnding.connect(function () { + cleanUp(); }); - - Script.update.connect(function() { - + + Script.update.connect(function () { + + if (!handTouchEnabled) { + return; + } + // index of the finger that needs to be updated this frame - updateFingerWithIndex = (updateFingerWithIndex < fingerKeys.length-1) ? updateFingerWithIndex + 1 : 0; - + ["right", "left"].forEach(function(side) { - + if (!palmData[side].set) { reEstimatePalmData(); recreateRayPicks(); } - + // recalculate the base data updateSphereHand(side); activateNextRay(side, updateFingerWithIndex); - + // this vars manage the transition to default pose var isHandTouching = getTouching(side); countToDefault[side] = isHandTouching ? 0 : countToDefault[side] + 1; - - + for (var i = 0; i < fingerKeys.length; i++) { var finger = fingerKeys[i]; var jointSuffixes = 3; // We need to update rotation of the 0, 1 and 2 joints - var names = getJointNames(side, finger, jointSuffixes); - + var names = getJointNames(side, finger, jointSuffixes); + // Add the animation increments - - dataCurrent[side][finger] = addVals(dataCurrent[side][finger], dataDelta[side][finger], 1); - + dataCurrent[side][finger] = addVals(dataCurrent[side][finger], dataDelta[side][finger], 1); + // update every finger joint - for (var j = 0; j < names.length; j++) { var index = MyAvatar.getJointIndex(names[j]); // if no finger is touching restate the default poses - if (isHandTouching || (dataDefault[side].set && + if (isHandTouching || (dataDefault[side].set && countToDefault[side] < fingerKeys.length*touchAnimationSteps)) { var quatRot = dataCurrent[side][finger][j]; - MyAvatar.setJointRotation(index, quatRot); + MyAvatar.setJointRotation(index, quatRot); } else { MyAvatar.clearJointData(index); } diff --git a/scripts/system/controllers/viveControllerConfiguration.js b/scripts/system/controllers/viveControllerConfiguration.js index dc4a5b6bb3..60f0b6b88a 100644 --- a/scripts/system/controllers/viveControllerConfiguration.js +++ b/scripts/system/controllers/viveControllerConfiguration.js @@ -77,6 +77,8 @@ VIVE_CONTROLLER_CONFIGURATION_LEFT = { dimensions: viveNaturalDimensions, parts: { + // DISABLED FOR NOW + /* tips: { type: "static", modelURL: viveTipsModelURL, @@ -103,6 +105,7 @@ VIVE_CONTROLLER_CONFIGURATION_LEFT = { } } }, + */ // The touchpad type draws a dot indicating the current touch/thumb position // and swaps in textures based on the thumb position. @@ -215,6 +218,8 @@ VIVE_CONTROLLER_CONFIGURATION_RIGHT = { }, parts: { + // DISABLED FOR NOW + /* tips: { type: "static", modelURL: viveTipsModelURL, @@ -242,6 +247,7 @@ VIVE_CONTROLLER_CONFIGURATION_RIGHT = { } } }, + */ // The touchpad type draws a dot indicating the current touch/thumb position // and swaps in textures based on the thumb position. diff --git a/scripts/system/edit.js b/scripts/system/edit.js index e9c7a49378..1c0b7c2dcb 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -10,17 +10,15 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Script, SelectionDisplay, LightOverlayManager, CameraManager, Grid, GridTool, EntityListTool, Vec3, SelectionManager, Overlays, OverlayWebWindow, UserActivityLogger, - Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool */ +/* global Script, SelectionDisplay, LightOverlayManager, CameraManager, Grid, GridTool, EntityListTool, Vec3, SelectionManager, + Overlays, OverlayWebWindow, UserActivityLogger, Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, + progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool, OverlaySystemWindow */ (function() { // BEGIN LOCAL_SCOPE "use strict"; -var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; var EDIT_TOGGLE_BUTTON = "com.highfidelity.interface.system.editButton"; -var SYSTEM_TOOLBAR = "com.highfidelity.interface.toolbar.system"; -var EDIT_TOOLBAR = "com.highfidelity.interface.toolbar.edit"; Script.include([ "libraries/stringHelpers.js", @@ -36,13 +34,52 @@ Script.include([ "libraries/entityIconOverlayManager.js" ]); +var CreateWindow = Script.require('./modules/createWindow.js'); + +var TITLE_OFFSET = 60; +var CREATE_TOOLS_WIDTH = 490; +var MAX_DEFAULT_ENTITY_LIST_HEIGHT = 942; + +var createToolsWindow = new CreateWindow( + Script.resourcesPath() + "qml/hifi/tablet/EditTools.qml", + 'Create Tools', + 'com.highfidelity.create.createToolsWindow', + function () { + var windowHeight = Window.innerHeight - TITLE_OFFSET; + if (windowHeight > MAX_DEFAULT_ENTITY_LIST_HEIGHT) { + windowHeight = MAX_DEFAULT_ENTITY_LIST_HEIGHT; + } + return { + size: { + x: CREATE_TOOLS_WIDTH, + y: windowHeight + }, + position: { + x: Window.x + Window.innerWidth - CREATE_TOOLS_WIDTH, + y: Window.y + TITLE_OFFSET + } + } + }, + false +); + +/** + * @description Returns true in case we should use the tablet version of the CreateApp + * @returns boolean + */ +var shouldUseEditTabletApp = function() { + return HMD.active || (!HMD.active && !Settings.getValue("desktopTabletBecomesToolbar", true)); +}; + + var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; var PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); var POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); var SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); -entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { + +var entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { var properties = Entities.getEntityProperties(entityID, ['type', 'isSpotlight']); if (properties.type === 'Light') { return { @@ -59,11 +96,13 @@ var cameraManager = new CameraManager(); var grid = new Grid(); var gridTool = new GridTool({ - horizontalGrid: grid + horizontalGrid: grid, + createToolsWindow: createToolsWindow, + shouldUseEditTabletApp: shouldUseEditTabletApp }); gridTool.setVisible(false); -var entityListTool = new EntityListTool(); +var entityListTool = new EntityListTool(shouldUseEditTabletApp); selectionManager.addEventListener(function () { selectionDisplay.updateHandles(); @@ -207,7 +246,7 @@ function hideMarketplace() { // } function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { - // Adjust the position such that the bounding box (registration, dimenions, and orientation) lies behind the original + // Adjust the position such that the bounding box (registration, dimensions and orientation) lies behind the original // position in the given direction. var CORNERS = [ { x: 0, y: 0, z: 0 }, @@ -232,7 +271,6 @@ function adjustPositionPerBoundingBox(position, direction, registration, dimensi return position; } -var TOOLS_PATH = Script.resolvePath("assets/images/tools/"); var GRABBABLE_ENTITIES_MENU_CATEGORY = "Edit"; // Handles any edit mode updates required when domains have switched @@ -260,6 +298,7 @@ var toolBar = (function () { toolBar, activeButton = null, systemToolbar = null, + dialogWindow = null, tablet = null; function createNewEntity(properties) { @@ -356,6 +395,13 @@ var toolBar = (function () { return entityID; } + function closeExistingDialogWindow() { + if (dialogWindow) { + dialogWindow.close(); + dialogWindow = null; + } + } + function cleanup() { that.setActive(false); if (tablet) { @@ -438,7 +484,7 @@ var toolBar = (function () { if (materialURL.startsWith("materialData")) { materialData = JSON.stringify({ "materials": {} - }) + }); } var DEFAULT_LAYERED_MATERIAL_PRIORITY = 1; @@ -458,15 +504,23 @@ var toolBar = (function () { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); tablet.popFromStack(); switch (message.method) { - case "newModelDialogAdd": - handleNewModelDialogResult(message.params); - break; - case "newEntityButtonClicked": - buttonHandlers[message.params.buttonName](); - break; - case "newMaterialDialogAdd": - handleNewMaterialDialogResult(message.params); - break; + case "newModelDialogAdd": + handleNewModelDialogResult(message.params); + closeExistingDialogWindow(); + break; + case "newModelDialogCancel": + closeExistingDialogWindow(); + break; + case "newEntityButtonClicked": + buttonHandlers[message.params.buttonName](); + break; + case "newMaterialDialogAdd": + handleNewMaterialDialogResult(message.params); + closeExistingDialogWindow(); + break; + case "newMaterialDialogCancel": + closeExistingDialogWindow(); + break; } } @@ -501,6 +555,13 @@ var toolBar = (function () { checkEditPermissionsAndUpdate(); }); + HMD.displayModeChanged.connect(function() { + if (isActive) { + tablet.gotoHomeScreen(); + } + that.setActive(false); + }); + Entities.canAdjustLocksChanged.connect(function (canAdjustLocks) { if (isActive && !canAdjustLocks) { that.setActive(false); @@ -527,11 +588,14 @@ var toolBar = (function () { }); createButton = activeButton; tablet.screenChanged.connect(function (type, url) { - if (isActive && (type !== "QML" || url !== "hifi/tablet/Edit.qml")) { - that.setActive(false) + var isGoingToHomescreenOnDesktop = (!shouldUseEditTabletApp() && + (url === 'hifi/tablet/TabletHome.qml' || url === '')); + if (isActive && (type !== "QML" || url !== "hifi/tablet/Edit.qml") && !isGoingToHomescreenOnDesktop) { + that.setActive(false); } }); tablet.fromQml.connect(fromQml); + createToolsWindow.fromQml.addListener(fromQml); createButton.clicked.connect(function() { if ( ! (Entities.canRez() || Entities.canRezTmp() || Entities.canRezCertified() || Entities.canRezTmpCertified()) ) { @@ -550,12 +614,29 @@ var toolBar = (function () { addButton("openAssetBrowserButton", function() { Window.showAssetServer(); }); + function createNewEntityDialogButtonCallback(entityType) { + return function() { + if (shouldUseEditTabletApp()) { + // tablet version of new-model dialog + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.pushOntoStack("hifi/tablet/New" + entityType + "Dialog.qml"); + } else { + closeExistingDialogWindow(); + var qmlPath = Script.resourcesPath() + "qml/hifi/tablet/New" + entityType + "Window.qml"; + var DIALOG_WINDOW_SIZE = { x: 500, y: 300 }; + dialogWindow = Desktop.createWindow(qmlPath, { + title: "New " + entityType + " Entity", + flags: Desktop.ALWAYS_ON_TOP | Desktop.CLOSE_BUTTON_HIDES, + presentationMode: Desktop.PresentationMode.NATIVE, + size: DIALOG_WINDOW_SIZE, + visible: true + }); + dialogWindow.fromQml.connect(fromQml); + } + }; + }; - addButton("newModelButton", function () { - // tablet version of new-model dialog - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - tablet.pushOntoStack("hifi/tablet/NewModelDialog.qml"); - }); + addButton("newModelButton", createNewEntityDialogButtonCallback("Model")); addButton("newCubeButton", function () { createNewEntity({ @@ -716,11 +797,7 @@ var toolBar = (function () { }); }); - addButton("newMaterialButton", function () { - // tablet version of new material dialog - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - tablet.pushOntoStack("hifi/tablet/NewMaterialDialog.qml"); - }); + addButton("newMaterialButton", createNewEntityDialogButtonCallback("Material")); that.setActive(false); } @@ -743,6 +820,8 @@ var toolBar = (function () { Controller.captureEntityClickEvents(); } else { Controller.releaseEntityClickEvents(); + + closeExistingDialogWindow(); } if (active === isActive) { return; @@ -769,7 +848,12 @@ var toolBar = (function () { selectionDisplay.triggerMapping.disable(); tablet.landscape = false; } else { - tablet.loadQMLSource("hifi/tablet/Edit.qml", true); + if (shouldUseEditTabletApp()) { + tablet.loadQMLSource("hifi/tablet/Edit.qml", true); + } else { + // make other apps inactive while in desktop mode + tablet.gotoHomeScreen(); + } UserActivityLogger.enabledEdit(); entityListTool.setVisible(true); gridTool.setVisible(true); @@ -790,17 +874,6 @@ var toolBar = (function () { return that; })(); - -function isLocked(properties) { - // special case to lock the ground plane model in hq. - if (location.hostname === "hq.highfidelity.io" && - properties.modelURL === HIFI_PUBLIC_BUCKET + "ozan/Terrain_Reduce_forAlpha.fbx") { - return true; - } - return false; -} - - var selectedEntityID; var orientation; var intersection; @@ -1013,15 +1086,19 @@ function mouseReleaseEvent(event) { } } -function wasTabletClicked(event) { +function wasTabletOrEditHandleClicked(event) { var rayPick = Camera.computePickRay(event.x, event.y); - var tabletIDs = getMainTabletIDs(); - if (tabletIDs.length === 0) { - return false; - } else { - var result = Overlays.findRayIntersection(rayPick, true, getMainTabletIDs()); - return result.intersects; + var result = Overlays.findRayIntersection(rayPick, true); + if (result.intersects) { + var overlayID = result.overlayID; + var tabletIDs = getMainTabletIDs(); + if (tabletIDs.indexOf(overlayID) >= 0) { + return true; + } else if (selectionDisplay.isEditHandle(overlayID)) { + return true; + } } + return false; } function mouseClickEvent(event) { @@ -1029,8 +1106,8 @@ function mouseClickEvent(event) { var result, properties, tabletClicked; if (isActive && event.isLeftButton) { result = findClickedEntity(event); - tabletClicked = wasTabletClicked(event); - if (tabletClicked) { + tabletOrEditHandleClicked = wasTabletOrEditHandleClicked(event); + if (tabletOrEditHandleClicked) { return; } @@ -1047,68 +1124,62 @@ function mouseClickEvent(event) { return; } properties = Entities.getEntityProperties(foundEntity); - if (isLocked(properties)) { - if (wantDebug) { - print("Model locked " + properties.id); + var halfDiagonal = Vec3.length(properties.dimensions) / 2.0; + + if (wantDebug) { + print("Checking properties: " + properties.id + " " + " - Half Diagonal:" + halfDiagonal); + } + // P P - Model + // /| A - Palm + // / | d B - unit vector toward tip + // / | X - base of the perpendicular line + // A---X----->B d - distance fom axis + // x x - distance from A + // + // |X-A| = (P-A).B + // X === A + ((P-A).B)B + // d = |P-X| + + var A = pickRay.origin; + var B = Vec3.normalize(pickRay.direction); + var P = properties.position; + + var x = Vec3.dot(Vec3.subtract(P, A), B); + + var angularSize = 2 * Math.atan(halfDiagonal / Vec3.distance(Camera.getPosition(), properties.position)) * + 180 / Math.PI; + + var sizeOK = (allowLargeModels || angularSize < MAX_ANGULAR_SIZE) && + (allowSmallModels || angularSize > MIN_ANGULAR_SIZE); + + if (0 < x && sizeOK) { + selectedEntityID = foundEntity; + orientation = MyAvatar.orientation; + intersection = rayPlaneIntersection(pickRay, P, Quat.getForward(orientation)); + + if (event.isShifted) { + particleExplorerTool.destroyWebView(); + } + if (properties.type !== "ParticleEffect") { + particleExplorerTool.destroyWebView(); + } + + if (!event.isShifted) { + selectionManager.setSelections([foundEntity]); + } else { + selectionManager.addEntity(foundEntity, true); } - } else { - var halfDiagonal = Vec3.length(properties.dimensions) / 2.0; if (wantDebug) { - print("Checking properties: " + properties.id + " " + " - Half Diagonal:" + halfDiagonal); + print("Model selected: " + foundEntity); } - // P P - Model - // /| A - Palm - // / | d B - unit vector toward tip - // / | X - base of the perpendicular line - // A---X----->B d - distance fom axis - // x x - distance from A - // - // |X-A| = (P-A).B - // X === A + ((P-A).B)B - // d = |P-X| + selectionDisplay.select(selectedEntityID, event); - var A = pickRay.origin; - var B = Vec3.normalize(pickRay.direction); - var P = properties.position; - - var x = Vec3.dot(Vec3.subtract(P, A), B); - - var angularSize = 2 * Math.atan(halfDiagonal / Vec3.distance(Camera.getPosition(), properties.position)) * - 180 / Math.PI; - - var sizeOK = (allowLargeModels || angularSize < MAX_ANGULAR_SIZE) && - (allowSmallModels || angularSize > MIN_ANGULAR_SIZE); - - if (0 < x && sizeOK) { - selectedEntityID = foundEntity; - orientation = MyAvatar.orientation; - intersection = rayPlaneIntersection(pickRay, P, Quat.getForward(orientation)); - - if (event.isShifted) { - particleExplorerTool.destroyWebView(); - } - if (properties.type !== "ParticleEffect") { - particleExplorerTool.destroyWebView(); - } - - if (!event.isShifted) { - selectionManager.setSelections([foundEntity]); - } else { - selectionManager.addEntity(foundEntity, true); - } - - if (wantDebug) { - print("Model selected: " + foundEntity); - } - selectionDisplay.select(selectedEntityID, event); - - if (Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)) { - cameraManager.enable(); - cameraManager.focus(selectionManager.worldPosition, - selectionManager.worldDimensions, - Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); - } + if (Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)) { + cameraManager.enable(); + cameraManager.focus(selectionManager.worldPosition, + selectionManager.worldDimensions, + Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); } } } else if (event.isRightButton) { @@ -1368,11 +1439,7 @@ function selectAllEtitiesInCurrentSelectionBox(keepIfTouching) { var localPosition = Vec3.multiplyQbyV(Quat.inverse(selectionManager.localRotation), Vec3.subtract(position, selectionManager.localPosition)); - return insideBox({ - x: 0, - y: 0, - z: 0 - }, selectionManager.localDimensions, localPosition); + return insideBox(Vec3.ZERO, selectionManager.localDimensions, localPosition); }; } for (var i = 0; i < entities.length; ++i) { @@ -1476,7 +1543,7 @@ function parentSelectedEntities() { return; } var parentCheck = false; - var lastEntityId = selectedEntities[selectedEntities.length-1]; + var lastEntityId = selectedEntities[selectedEntities.length - 1]; selectedEntities.forEach(function (id, index) { if (lastEntityId !== id) { var parentId = Entities.getEntityProperties(id, ["parentID"]).parentID; @@ -1489,7 +1556,7 @@ function parentSelectedEntities() { if (parentCheck) { Window.notify("Entities parented"); - }else { + } else { Window.notify("Entities are already parented to last"); } } else { @@ -1521,15 +1588,11 @@ function deleteSelectedEntities() { Entities.deleteEntity(entityID); } } - + if (savedProperties.length > 0) { SelectionManager.clearSelections(); pushCommandForSelections([], savedProperties); - - entityListTool.webView.emitScriptEvent(JSON.stringify({ - type: "deleted", - ids: deletedIDs - })); + entityListTool.deleteEntities(deletedIDs); } } } @@ -1782,9 +1845,9 @@ var keyReleaseEvent = function (event) { // since sometimes our menu shortcut keys don't work, trap our menu items here also and fire the appropriate menu items if (event.text === "DELETE") { deleteSelectedEntities(); - } else if (event.text === "ESC") { + } else if (event.text === 'd' && event.isControl) { selectionManager.clearSelections(); - } else if (event.text === "TAB") { + } else if (event.text === "t") { selectionDisplay.toggleSpaceMode(); } else if (event.text === "f") { if (isActive) { @@ -1801,13 +1864,7 @@ var keyReleaseEvent = function (event) { } } else if (event.text === 'g') { if (isActive && selectionManager.hasSelection()) { - var newPosition = selectionManager.worldPosition; - newPosition = Vec3.subtract(newPosition, { - x: 0, - y: selectionManager.worldDimensions.y * 0.5, - z: 0 - }); - grid.setPosition(newPosition); + grid.moveToSelection(); } } else if (event.key === KEY_P && event.isControl && !event.isAutoRepeat ) { if (event.isShifted) { @@ -1865,7 +1922,11 @@ function applyEntityProperties(data) { Entities.deleteEntity(entityID); } - selectionManager.setSelections(selectedEntityIDs); + // We might be getting an undo while edit.js is disabled. If that is the case, don't set + // our selections, causing the edit widgets to display. + if (isActive) { + selectionManager.setSelections(selectedEntityIDs); + } } // For currently selected entities, push a command to the UndoStack that uses the current entity properties for the @@ -1902,8 +1963,6 @@ function pushCommandForSelections(createdEntityData, deletedEntityData) { UndoStack.pushCommand(applyEntityProperties, undoData, applyEntityProperties, redoData); } -var ENTITY_PROPERTIES_URL = Script.resolvePath('html/entityProperties.html'); - var ServerScriptStatusMonitor = function(entityID, statusCallback) { var self = this; @@ -1947,13 +2006,14 @@ var PropertiesTool = function (opts) { var currentSelectedEntityID = null; var statusMonitor = null; - webView.setVisible(visible); - that.setVisible = function (newVisible) { visible = newVisible; - webView.setVisible(visible); + webView.setVisible(shouldUseEditTabletApp() && visible); + createToolsWindow.setVisible(!shouldUseEditTabletApp() && visible); }; + that.setVisible(false); + function updateScriptStatus(info) { info.type = "server_script_status"; webView.emitScriptEvent(JSON.stringify(info)); @@ -1982,7 +2042,7 @@ var PropertiesTool = function (opts) { statusMonitor = null; } currentSelectedEntityID = null; - } else if (currentSelectedEntityID != selectionManager.selections[0]) { + } else if (currentSelectedEntityID !== selectionManager.selections[0]) { if (statusMonitor !== null) { statusMonitor.stop(); } @@ -2008,11 +2068,14 @@ var PropertiesTool = function (opts) { selections.push(entity); } data.selections = selections; + webView.emitScriptEvent(JSON.stringify(data)); + createToolsWindow.emitScriptEvent(JSON.stringify(data)); } selectionManager.addEventListener(updateSelections); - webView.webEventReceived.connect(function (data) { + + var onWebEventReceived = function(data) { try { data = JSON.parse(data); } @@ -2034,16 +2097,8 @@ var PropertiesTool = function (opts) { } else if (data.properties) { if (data.properties.dynamic === false) { // this object is leaving dynamic, so we zero its velocities - data.properties.velocity = { - x: 0, - y: 0, - z: 0 - }; - data.properties.angularVelocity = { - x: 0, - y: 0, - z: 0 - }; + data.properties.velocity = Vec3.ZERO; + data.properties.angularVelocity = Vec3.ZERO; } if (data.properties.rotation !== undefined) { var rotation = data.properties.rotation; @@ -2171,7 +2226,11 @@ var PropertiesTool = function (opts) { } else if (data.type === "propertiesPageReady") { updateSelections(true); } - }); + }; + + createToolsWindow.webEventReceived.addListener(this, onWebEventReceived); + + webView.webEventReceived.connect(onWebEventReceived); return that; }; @@ -2186,6 +2245,8 @@ var PopupMenu = function () { var overlays = []; var overlayInfo = {}; + var visible = false; + var upColor = { red: 0, green: 0, @@ -2303,8 +2364,6 @@ var PopupMenu = function () { } }; - var visible = false; - self.setVisible = function (newVisible) { if (newVisible !== visible) { visible = newVisible; @@ -2358,7 +2417,7 @@ propertyMenu.onSelectMenuItem = function (name) { var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace"); var propertiesTool = new PropertiesTool(); -var particleExplorerTool = new ParticleExplorerTool(); +var particleExplorerTool = new ParticleExplorerTool(createToolsWindow); var selectedParticleEntityID = null; function selectParticleEntity(entityID) { @@ -2373,14 +2432,18 @@ function selectParticleEntity(entityID) { particleExplorerTool.createWebView(); particleExplorerTool.setActiveParticleEntity(entityID); - particleExplorerTool.setActiveParticleProperties(properties); // Switch to particle explorer - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - tablet.sendToQml({method: 'selectTab', params: {id: 'particle'}}); + var selectTabMethod = { method: 'selectTab', params: { id: 'particle' } }; + if (shouldUseEditTabletApp()) { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.sendToQml(selectTabMethod); + } else { + createToolsWindow.sendToQml(selectTabMethod); + } } -entityListTool.webView.webEventReceived.connect(function (data) { +entityListTool.webView.webEventReceived.connect(function(data) { try { data = JSON.parse(data); } catch(e) { diff --git a/scripts/system/emote.js b/scripts/system/emote.js index 87fc86d569..d484078b7b 100644 --- a/scripts/system/emote.js +++ b/scripts/system/emote.js @@ -16,7 +16,8 @@ (function() { // BEGIN LOCAL_SCOPE -var EMOTE_ANIMATIONS = ['Cry', 'Surprised', 'Dance', 'Cheer', 'Wave', 'Fall', 'Point', 'Clap', 'Sit1', 'Sit2', 'Sit3', 'Love']; +var EMOTE_ANIMATIONS = + ['Crying', 'Surprised', 'Dancing', 'Cheering', 'Waving', 'Fall', 'Pointing', 'Clapping', 'Sit1', 'Sit2', 'Sit3', 'Love']; var ANIMATIONS = Array(); var eventMappingName = "io.highfidelity.away"; // restoreAnimation on hand controller button events, too @@ -36,6 +37,7 @@ var EMOTE_LABEL = "EMOTE"; var EMOTE_APP_SORT_ORDER = 12; var FPS = 60; var MSEC_PER_SEC = 1000; +var FINISHED = 3; // see ScriptableResource::State var onEmoteScreen = false; var button; @@ -73,63 +75,67 @@ function onWebEventReceived(event) { if (event.type === "click") { + // Allow for a random sitting animation when a user selects sit + var randSit = Math.floor(Math.random() * 3) + 1; + var emoteName = event.data; - if (activeTimer !== false) { - Script.clearTimeout(activeTimer); + if (emoteName === "Sit"){ + emoteName = event.data + randSit; // Sit1, Sit2, Sit3 } - // If the activeEmote is different from the chosen emote, then play the new emote. Other wise, - // This is a second click on the same emote as the activeEmote, and we will just stop it. - if (activeEmote !== emoteName) { - activeEmote = emoteName; + if (ANIMATIONS[emoteName].resource.state === FINISHED) { - // Allow for a random sitting animation when a user selects sit - var randSit = Math.floor(Math.random() * 3) + 1; - if (emoteName === "Sit"){ - emoteName = event.data + randSit; // "Sit1, Sit2, Sit3" + if (activeTimer !== false) { + Script.clearTimeout(activeTimer); } - var frameCount = ANIMATIONS[emoteName].animation.frames.length; - - // Three types of emotes (non-looping end, non-looping return, looping) - if (emoteName.match(/^Sit.*$/) || emoteName === "Fall") { // non-looping end - - MyAvatar.overrideAnimation(ANIMATIONS[emoteName].url, FPS, false, 0, frameCount); - - // Non-looping return - } else if (emoteName === "Love" || emoteName === "Surprised" || emoteName === "Cry" || emoteName === "Point"){ - - MyAvatar.overrideAnimation(ANIMATIONS[emoteName].url, FPS, false, 0, frameCount); - var timeOut = MSEC_PER_SEC * frameCount / FPS; - activeTimer = Script.setTimeout(function () { - MyAvatar.restoreAnimation(); - activeTimer = false; - activeEmote = false; - }, timeOut); + // If the activeEmote is different from the chosen emote, then play the new emote + // This is a second click on the same emote as the activeEmote, and we will just stop it + if (activeEmote !== emoteName) { + activeEmote = emoteName; - } else { // Looping - - MyAvatar.overrideAnimation(ANIMATIONS[emoteName].url, FPS, true, 0, frameCount); + + // Sit is the only animation currently that plays and then ends at the last frame + if (emoteName.match(/^Sit.*$/)) { - } - - } else { - activeEmote = false; - MyAvatar.restoreAnimation(); - } + // If user provides input during a sit, the avatar animation state should be restored + Controller.keyPressEvent.connect(restoreAnimation); + Controller.enableMapping(eventMappingName); + MyAvatar.overrideAnimation(ANIMATIONS[emoteName].url, FPS, false, 0, frameCount); + + } else { + + activeEmote = emoteName; + var frameCount = ANIMATIONS[emoteName].animation.frames.length; + MyAvatar.overrideAnimation(ANIMATIONS[emoteName].url, FPS, false, 0, frameCount); - + var timeOut = MSEC_PER_SEC * frameCount / FPS; + activeTimer = Script.setTimeout(function () { + MyAvatar.restoreAnimation(); + activeTimer = false; + activeEmote = false; + }, timeOut); + + } + + } else { + activeEmote = false; + MyAvatar.restoreAnimation(); + } + } } } -// If a user provides input, end the emote animation and restore the navigation animation states (idle, walk, run) +// Restore the navigation animation states (idle, walk, run) function restoreAnimation() { MyAvatar.restoreAnimation(); + + // Make sure the input is disconnected after animations are restored so it doesn't affect any emotes other than sit + Controller.keyPressEvent.disconnect(restoreAnimation); + Controller.disableMapping(eventMappingName); } - -Controller.keyPressEvent.connect(restoreAnimation); - + // Note peek() so as to not interfere with other mappings. eventMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(restoreAnimation); eventMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(restoreAnimation); @@ -137,13 +143,17 @@ eventMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(restoreAnima eventMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(restoreAnimation); eventMapping.from(Controller.Standard.LB).peek().to(restoreAnimation); eventMapping.from(Controller.Standard.LS).peek().to(restoreAnimation); +eventMapping.from(Controller.Standard.RY).peek().to(restoreAnimation); +eventMapping.from(Controller.Standard.RX).peek().to(restoreAnimation); +eventMapping.from(Controller.Standard.LY).peek().to(restoreAnimation); +eventMapping.from(Controller.Standard.LX).peek().to(restoreAnimation); eventMapping.from(Controller.Standard.LeftGrip).peek().to(restoreAnimation); eventMapping.from(Controller.Standard.RB).peek().to(restoreAnimation); eventMapping.from(Controller.Standard.RS).peek().to(restoreAnimation); eventMapping.from(Controller.Standard.RightGrip).peek().to(restoreAnimation); eventMapping.from(Controller.Standard.Back).peek().to(restoreAnimation); eventMapping.from(Controller.Standard.Start).peek().to(restoreAnimation); -Controller.enableMapping(eventMappingName); + button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); diff --git a/scripts/system/html/EmoteApp.html b/scripts/system/html/EmoteApp.html index 16dee490a9..6b42fb8dc8 100644 --- a/scripts/system/html/EmoteApp.html +++ b/scripts/system/html/EmoteApp.html @@ -98,14 +98,14 @@

Choose an emote:

-

+

-

-

-

+

+

+

-

-

+

+

diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index 0c3e6199f3..91866605a4 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -668,6 +668,7 @@ window.onload = function () { addImage(element, messageOptions.isLoggedIn, idx === 0 && messageOptions.canShare, idx === 1, false, false, false, true); }); document.getElementById("p1").classList.add("processingGif"); + document.getElementById("snap-button").disabled = true; } else { var gifPath = message.image_data[0].localPath, p1img = document.getElementById('p1img'); @@ -677,14 +678,15 @@ window.onload = function () { shareForUrl("p1"); appendShareBar("p1", messageOptions.isLoggedIn, messageOptions.canShare, true, false, false, messageOptions.canBlast); document.getElementById("p1").classList.remove("processingGif"); + document.getElementById("snap-button").disabled = false; } } else { imageCount = message.image_data.length; message.image_data.forEach(function (element) { addImage(element, messageOptions.isLoggedIn, messageOptions.canShare, false, false, false, false, true); }); + document.getElementById("snap-button").disabled = false; } - document.getElementById("snap-button").disabled = false; break; case 'captureSettings': handleCaptureSetting(message.setting); @@ -701,7 +703,7 @@ window.onload = function () { case 'snapshotUploadComplete': var isGif = fileExtensionMatches(message.image_url, "gif"); updateShareInfo(isGif ? "p1" : "p0", message.story_id); - if (isPrintProcessing()) { + if (isPrintProcessing()) { setPrintButtonEnabled(); } break; @@ -724,11 +726,11 @@ function snapshotSettings() { })); } function takeSnapshot() { + document.getElementById("snap-button").disabled = true; EventBridge.emitWebEvent(JSON.stringify({ type: "snapshot", action: "takeSnapshot" })); - document.getElementById("snap-button").disabled = true; } function isPrintDisabled() { @@ -739,14 +741,14 @@ function isPrintDisabled() { document.getElementById('print-button').disabled; } function isPrintProcessing() { - var printElement = document.getElementById('print-icon'); + var printElement = document.getElementById('print-icon'); return printElement.classList.contains("print-icon") && printElement.classList.contains("print-icon-loading") && document.getElementById('print-button').disabled; } function isPrintEnabled() { - var printElement = document.getElementById('print-icon'); + var printElement = document.getElementById('print-icon'); return printElement.classList.contains("print-icon") && printElement.classList.contains("print-icon-default") && @@ -773,8 +775,8 @@ function requestPrintButtonUpdate() { })); } -function printToPolaroid() { - if (isPrintEnabled()) { +function printToPolaroid() { + if (isPrintEnabled()) { EventBridge.emitWebEvent(JSON.stringify({ type: "snapshot", action: "printToPolaroid" diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 5dfb0d5b69..e817bb4ee1 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -59,6 +59,7 @@ highlightTargetEntity:true, clearHighlightedEntities:true, unhighlightTargetEntity:true + distanceBetweenEntityLocalPositionAndBoundingBox: true */ MSECS_PER_SEC = 1000.0; @@ -130,7 +131,9 @@ DISPATCHER_PROPERTIES = [ "type", "href", "cloneable", - "cloneDynamic" + "cloneDynamic", + "localPosition", + "localRotation" ]; // priority -- a lower priority means the module will be asked sooner than one with a higher priority in a given update step @@ -413,6 +416,25 @@ findHandChildEntities = function(hand) { }); }; +distanceBetweenEntityLocalPositionAndBoundingBox = function(entityProps) { + var localPoint = entityProps.localPosition; + var entityXform = new Xform(entityProps.rotation, entityProps.position); + var minOffset = Vec3.multiplyVbyV(entityProps.registrationPoint, entityProps.dimensions); + var maxOffset = Vec3.multiplyVbyV(Vec3.subtract(ONE_VEC, entityProps.registrationPoint), entityProps.dimensions); + var localMin = Vec3.subtract(entityXform.trans, minOffset); + var localMax = Vec3.sum(entityXform.trans, maxOffset); + + var v = {x: localPoint.x, y: localPoint.y, z: localPoint.z}; + v.x = Math.max(v.x, localMin.x); + v.x = Math.min(v.x, localMax.x); + v.y = Math.max(v.y, localMin.y); + v.y = Math.min(v.y, localMax.y); + v.z = Math.max(v.z, localMin.z); + v.z = Math.min(v.z, localMax.z); + + return Vec3.distance(v, localPoint); +}; + distanceBetweenPointAndEntityBoundingBox = function(point, entityProps) { var entityXform = new Xform(entityProps.rotation, entityProps.position); var localPoint = entityXform.inv().xformPoint(point); diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 3fda7588df..fb876302dd 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -11,27 +11,64 @@ /* global EntityListTool, Tablet, selectionManager, Entities, Camera, MyAvatar, Vec3, Menu, Messages, cameraManager, MENU_EASE_ON_FOCUS, deleteSelectedEntities, toggleSelectedEntitiesLocked, toggleSelectedEntitiesVisible */ -EntityListTool = function(opts) { +EntityListTool = function(shouldUseEditTabletApp) { var that = {}; + var CreateWindow = Script.require('../modules/createWindow.js'); + + var TITLE_OFFSET = 60; + var ENTITY_LIST_WIDTH = 495; + var MAX_DEFAULT_CREATE_TOOLS_HEIGHT = 778; + var entityListWindow = new CreateWindow( + Script.resourcesPath() + "qml/hifi/tablet/EditEntityList.qml", + 'Entity List', + 'com.highfidelity.create.entityListWindow', + function () { + var windowHeight = Window.innerHeight - TITLE_OFFSET; + if (windowHeight > MAX_DEFAULT_CREATE_TOOLS_HEIGHT) { + windowHeight = MAX_DEFAULT_CREATE_TOOLS_HEIGHT; + } + return { + size: { + x: ENTITY_LIST_WIDTH, + y: windowHeight + }, + position: { + x: Window.x, + y: Window.y + TITLE_OFFSET + } + }; + }, + false + ); + var webView = null; webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - webView.setVisible = function(value) {}; + webView.setVisible = function(value){ }; var filterInView = false; var searchRadius = 100; var visible = false; - webView.setVisible(visible); - that.webView = webView; that.setVisible = function(newVisible) { visible = newVisible; - webView.setVisible(visible); + webView.setVisible(shouldUseEditTabletApp() && visible); + entityListWindow.setVisible(!shouldUseEditTabletApp() && visible); }; + that.setVisible(false); + + function emitJSONScriptEvent(data) { + var dataString = JSON.stringify(data); + webView.emitScriptEvent(dataString); + if (entityListWindow.window) { + entityListWindow.window.emitScriptEvent(dataString); + } + } + that.toggleVisible = function() { that.setVisible(!visible); }; @@ -43,27 +80,31 @@ EntityListTool = function(opts) { selectedIDs.push(selectionManager.selections[i]); } - var data = { + emitJSONScriptEvent({ type: 'selectionUpdate', - selectedIDs: selectedIDs, - }; - webView.emitScriptEvent(JSON.stringify(data)); + selectedIDs: selectedIDs + }); }); - that.clearEntityList = function () { - var data = { + that.clearEntityList = function() { + emitJSONScriptEvent({ type: 'clearEntityList' - }; - webView.emitScriptEvent(JSON.stringify(data)); + }); }; that.removeEntities = function (deletedIDs, selectedIDs) { - var data = { + emitJSONScriptEvent({ type: 'removeEntities', deletedIDs: deletedIDs, selectedIDs: selectedIDs - }; - webView.emitScriptEvent(JSON.stringify(data)); + }); + }; + + that.deleteEntities = function (deletedIDs) { + emitJSONScriptEvent({ + type: "deleted", + ids: deletedIDs + }); }; function valueIfDefined(value) { @@ -87,9 +128,9 @@ EntityListTool = function(opts) { if (!filterInView || Vec3.distance(properties.position, cameraPosition) <= searchRadius) { var url = ""; - if (properties.type == "Model") { + if (properties.type === "Model") { url = properties.modelURL; - } else if (properties.type == "Material") { + } else if (properties.type === "Material") { url = properties.materialURL; } entities.push({ @@ -99,12 +140,17 @@ EntityListTool = function(opts) { url: url, locked: properties.locked, visible: properties.visible, - verticesCount: valueIfDefined(properties.renderInfo.verticesCount), - texturesCount: valueIfDefined(properties.renderInfo.texturesCount), - texturesSize: valueIfDefined(properties.renderInfo.texturesSize), - hasTransparent: valueIfDefined(properties.renderInfo.hasTransparent), - isBaked: properties.type == "Model" ? url.toLowerCase().endsWith(".baked.fbx") : false, - drawCalls: valueIfDefined(properties.renderInfo.drawCalls), + verticesCount: (properties.renderInfo !== undefined ? + valueIfDefined(properties.renderInfo.verticesCount) : ""), + texturesCount: (properties.renderInfo !== undefined ? + valueIfDefined(properties.renderInfo.texturesCount) : ""), + texturesSize: (properties.renderInfo !== undefined ? + valueIfDefined(properties.renderInfo.texturesSize) : ""), + hasTransparent: (properties.renderInfo !== undefined ? + valueIfDefined(properties.renderInfo.hasTransparent) : ""), + isBaked: properties.type === "Model" ? url.toLowerCase().endsWith(".baked.fbx") : false, + drawCalls: (properties.renderInfo !== undefined ? + valueIfDefined(properties.renderInfo.drawCalls) : ""), hasScript: properties.script !== "" }); } @@ -115,12 +161,11 @@ EntityListTool = function(opts) { selectedIDs.push(selectionManager.selections[j]); } - var data = { + emitJSONScriptEvent({ type: "update", entities: entities, selectedIDs: selectedIDs, - }; - webView.emitScriptEvent(JSON.stringify(data)); + }); }; function onFileSaveChanged(filename) { @@ -133,15 +178,15 @@ EntityListTool = function(opts) { } } - webView.webEventReceived.connect(function(data) { + var onWebEventReceived = function(data) { try { data = JSON.parse(data); } catch(e) { - print("entityList.js: Error parsing JSON: " + e.name + " data " + data) + print("entityList.js: Error parsing JSON: " + e.name + " data " + data); return; } - if (data.type == "selectionUpdate") { + if (data.type === "selectionUpdate") { var ids = data.entityIds; var entityIDs = []; for (var i = 0; i < ids.length; i++) { @@ -154,20 +199,20 @@ EntityListTool = function(opts) { selectionManager.worldDimensions, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); } - } else if (data.type == "refresh") { + } else if (data.type === "refresh") { that.sendUpdate(); - } else if (data.type == "teleport") { + } else if (data.type === "teleport") { if (selectionManager.hasSelection()) { MyAvatar.position = selectionManager.worldPosition; } - } else if (data.type == "export") { + } else if (data.type === "export") { if (!selectionManager.hasSelection()) { Window.notifyEditError("No entities have been selected."); } else { Window.saveFileChanged.connect(onFileSaveChanged); Window.saveAsync("Select Where to Save", "", "*.json"); } - } else if (data.type == "pal") { + } else if (data.type === "pal") { var sessionIds = {}; // Collect the sessionsIds of all selected entitities, w/o duplicates. selectionManager.selections.forEach(function (id) { var lastEditedBy = Entities.getEntityProperties(id, 'lastEditedBy').lastEditedBy; @@ -184,24 +229,21 @@ EntityListTool = function(opts) { // No need to subscribe if we're just sending. Messages.sendMessage('com.highfidelity.pal', JSON.stringify({method: 'select', params: [dedupped, true, false]}), 'local'); } - } else if (data.type == "delete") { + } else if (data.type === "delete") { deleteSelectedEntities(); - } else if (data.type == "toggleLocked") { + } else if (data.type === "toggleLocked") { toggleSelectedEntitiesLocked(); - } else if (data.type == "toggleVisible") { + } else if (data.type === "toggleVisible") { toggleSelectedEntitiesVisible(); } else if (data.type === "filterInView") { filterInView = data.filterInView === true; } else if (data.type === "radius") { searchRadius = data.radius; } - }); + }; - // webView.visibleChanged.connect(function () { - // if (webView.visible) { - // that.sendUpdate(); - // } - // }); + webView.webEventReceived.connect(onWebEventReceived); + entityListWindow.webEventReceived.addListener(onWebEventReceived); return that; }; diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index cd3c9fe418..96a3b2b015 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -13,7 +13,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global SelectionManager, grid, rayPlaneIntersection, rayPlaneIntersection2, pushCommandForSelections, +/* global SelectionManager, SelectionDisplay, grid, rayPlaneIntersection, rayPlaneIntersection2, pushCommandForSelections, getMainTabletIDs, getControllerWorldLocation */ var SPACE_LOCAL = "local"; @@ -72,7 +72,9 @@ SelectionManager = (function() { subscribeToUpdateMessages(); - var COLOR_ORANGE_HIGHLIGHT = { red: 255, green: 99, blue: 9 } + // disabling this for now as it is causing rendering issues with the other handle overlays + /* + var COLOR_ORANGE_HIGHLIGHT = { red: 255, green: 99, blue: 9 }; var editHandleOutlineStyle = { outlineUnoccludedColor: COLOR_ORANGE_HIGHLIGHT, outlineOccludedColor: COLOR_ORANGE_HIGHLIGHT, @@ -85,8 +87,8 @@ SelectionManager = (function() { outlineWidth: 3, isOutlineSmooth: true }; - // disabling this for now as it is causing rendering issues with the other handle overlays - //Selection.enableListHighlight(HIGHLIGHT_LIST_NAME, editHandleOutlineStyle); + Selection.enableListHighlight(HIGHLIGHT_LIST_NAME, editHandleOutlineStyle); + */ that.savedProperties = {}; that.selections = []; @@ -180,18 +182,67 @@ SelectionManager = (function() { that.selections = []; that._update(true); }; + + that.addChildrenEntities = function(parentEntityID, entityList) { + var children = Entities.getChildrenIDs(parentEntityID); + for (var i = 0; i < children.length; i++) { + var childID = children[i]; + if (entityList.indexOf(childID) < 0) { + entityList.push(childID); + } + that.addChildrenEntities(childID, entityList); + } + }; that.duplicateSelection = function() { + var entitiesToDuplicate = []; var duplicatedEntityIDs = []; - Object.keys(that.savedProperties).forEach(function(otherEntityID) { - var properties = that.savedProperties[otherEntityID]; + var duplicatedChildrenWithOldParents = []; + var originalEntityToNewEntityID = []; + + // build list of entities to duplicate by including any unselected children of selected parent entities + Object.keys(that.savedProperties).forEach(function(originalEntityID) { + if (entitiesToDuplicate.indexOf(originalEntityID) < 0) { + entitiesToDuplicate.push(originalEntityID); + } + that.addChildrenEntities(originalEntityID, entitiesToDuplicate); + }); + + // duplicate entities from above and store their original to new entity mappings and children needing re-parenting + for (var i = 0; i < entitiesToDuplicate.length; i++) { + var originalEntityID = entitiesToDuplicate[i]; + var properties = that.savedProperties[originalEntityID]; + if (properties === undefined) { + properties = Entities.getEntityProperties(originalEntityID); + } if (!properties.locked && (!properties.clientOnly || properties.owningAvatarID === MyAvatar.sessionUUID)) { + var newEntityID = Entities.addEntity(properties); duplicatedEntityIDs.push({ - entityID: Entities.addEntity(properties), + entityID: newEntityID, properties: properties }); + if (properties.parentID !== Uuid.NULL) { + duplicatedChildrenWithOldParents[newEntityID] = properties.parentID; + } + originalEntityToNewEntityID[originalEntityID] = newEntityID; + } + } + + // re-parent duplicated children to the duplicate entities of their original parents (if they were duplicated) + Object.keys(duplicatedChildrenWithOldParents).forEach(function(childIDNeedingNewParent) { + var originalParentID = duplicatedChildrenWithOldParents[childIDNeedingNewParent]; + var newParentID = originalEntityToNewEntityID[originalParentID]; + if (newParentID) { + Entities.editEntity(childIDNeedingNewParent, { parentID: newParentID }); + for (var i = 0; i < duplicatedEntityIDs.length; i++) { + var duplicatedEntity = duplicatedEntityIDs[i]; + if (duplicatedEntity.entityID === childIDNeedingNewParent) { + duplicatedEntity.properties.parentID = newParentID; + } + } } }); + return duplicatedEntityIDs; }; @@ -215,8 +266,10 @@ SelectionManager = (function() { that.worldRotation = properties.boundingBox.rotation; that.entityType = properties.type; - - SelectionDisplay.setSpaceMode(SPACE_LOCAL); + + if (selectionUpdated) { + SelectionDisplay.setSpaceMode(SPACE_LOCAL); + } } else { that.localRotation = null; that.localDimensions = null; @@ -360,8 +413,6 @@ SelectionDisplay = (function() { var spaceMode = SPACE_LOCAL; var overlayNames = []; - var lastCameraPosition = Camera.getPosition(); - var lastCameraOrientation = Camera.getOrientation(); var lastControllerPoses = [ getControllerWorldLocation(Controller.Standard.LeftHand, true), getControllerWorldLocation(Controller.Standard.RightHand, true) @@ -381,7 +432,7 @@ SelectionDisplay = (function() { var ctrlPressed = false; - var replaceCollisionsAfterStretch = false; + that.replaceCollisionsAfterStretch = false; var handlePropertiesTranslateArrowCones = { shape: "Cone", @@ -607,6 +658,7 @@ SelectionDisplay = (function() { selectionBox, iconSelectionBox ]; + var maximumHandleInAllOverlays = handleCloner; overlayNames[handleTranslateXCone] = "handleTranslateXCone"; overlayNames[handleTranslateXCylinder] = "handleTranslateXCylinder"; @@ -730,6 +782,12 @@ SelectionDisplay = (function() { return Math.abs(position.x) <= box.dimensions.x / 2 && Math.abs(position.y) <= box.dimensions.y / 2 && Math.abs(position.z) <= box.dimensions.z / 2; } + + that.isEditHandle = function(overlayID) { + var overlayIndex = allOverlays.indexOf(overlayID); + var maxHandleIndex = allOverlays.indexOf(maximumHandleInAllOverlays); + return overlayIndex >= 0 && overlayIndex <= maxHandleIndex; + }; // FUNCTION: MOUSE PRESS EVENT that.mousePressEvent = function (event) { @@ -967,7 +1025,7 @@ SelectionDisplay = (function() { if (SelectionManager.hasSelection()) { var controllerPose = getControllerWorldLocation(activeHand, true); var hand = (activeHand === Controller.Standard.LeftHand) ? 0 : 1; - if (controllerPose.valid && lastControllerPoses[hand].valid) { + if (controllerPose.valid && lastControllerPoses[hand].valid && that.triggered) { if (!Vec3.equal(controllerPose.position, lastControllerPoses[hand].position) || !Vec3.equal(controllerPose.rotation, lastControllerPoses[hand].rotation)) { that.mouseMoveEvent({}); @@ -1017,9 +1075,6 @@ SelectionDisplay = (function() { that.select = function(entityID, event) { var properties = Entities.getEntityProperties(SelectionManager.selections[0]); - lastCameraPosition = Camera.getPosition(); - lastCameraOrientation = Camera.getOrientation(); - if (event !== false) { var wantDebug = false; if (wantDebug) { @@ -1138,10 +1193,17 @@ SelectionDisplay = (function() { var localRotationZ = Quat.fromPitchYawRollDegrees(rotationDegrees, 0, 0); var rotationZ = Quat.multiply(rotation, localRotationZ); worldRotationZ = rotationZ; + + var selectionBoxGeometry = { + position: position, + rotation: rotation, + dimensions: dimensions + }; + var isCameraInsideBox = isPointInsideBox(Camera.position, selectionBoxGeometry); - // in HMD we clamp the overlays to the bounding box for now so lasers can hit them + // in HMD if outside the bounding box clamp the overlays to the bounding box for now so lasers can hit them var maxHandleDimension = 0; - if (HMD.active) { + if (HMD.active && !isCameraInsideBox) { maxHandleDimension = Math.max(dimensions.x, dimensions.y, dimensions.z); } @@ -1390,12 +1452,6 @@ SelectionDisplay = (function() { var inModeRotate = isActiveTool(handleRotatePitchRing) || isActiveTool(handleRotateYawRing) || isActiveTool(handleRotateRollRing); - var selectionBoxGeometry = { - position: position, - rotation: rotation, - dimensions: dimensions - }; - var isCameraInsideBox = isPointInsideBox(Camera.position, selectionBoxGeometry); selectionBoxGeometry.visible = !inModeRotate && !isCameraInsideBox; Overlays.editOverlay(selectionBox, selectionBoxGeometry); @@ -1435,7 +1491,7 @@ SelectionDisplay = (function() { that.setHandleRotateYawVisible(!activeTool || isActiveTool(handleRotateYawRing)); that.setHandleRotateRollVisible(!activeTool || isActiveTool(handleRotateRollRing)); - var showScaleStretch = !activeTool && SelectionManager.selections.length === 1; + var showScaleStretch = !activeTool && SelectionManager.selections.length === 1 && spaceMode === SPACE_LOCAL; that.setHandleStretchXVisible(showScaleStretch || isActiveTool(handleStretchXSphere)); that.setHandleStretchYVisible(showScaleStretch || isActiveTool(handleStretchYSphere)); that.setHandleStretchZVisible(showScaleStretch || isActiveTool(handleStretchZSphere)); @@ -1617,7 +1673,7 @@ SelectionDisplay = (function() { translateXZTool.pickPlanePosition = pickResult.intersection; translateXZTool.greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, - SelectionManager.worldDimensions.y), + SelectionManager.worldDimensions.y), SelectionManager.worldDimensions.z); translateXZTool.startingDistance = Vec3.distance(pickRay.origin, SelectionManager.position); translateXZTool.startingElevation = translateXZTool.elevation(pickRay.origin, translateXZTool.pickPlanePosition); @@ -1767,23 +1823,27 @@ SelectionDisplay = (function() { function addHandleTranslateTool(overlay, mode, direction) { var pickNormal = null; var lastPick = null; + var initialPosition = null; var projectionVector = null; var previousPickRay = null; addHandleTool(overlay, { mode: mode, onBegin: function(event, pickRay, pickResult) { + var axisVector; if (direction === TRANSLATE_DIRECTION.X) { - pickNormal = { x: 0, y: 1, z: 1 }; + axisVector = { x: 1, y: 0, z: 0 }; } else if (direction === TRANSLATE_DIRECTION.Y) { - pickNormal = { x: 1, y: 0, z: 1 }; + axisVector = { x: 0, y: 1, z: 0 }; } else if (direction === TRANSLATE_DIRECTION.Z) { - pickNormal = { x: 1, y: 1, z: 0 }; + axisVector = { x: 0, y: 0, z: 1 }; } var rotation = spaceMode === SPACE_LOCAL ? SelectionManager.localRotation : SelectionManager.worldRotation; - pickNormal = Vec3.multiplyQbyV(rotation, pickNormal); + axisVector = Vec3.multiplyQbyV(rotation, axisVector); + pickNormal = Vec3.cross(Vec3.cross(pickRay.direction, axisVector), axisVector); lastPick = rayPlaneIntersection(pickRay, SelectionManager.worldPosition, pickNormal); + initialPosition = SelectionManager.worldPosition; SelectionManager.saveProperties(); that.resetPreviousHandleColor(); @@ -1818,7 +1878,7 @@ SelectionDisplay = (function() { pickRay = previousPickRay; } - var newIntersection = rayPlaneIntersection(pickRay, SelectionManager.worldPosition, pickNormal); + var newIntersection = rayPlaneIntersection(pickRay, initialPosition, pickNormal); var vector = Vec3.subtract(newIntersection, lastPick); if (direction === TRANSLATE_DIRECTION.X) { @@ -1834,7 +1894,8 @@ SelectionDisplay = (function() { var dotVector = Vec3.dot(vector, projectionVector); vector = Vec3.multiply(dotVector, projectionVector); - vector = grid.snapToGrid(vector); + var gridOrigin = grid.getOrigin(); + vector = Vec3.subtract(grid.snapToGrid(Vec3.sum(vector, gridOrigin)), gridOrigin); var wantDebug = false; if (wantDebug) { @@ -2061,7 +2122,7 @@ SelectionDisplay = (function() { }; var onMove = function(event) { - var proportional = (spaceMode === SPACE_WORLD) || directionEnum === STRETCH_DIRECTION.ALL; + var proportional = directionEnum === STRETCH_DIRECTION.ALL; var position, rotation; if (spaceMode === SPACE_LOCAL) { @@ -2134,8 +2195,8 @@ SelectionDisplay = (function() { newDimensions = Vec3.sum(initialDimensions, changeInDimensions); } - var minimumDimension = directionEnum === STRETCH_DIRECTION.ALL ? STRETCH_ALL_MINIMUM_DIMENSION : - STRETCH_MINIMUM_DIMENSION; + var minimumDimension = directionEnum === + STRETCH_DIRECTION.ALL ? STRETCH_ALL_MINIMUM_DIMENSION : STRETCH_MINIMUM_DIMENSION; if (newDimensions.x < minimumDimension) { newDimensions.x = minimumDimension; changeInDimensions.x = minimumDimension - initialDimensions.x; @@ -2229,8 +2290,7 @@ SelectionDisplay = (function() { selectedHandle = handleScaleRTFCube; } offset = Vec3.multiply(directionVector, NEGATE_VECTOR); - var tool = makeStretchTool(mode, STRETCH_DIRECTION.ALL, directionVector, - directionVector, offset, null, selectedHandle); + var tool = makeStretchTool(mode, STRETCH_DIRECTION.ALL, directionVector, directionVector, offset, null, selectedHandle); return addHandleTool(overlay, tool); } diff --git a/scripts/system/libraries/gridTool.js b/scripts/system/libraries/gridTool.js index 3be6ac0b00..3a114f23c7 100644 --- a/scripts/system/libraries/gridTool.js +++ b/scripts/system/libraries/gridTool.js @@ -1,6 +1,6 @@ var GRID_CONTROLS_HTML_URL = Script.resolvePath('../html/gridControls.html'); -Grid = function(opts) { +Grid = function() { var that = {}; var gridColor = { red: 0, green: 0, blue: 0 }; var gridAlpha = 0.6; @@ -154,6 +154,12 @@ Grid = function(opts) { that.emitUpdate(); } }; + + that.moveToSelection = function() { + var newPosition = SelectionManager.worldPosition; + newPosition = Vec3.subtract(newPosition, { x: 0, y: SelectionManager.worldDimensions.y * 0.5, z: 0 }); + that.setPosition(newPosition); + }; that.emitUpdate = function() { if (that.onUpdate) { @@ -240,6 +246,8 @@ GridTool = function(opts) { var horizontalGrid = opts.horizontalGrid; var verticalGrid = opts.verticalGrid; + var createToolsWindow = opts.createToolsWindow; + var shouldUseEditTabletApp = opts.shouldUseEditTabletApp; var listeners = []; var webView = null; @@ -247,13 +255,15 @@ GridTool = function(opts) { webView.setVisible = function(value) { }; horizontalGrid.addListener(function(data) { - webView.emitScriptEvent(JSON.stringify(data)); + var dataString = JSON.stringify(data); + webView.emitScriptEvent(dataString); + createToolsWindow.emitScriptEvent(dataString); if (selectionDisplay) { selectionDisplay.updateHandles(); } }); - webView.webEventReceived.connect(function(data) { + var webEventReceived = function(data) { try { data = JSON.parse(data); } catch (e) { @@ -277,19 +287,20 @@ GridTool = function(opts) { } horizontalGrid.setPosition(position); } else if (action == "moveToSelection") { - var newPosition = selectionManager.worldPosition; - newPosition = Vec3.subtract(newPosition, { x: 0, y: selectionManager.worldDimensions.y * 0.5, z: 0 }); - grid.setPosition(newPosition); + horizontalGrid.moveToSelection(); } } - }); + }; + + webView.webEventReceived.connect(webEventReceived); + createToolsWindow.webEventReceived.addListener(webEventReceived); that.addListener = function(callback) { listeners.push(callback); }; that.setVisible = function(visible) { - webView.setVisible(visible); + webView.setVisible(shouldUseEditTabletApp() && visible); }; return that; diff --git a/scripts/system/modules/createWindow.js b/scripts/system/modules/createWindow.js new file mode 100644 index 0000000000..185991d2ef --- /dev/null +++ b/scripts/system/modules/createWindow.js @@ -0,0 +1,151 @@ +"use strict"; + +// createWindow.js +// +// Created by Thijs Wenker on 6/1/18 +// +// Copyright 2018 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 getWindowRect = function(settingsKey, defaultRect) { + var windowRect = Settings.getValue(settingsKey, defaultRect); + return windowRect; +}; + +var setWindowRect = function(settingsKey, position, size) { + Settings.setValue(settingsKey, { + position: position, + size: size + }); +}; + +var CallableEvent = (function() { + function CallableEvent() { + this.callbacks = []; + } + + CallableEvent.prototype = { + callbacks: null, + call: function () { + var callArguments = arguments; + this.callbacks.forEach(function(callbackObject) { + try { + callbackObject.callback.apply(callbackObject.context ? callbackObject.context : this, callArguments); + } catch (e) { + console.error('Call to CallableEvent callback failed!'); + } + }); + }, + addListener: function(contextOrCallback, callback) { + if (callback) { + this.callbacks.push({ + context: contextOrCallback, + callback: callback + }); + } else { + this.callbacks.push({ + callback: contextOrCallback + }); + } + }, + removeListener: function(callback) { + var foundIndex = -1; + this.callbacks.forEach(function (callbackObject, index) { + if (callbackObject.callback === callback) { + foundIndex = index; + } + }); + + if (foundIndex !== -1) { + this.callbacks.splice(foundIndex, 1); + } + } + }; + + return CallableEvent; +})(); + +module.exports = (function() { + function CreateWindow(qmlPath, title, settingsKey, defaultRect, createOnStartup) { + this.qmlPath = qmlPath; + this.title = title; + this.settingsKey = settingsKey; + this.defaultRect = defaultRect; + this.webEventReceived = new CallableEvent(); + this.fromQml = new CallableEvent(); + if (createOnStartup) { + this.createWindow(); + } + } + + CreateWindow.prototype = { + window: null, + createWindow: function() { + var defaultRect = this.defaultRect; + if (typeof this.defaultRect === "function") { + defaultRect = this.defaultRect(); + } + + var windowRect = getWindowRect(this.settingsKey, defaultRect); + this.window = Desktop.createWindow(this.qmlPath, { + title: this.title, + flags: Desktop.ALWAYS_ON_TOP | Desktop.CLOSE_BUTTON_HIDES, + presentationMode: Desktop.PresentationMode.NATIVE, + size: windowRect.size, + visible: true, + position: windowRect.position + }); + + var windowRectChanged = function () { + if (this.window.visible) { + setWindowRect(this.settingsKey, this.window.position, this.window.size); + } + }; + + this.window.sizeChanged.connect(this, windowRectChanged); + this.window.positionChanged.connect(this, windowRectChanged); + + this.window.webEventReceived.connect(this, function (data) { + this.webEventReceived.call(data); + }); + + this.window.fromQml.connect(this, function (data) { + this.fromQml.call(data); + }); + + Script.scriptEnding.connect(this, function() { + this.window.close(); + }); + }, + setVisible: function(visible) { + if (visible && !this.window) { + this.createWindow(); + } + + if (this.window) { + if (visible) { + this.window.show(); + } else { + this.window.visible = false; + } + } + }, + emitScriptEvent: function(data) { + if (this.window) { + this.window.emitScriptEvent(data); + } + }, + sendToQml: function(data) { + if (this.window) { + this.window.sendToQml(data); + } + }, + webEventReceived: null, + fromQml: null + }; + + return CreateWindow; +})(); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 7175685b4f..9485b8b49a 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -12,14 +12,15 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -(function() { // BEGIN LOCAL_SCOPE +(function () { // BEGIN LOCAL_SCOPE - var request = Script.require('request').request; +var request = Script.require('request').request; +var AppUi = Script.require('appUi'); var populateNearbyUserList, color, textures, removeOverlays, - controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged, + controllerComputePickRay, off, receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, - createAudioInterval, tablet, CHANNEL, getConnectionData, findableByChanged, + CHANNEL, getConnectionData, findableByChanged, avatarAdded, avatarRemoved, avatarSessionChanged; // forward references; // hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed @@ -40,6 +41,7 @@ var HOVER_TEXTURES = { var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6}; var SELECTED_COLOR = {red: 0xF3, green: 0x91, blue: 0x29}; var HOVER_COLOR = {red: 0xD0, green: 0xD0, blue: 0xD0}; // almost white for now +var METAVERSE_BASE = Account.metaverseServerURL; Script.include("/~/system/libraries/controllers.js"); @@ -221,7 +223,7 @@ function convertDbToLinear(decibels) { return Math.pow(2, decibels / 10.0); } function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. - var data; + var data, connectionUserName, friendUserName; switch (message.method) { case 'selected': selectedIds = message.params; @@ -281,7 +283,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See } getConnectionData(false); }); - break + break; case 'removeFriend': friendUserName = message.params; @@ -296,7 +298,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See } getConnectionData(friendUserName); }); - break + break; case 'addFriend': friendUserName = message.params; print("Adding " + friendUserName + " to friends."); @@ -307,24 +309,23 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See body: { username: friendUserName, } - }, function (error, response) { - if (error || (response.status !== 'success')) { - print("Error: unable to friend " + friendUserName, error || response.status); - return; - } - getConnectionData(friendUserName); + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("Error: unable to friend " + friendUserName, error || response.status); + return; } - ); + getConnectionData(friendUserName); + }); break; case 'http.request': - break; // Handled by request-service. + break; // Handled by request-service. default: print('Unrecognized message from Pal.qml:', JSON.stringify(message)); } } function sendToQml(message) { - tablet.sendToQml(message); + ui.sendMessage(message); } function updateUser(data) { print('PAL update:', JSON.stringify(data)); @@ -334,7 +335,6 @@ function updateUser(data) { // User management services // // These are prototype versions that will be changed when the back end changes. -var METAVERSE_BASE = Account.metaverseServerURL; function requestJSON(url, callback) { // callback(data) if successfull. Logs otherwise. request({ @@ -362,7 +362,7 @@ function getProfilePicture(username, callback) { // callback(url) if successfull }); } function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise) - url = METAVERSE_BASE + '/api/v1/users?per_page=400&' + var url = METAVERSE_BASE + '/api/v1/users?per_page=400&'; if (domain) { url += 'status=' + domain.slice(1, -1); // without curly braces } else { @@ -373,7 +373,7 @@ function getAvailableConnections(domain, callback) { // callback([{usename, loca }); } function getInfoAboutUser(specificUsername, callback) { - url = METAVERSE_BASE + '/api/v1/users?filter=connections' + var url = METAVERSE_BASE + '/api/v1/users?filter=connections'; requestJSON(url, function (connectionsData) { for (user in connectionsData.users) { if (connectionsData.users[user].username === specificUsername) { @@ -447,21 +447,24 @@ function populateNearbyUserList(selectData, oldAudioData) { verticalAngleNormal = filter && Quat.getRight(orientation), horizontalAngleNormal = filter && Quat.getUp(orientation); avatarsOfInterest = {}; - avatars.forEach(function (id) { - var avatar = AvatarList.getAvatar(id); - var name = avatar.sessionDisplayName; + + var avatarData = AvatarList.getPalData().data; + + avatarData.forEach(function (currentAvatarData) { + var id = currentAvatarData.sessionUUID; + var name = currentAvatarData.sessionDisplayName; if (!name) { // Either we got a data packet but no identity yet, or something is really messed up. In any case, // we won't be able to do anything with this user, so don't include them. // In normal circumstances, a refresh will bring in the new user, but if we're very heavily loaded, // we could be losing and gaining people randomly. - print('No avatar identity data for', id); + print('No avatar identity data for', currentAvatarData.sessionUUID); return; } - if (id && myPosition && (Vec3.distance(avatar.position, myPosition) > filter.distance)) { + if (id && myPosition && (Vec3.distance(currentAvatarData.position, myPosition) > filter.distance)) { return; } - var normal = id && filter && Vec3.normalize(Vec3.subtract(avatar.position, myPosition)); + var normal = id && filter && Vec3.normalize(Vec3.subtract(currentAvatarData.position, myPosition)); var horizontal = normal && angleBetweenVectorsInPlane(normal, forward, horizontalAngleNormal); var vertical = normal && angleBetweenVectorsInPlane(normal, forward, verticalAngleNormal); if (id && filter && ((Math.abs(horizontal) > horizontalHalfAngle) || (Math.abs(vertical) > verticalHalfAngle))) { @@ -480,11 +483,11 @@ function populateNearbyUserList(selectData, oldAudioData) { personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null ignore: !!id && Users.getIgnoreStatus(id), // ditto isPresent: true, - isReplicated: avatar.isReplicated + isReplicated: currentAvatarData.isReplicated }; // Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin. Users.requestUsernameFromID(id); - if (id) { + if (id !== "") { addAvatarNode(id); // No overlay for ourselves avatarsOfInterest[id] = true; } else { @@ -515,30 +518,63 @@ function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { updateUser(data); } +function updateAudioLevel(avatarData) { + // the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged + // But of course it gets the data at a different rate, so we tweak the averaging ratio and frequency + // of updating (the latter for efficiency too). + var audioLevel = 0.0; + var avgAudioLevel = 0.0; + + var data = avatarData.sessionUUID === "" ? myData : ExtendedOverlay.get(avatarData.sessionUUID); + + if (data) { + // we will do exponential moving average by taking some the last loudness and averaging + data.accumulatedLevel = AVERAGING_RATIO * (data.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (avatarData.audioLoudness); + + // add 1 to insure we don't go log() and hit -infinity. Math.log is + // natural log, so to get log base 2, just divide by ln(2). + audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2); + + // decay avgAudioLevel + avgAudioLevel = Math.max((1 - AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); + + data.avgAudioLevel = avgAudioLevel; + data.audioLevel = audioLevel; + + // now scale for the gain. Also, asked to boost the low end, so one simple way is + // to take sqrt of the value. Lets try that, see how it feels. + avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel * (sessionGains[avatarData.sessionUUID] || 0.75))); + } + + var param = {}; + var level = [audioLevel, avgAudioLevel]; + var userId = avatarData.sessionUUID; + param[userId] = level; + sendToQml({ method: 'updateAudioLevel', params: param }); +} + var pingPong = true; function updateOverlays() { var eye = Camera.position; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - if (!id || !avatarsOfInterest[id]) { + + var avatarData = AvatarList.getPalData().data; + + avatarData.forEach(function (currentAvatarData) { + + if (currentAvatarData.sessionUUID === "" || !avatarsOfInterest[currentAvatarData.sessionUUID]) { return; // don't update ourself, or avatars we're not interested in } - var avatar = AvatarList.getAvatar(id); - if (!avatar) { - return; // will be deleted below if there had been an overlay. - } - var overlay = ExtendedOverlay.get(id); + updateAudioLevel(currentAvatarData); + var overlay = ExtendedOverlay.get(currentAvatarData.sessionUUID); if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. - print('Adding non-PAL avatar node', id); - overlay = addAvatarNode(id); + print('Adding non-PAL avatar node', currentAvatarData.sessionUUID); + overlay = addAvatarNode(currentAvatarData.sessionUUID); } - var target = avatar.position; + + var target = currentAvatarData.position; var distance = Vec3.distance(target, eye); - var offset = 0.2; + var offset = currentAvatarData.palOrbOffset; var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) - var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can - if (headIndex > 0) { - offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; - } // move a bit in front, towards the camera target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); @@ -548,7 +584,7 @@ function updateOverlays() { overlay.ping = pingPong; overlay.editOverlay({ - color: color(ExtendedOverlay.isSelected(id), overlay.hovering, overlay.audioLevel), + color: color(ExtendedOverlay.isSelected(currentAvatarData.sessionUUID), overlay.hovering, overlay.audioLevel), position: target, dimensions: 0.032 * distance }); @@ -669,68 +705,41 @@ triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Cont triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); +var ui; +// Most apps can have people toggle the tablet closed and open again, and the app should remain "open" even while +// the tablet is not shown. However, for the pal, we explicitly close the app and return the tablet to it's +// home screen (so that the avatar highlighting goes away). function tabletVisibilityChanged() { - if (!tablet.tabletShown) { - ContextOverlay.enabled = true; - tablet.gotoHomeScreen(); + if (!ui.tablet.tabletShown && ui.isOpen) { + ui.close(); } + } + +var UPDATE_INTERVAL_MS = 100; +var updateInterval; +function createUpdateInterval() { + return Script.setInterval(function () { + updateOverlays(); + }, UPDATE_INTERVAL_MS); } -var onPalScreen = false; -var PAL_QML_SOURCE = "hifi/Pal.qml"; -function onTabletButtonClicked() { - if (!tablet) { - print("Warning in onTabletButtonClicked(): 'tablet' undefined!"); - return; - } - if (onPalScreen) { - // In Toolbar Mode, `gotoHomeScreen` will close the app window. - tablet.gotoHomeScreen(); - } else { - tablet.loadQMLSource(PAL_QML_SOURCE); - } -} -var hasEventBridge = false; -function wireEventBridge(on) { - if (on) { - if (!hasEventBridge) { - tablet.fromQml.connect(fromQml); - hasEventBridge = true; - } - } else { - if (hasEventBridge) { - tablet.fromQml.disconnect(fromQml); - hasEventBridge = false; - } - } -} +var previousContextOverlay = ContextOverlay.enabled; +var previousRequestsDomainListData = Users.requestsDomainListData; +function on() { -function onTabletScreenChanged(type, url) { - onPalScreen = (type === "QML" && url === PAL_QML_SOURCE); - wireEventBridge(onPalScreen); - // for toolbar mode: change button to active when window is first openend, false otherwise. - button.editProperties({isActive: onPalScreen}); + previousContextOverlay = ContextOverlay.enabled; + previousRequestsDomainListData = Users.requestsDomainListData + ContextOverlay.enabled = false; + Users.requestsDomainListData = true; - if (onPalScreen) { - isWired = true; - - ContextOverlay.enabled = false; - Users.requestsDomainListData = true; - - audioTimer = createAudioInterval(AUDIO_LEVEL_UPDATE_INTERVAL_MS); - - tablet.tabletShownChanged.connect(tabletVisibilityChanged); - Script.update.connect(updateOverlays); - Controller.mousePressEvent.connect(handleMouseEvent); - Controller.mouseMoveEvent.connect(handleMouseMoveEvent); - Users.usernameFromIDReply.connect(usernameFromIDReply); - triggerMapping.enable(); - triggerPressMapping.enable(); - populateNearbyUserList(); - } else { - off(); - ContextOverlay.enabled = true; - } + ui.tablet.tabletShownChanged.connect(tabletVisibilityChanged); + updateInterval = createUpdateInterval(); + Controller.mousePressEvent.connect(handleMouseEvent); + Controller.mouseMoveEvent.connect(handleMouseMoveEvent); + Users.usernameFromIDReply.connect(usernameFromIDReply); + triggerMapping.enable(); + triggerPressMapping.enable(); + populateNearbyUserList(); } // @@ -744,8 +753,8 @@ function receiveMessage(channel, messageString, senderID) { var message = JSON.parse(messageString); switch (message.method) { case 'select': - if (!onPalScreen) { - tablet.loadQMLSource(PAL_QML_SOURCE); + if (!ui.isOpen) { + ui.open(); Script.setTimeout(function () { sendToQml(message); }, 1000); } else { sendToQml(message); // Accepts objects, not just strings. @@ -774,50 +783,6 @@ function scaleAudio(val) { return audioLevel; } -function getAudioLevel(id) { - // the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged - // But of course it gets the data at a different rate, so we tweak the averaging ratio and frequency - // of updating (the latter for efficiency too). - var avatar = AvatarList.getAvatar(id); - var audioLevel = 0.0; - var avgAudioLevel = 0.0; - var data = id ? ExtendedOverlay.get(id) : myData; - if (data) { - - // we will do exponential moving average by taking some the last loudness and averaging - data.accumulatedLevel = AVERAGING_RATIO * (data.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (avatar.audioLoudness); - - // add 1 to insure we don't go log() and hit -infinity. Math.log is - // natural log, so to get log base 2, just divide by ln(2). - audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2); - - // decay avgAudioLevel - avgAudioLevel = Math.max((1 - AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); - - data.avgAudioLevel = avgAudioLevel; - data.audioLevel = audioLevel; - - // now scale for the gain. Also, asked to boost the low end, so one simple way is - // to take sqrt of the value. Lets try that, see how it feels. - avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel * (sessionGains[id] || 0.75))); - } - return [audioLevel, avgAudioLevel]; -} - -function createAudioInterval(interval) { - // we will update the audioLevels periodically - // TODO: tune for efficiency - expecially with large numbers of avatars - return Script.setInterval(function () { - var param = {}; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - var level = getAudioLevel(id), - userId = id || 0; // qml didn't like an object with null/empty string for a key, so... - param[userId] = level; - }); - sendToQml({method: 'updateAudioLevel', params: param}); - }, interval); -} - function avatarDisconnected(nodeID) { // remove from the pal list sendToQml({method: 'avatarDisconnected', params: [nodeID]}); @@ -825,9 +790,8 @@ function avatarDisconnected(nodeID) { function clearLocalQMLDataAndClosePAL() { sendToQml({ method: 'clearLocalQMLData' }); - if (onPalScreen) { - ContextOverlay.enabled = true; - tablet.gotoHomeScreen(); + if (ui.isOpen) { + ui.close(); } } @@ -843,20 +807,15 @@ function avatarSessionChanged(avatarID) { sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] }); } - -var button; -var buttonName = "PEOPLE"; -var tablet = null; function startup() { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - button = tablet.addButton({ - text: buttonName, - icon: "icons/tablet-icons/people-i.svg", - activeIcon: "icons/tablet-icons/people-a.svg", - sortOrder: 7 + ui = new AppUi({ + buttonName: "PEOPLE", + sortOrder: 7, + home: "hifi/Pal.qml", + onOpened: on, + onClosed: off, + onMessage: fromQml }); - button.clicked.connect(onTabletButtonClicked); - tablet.screenChanged.connect(onTabletScreenChanged); Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); Messages.subscribe(CHANNEL); @@ -868,39 +827,25 @@ function startup() { } startup(); - -var isWired = false; -var audioTimer; -var AUDIO_LEVEL_UPDATE_INTERVAL_MS = 100; // 10hz for now (change this and change the AVERAGING_RATIO too) function off() { - if (isWired) { - Script.update.disconnect(updateOverlays); + if (ui.isOpen) { // i.e., only when connected + if (updateInterval) { + Script.clearInterval(updateInterval); + } Controller.mousePressEvent.disconnect(handleMouseEvent); Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); - tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); + ui.tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); Users.usernameFromIDReply.disconnect(usernameFromIDReply); - ContextOverlay.enabled = true triggerMapping.disable(); triggerPressMapping.disable(); - Users.requestsDomainListData = false; - - isWired = false; - - if (audioTimer) { - Script.clearInterval(audioTimer); - } } removeOverlays(); + ContextOverlay.enabled = previousContextOverlay; + Users.requestsDomainListData = previousRequestsDomainListData; } function shutdown() { - if (onPalScreen) { - tablet.gotoHomeScreen(); - } - button.clicked.disconnect(onTabletButtonClicked); - tablet.removeButton(button); - tablet.screenChanged.disconnect(onTabletScreenChanged); Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL); Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL); Messages.subscribe(CHANNEL); diff --git a/scripts/system/particle_explorer/hifi-entity-ui.js b/scripts/system/particle_explorer/hifi-entity-ui.js index 36505a0b7f..62a0aadc86 100644 --- a/scripts/system/particle_explorer/hifi-entity-ui.js +++ b/scripts/system/particle_explorer/hifi-entity-ui.js @@ -61,12 +61,18 @@ function HifiEntityUI(parent) { this.parent = parent; var self = this; + this.sendPackage = {}; this.settingsUpdateLock = false; - this.webBridgeSync = _.debounce(function (id, val) { - if (self.EventBridge && !self.settingsUpdateLock) { - var sendPackage = {}; - sendPackage[id] = val; - self.submitChanges(sendPackage); + this.webBridgeSync = function(id, val) { + if (!this.settingsUpdateLock) { + this.sendPackage[id] = val; + this.webBridgeSyncDebounce(); + } + }; + this.webBridgeSyncDebounce = _.debounce(function () { + if (self.EventBridge) { + self.submitChanges(self.sendPackage); + self.sendPackage = {}; } }, DEBOUNCE_TIMEOUT); } @@ -159,7 +165,6 @@ HifiEntityUI.prototype = { var self = this; var fields = document.getElementsByTagName("input"); - self.settingsUpdateLock = true; if (!currentProperties.locked) { for (var i = 0; i < fields.length; i++) { fields[i].removeAttribute("disabled"); @@ -179,7 +184,7 @@ HifiEntityUI.prototype = { for (var e in keys) { if (keys.hasOwnProperty(e)) { var value = keys[e]; - + var property = currentProperties[value]; var field = self.builtRows[value]; if (field) { @@ -235,10 +240,6 @@ HifiEntityUI.prototype = { } } } - // Now unlocking settings Update lock for sending messages on callbacks. - setTimeout(function () { - self.settingsUpdateLock = false; - }, DEBOUNCE_TIMEOUT * 2.5); }, connect: function (EventBridge) { this.EventBridge = EventBridge; @@ -253,28 +254,9 @@ HifiEntityUI.prototype = { data = JSON.parse(data); if (data.messageType === 'particle_settings') { - // Update settings - var currentProperties = data.currentProperties; - // Update uninitialized variables - if (!currentProperties.alphaStart) { - currentProperties.alphaStart = currentProperties.alpha; - } - if (!currentProperties.alphaFinish) { - currentProperties.alphaFinish = currentProperties.alpha; - } - if (!currentProperties.radiusStart) { - currentProperties.radiusStart = currentProperties.particleRadius; - } - if (!currentProperties.radiusFinish) { - currentProperties.radiusFinish = currentProperties.particleRadius; - } - if (!currentProperties.colorStart || !currentProperties.colorStart.red) { - currentProperties.colorStart = currentProperties.color; - } - if (!currentProperties.colorFinish || !currentProperties.colorFinish.red) { - currentProperties.colorFinish = currentProperties.color; - } - self.fillFields(currentProperties); + self.settingsUpdateLock = true; + self.fillFields(data.currentProperties); + self.settingsUpdateLock = false; // Do expected property match with structure; } else if (data.messageType === 'particle_close') { self.disableFields(); diff --git a/scripts/system/particle_explorer/particleExplorerTool.js b/scripts/system/particle_explorer/particleExplorerTool.js index a1f06fda35..1914180ff9 100644 --- a/scripts/system/particle_explorer/particleExplorerTool.js +++ b/scripts/system/particle_explorer/particleExplorerTool.js @@ -9,75 +9,121 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global window, alert, ParticleExplorerTool, EventBridge, dat, listenForSettingsUpdates,createVec3Folder,createQuatFolder,writeVec3ToInterface,writeDataToInterface*/ +/* global ParticleExplorerTool */ var PARTICLE_EXPLORER_HTML_URL = Script.resolvePath('particleExplorer.html'); -ParticleExplorerTool = function() { +ParticleExplorerTool = function(createToolsWindow) { var that = {}; that.activeParticleEntity = 0; - that.activeParticleProperties = {}; + that.updatedActiveParticleProperties = {}; that.createWebView = function() { that.webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); that.webView.setVisible = function(value) {}; that.webView.webEventReceived.connect(that.webEventReceived); + createToolsWindow.webEventReceived.addListener(this, that.webEventReceived); }; + function emitScriptEvent(data) { + var messageData = JSON.stringify(data); + that.webView.emitScriptEvent(messageData); + createToolsWindow.emitScriptEvent(messageData); + } + that.destroyWebView = function() { if (!that.webView) { return; } that.activeParticleEntity = 0; - that.activeParticleProperties = {}; + that.updatedActiveParticleProperties = {}; - var messageData = { + emitScriptEvent({ messageType: "particle_close" - }; - that.webView.emitScriptEvent(JSON.stringify(messageData)); + }); }; - function sendActiveParticleProperties() { - that.webView.emitScriptEvent(JSON.stringify({ + function sendParticleProperties(properties) { + emitScriptEvent({ messageType: "particle_settings", - currentProperties: that.activeParticleProperties - })); + currentProperties: properties + }); + } + + function sendActiveParticleProperties() { + var properties = Entities.getEntityProperties(that.activeParticleEntity); + if (properties.emitOrientation) { + properties.emitOrientation = Quat.safeEulerAngles(properties.emitOrientation); + } + // Update uninitialized variables + if (isNaN(properties.alphaStart)) { + properties.alphaStart = properties.alpha; + } + if (isNaN(properties.alphaFinish)) { + properties.alphaFinish = properties.alpha; + } + if (isNaN(properties.radiusStart)) { + properties.radiusStart = properties.particleRadius; + } + if (isNaN(properties.radiusFinish)) { + properties.radiusFinish = properties.particleRadius; + } + if (isNaN(properties.colorStart.red)) { + properties.colorStart = properties.color; + } + if (isNaN(properties.colorFinish.red)) { + properties.colorFinish = properties.color; + } + sendParticleProperties(properties); + } + + function sendUpdatedActiveParticleProperties() { + sendParticleProperties(that.updatedActiveParticleProperties); + that.updatedActiveParticleProperties = {}; } that.webEventReceived = function(message) { var data = JSON.parse(message); if (data.messageType === "settings_update") { - if (data.updatedSettings.emitOrientation) { - data.updatedSettings.emitOrientation = Quat.fromVec3Degrees(data.updatedSettings.emitOrientation); - } - Entities.editEntity(that.activeParticleEntity, data.updatedSettings); - - for (var key in data.updatedSettings) { - if (that.activeParticleProperties.hasOwnProperty(key)) { - that.activeParticleProperties[key] = data.updatedSettings[key]; - } - } + var updatedSettings = data.updatedSettings; var optionalProps = ["alphaStart", "alphaFinish", "radiusStart", "radiusFinish", "colorStart", "colorFinish"]; var fallbackProps = ["alpha", "particleRadius", "color"]; - var entityProps = Entities.getEntityProperties(that.activeParticleProperties, optionalProps); + for (var i = 0; i < optionalProps.length; i++) { + var fallbackProp = fallbackProps[Math.floor(i / 2)]; + var optionalValue = updatedSettings[optionalProps[i]]; + var fallbackValue = updatedSettings[fallbackProp]; + if (optionalValue && fallbackValue) { + delete updatedSettings[optionalProps[i]]; + } + } + + if (updatedSettings.emitOrientation) { + updatedSettings.emitOrientation = Quat.fromVec3Degrees(updatedSettings.emitOrientation); + } + + Entities.editEntity(that.activeParticleEntity, updatedSettings); + + var entityProps = Entities.getEntityProperties(that.activeParticleEntity, optionalProps); + var needsUpdate = false; for (var i = 0; i < optionalProps.length; i++) { - var fallback = fallbackProps[Math.floor(i / 2)]; - if (data.updatedSettings[fallback]) { - var prop = optionalProps[i]; - if (!that.activeParticleProperties[prop] || (fallback === "color" && !that.activeParticleProperties[prop].red)) { - that.activeParticleProperties[prop] = entityProps[fallback]; + var fallbackProp = fallbackProps[Math.floor(i / 2)]; + var fallbackValue = updatedSettings[fallbackProp]; + if (fallbackValue) { + var optionalProp = optionalProps[i]; + if ((fallbackProp !== "color" && isNaN(entityProps[optionalProp])) || (fallbackProp === "color" && isNaN(entityProps[optionalProp].red))) { + that.updatedActiveParticleProperties[optionalProp] = fallbackValue; needsUpdate = true; } } } if (needsUpdate) { - sendActiveParticleProperties(); + sendUpdatedActiveParticleProperties(); } - + } else if (data.messageType === "page_loaded") { sendActiveParticleProperties(); } @@ -85,12 +131,8 @@ ParticleExplorerTool = function() { that.setActiveParticleEntity = function(id) { that.activeParticleEntity = id; - }; - - that.setActiveParticleProperties = function(properties) { - that.activeParticleProperties = properties; sendActiveParticleProperties(); }; - + return that; }; diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 9b540aefc8..3ddbeb997d 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -17,7 +17,6 @@ var SNAPSHOT_DELAY = 500; // 500ms var FINISH_SOUND_DELAY = 350; var resetOverlays; var reticleVisible; -var clearOverlayWhenMoving; var buttonName = "SNAP"; var buttonConnected = false; @@ -286,6 +285,7 @@ function printToPolaroid(image_url) { var polaroid_url = image_url; var model_pos = Vec3.sum(MyAvatar.position, Vec3.multiply(1.25, Quat.getForward(MyAvatar.orientation))); + model_pos.y += 0.39; // Print a bit closer to the head var model_q1 = MyAvatar.orientation; var model_q2 = Quat.angleAxis(90, Quat.getRight(model_q1)); @@ -295,11 +295,11 @@ function printToPolaroid(image_url) { "type": 'Model', "shapeType": 'box', - "name": "New Snapshot", - "description": "Printed from Snaps", + "name": "Snapshot by " + MyAvatar.sessionDisplayName, + "description": "Printed from SNAP app", "modelURL": POLAROID_MODEL_URL, - "dimensions": { "x": 0.5667, "y": 0.0212, "z": 0.4176 }, + "dimensions": { "x": 0.5667, "y": 0.042, "z": 0.4176 }, "position": model_pos, "rotation": model_rot, @@ -307,10 +307,8 @@ function printToPolaroid(image_url) { "density": 200, "restitution": 0.15, - "gravity": { "x": 0, "y": -4.5, "z": 0 }, - - "velocity": { "x": 0, "y": 3.5, "z": 0 }, - "angularVelocity": { "x": -1.0, "y": 0, "z": -1.3 }, + "gravity": { "x": 0, "y": -2.0, "z": 0 }, + "damping": 0.45, "dynamic": true, "collisionsWillMove": true, @@ -438,11 +436,6 @@ function takeSnapshot() { isUploadingPrintableStill = true; updatePrintPermissions(); - // Raising the desktop for the share dialog at end will interact badly with clearOverlayWhenMoving. - // Turn it off now, before we start futzing with things (and possibly moving). - clearOverlayWhenMoving = MyAvatar.getClearOverlayWhenMoving(); // Do not use Settings. MyAvatar keeps a separate copy. - MyAvatar.setClearOverlayWhenMoving(false); - // We will record snapshots based on the starting location. That could change, e.g., when recording a .gif. // Even the domainID could change (e.g., if the user falls into a teleporter while recording). href = location.href; @@ -544,9 +537,6 @@ function stillSnapshotTaken(pathStillSnapshot, notify) { // last element in data array tells dialog whether we can share or not Settings.setValue("previousStillSnapPath", pathStillSnapshot); - if (clearOverlayWhenMoving) { - MyAvatar.setClearOverlayWhenMoving(true); // not until after the share dialog - } HMD.openTablet(); isDomainOpen(domainID, function (canShare) { @@ -590,9 +580,6 @@ function processingGifStarted(pathStillSnapshot) { } Settings.setValue("previousStillSnapPath", pathStillSnapshot); - if (clearOverlayWhenMoving) { - MyAvatar.setClearOverlayWhenMoving(true); // not until after the share dialog - } HMD.openTablet(); isDomainOpen(domainID, function (canShare) { diff --git a/tools/dissectors/1-hfudt.lua b/tools/dissectors/1-hfudt.lua index fa491723fb..137bee659b 100644 --- a/tools/dissectors/1-hfudt.lua +++ b/tools/dissectors/1-hfudt.lua @@ -87,7 +87,7 @@ local packet_types = { [23] = "ICEServerQuery", [24] = "OctreeStats", [25] = "Jurisdiction", - [26] = "JurisdictionRequest", + [26] = "AvatarIdentityRequest", [27] = "AssignmentClientStatus", [28] = "NoisyMute", [29] = "AvatarIdentity", diff --git a/tools/jsdoc/README.md b/tools/jsdoc/README.md index 5cdb1ea44e..f9864a21e4 100644 --- a/tools/jsdoc/README.md +++ b/tools/jsdoc/README.md @@ -11,7 +11,7 @@ If you would like the extra functionality for gravPrep: To generate html documentation for the High Fidelity JavaScript API: * `cd tools/jsdoc` -* `jsdoc . -c config.json` +* `jsdoc root.js -c config.json` The out folder should contain index.html. diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index 0db70f6fe4..ff672d13bf 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -31,8 +31,8 @@ BakerCLI::BakerCLI(OvenCLIApplication* parent) : QObject(parent) { void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& type) { // if the URL doesn't have a scheme, assume it is a local file - if (inputUrl.scheme() != "http" && inputUrl.scheme() != "https" && inputUrl.scheme() != "ftp") { - inputUrl.setScheme("file"); + if (inputUrl.scheme() != "http" && inputUrl.scheme() != "https" && inputUrl.scheme() != "ftp" && inputUrl.scheme() != "file") { + inputUrl = QUrl::fromLocalFile(inputUrl.toString()); } qDebug() << "Baking file type: " << type; diff --git a/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml b/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml index e0c836fb1c..033039b87d 100644 --- a/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml +++ b/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml @@ -60,7 +60,7 @@ Rectangle { // "Spectator" text HifiStylesUit.RalewaySemiBold { id: titleBarText; - text: "Spectator Camera 2.2"; + text: "Spectator Camera 2.3"; // Anchors anchors.left: parent.left; anchors.leftMargin: 30; diff --git a/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js index 3e749e38a2..4c39c5fb95 100644 --- a/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js +++ b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js @@ -74,6 +74,7 @@ "collisionMask": 7, "dynamic": false, "modelURL": Script.resolvePath("spectator-camera.fbx"), + "name": "Spectator Camera", "registrationPoint": { "x": 0.56, "y": 0.545, @@ -102,6 +103,18 @@ position: cameraPosition, localOnly: true }); + + // Remove the existing camera model from the domain if one exists. + // It's easy for this to happen if the user crashes while the Spectator Camera is on. + // We do this down here (after the new one is rezzed) so that we don't accidentally delete + // the newly-rezzed model. + var entityIDs = Entities.findEntitiesByName("Spectator Camera", MyAvatar.position, 100, false); + entityIDs.forEach(function (currentEntityID) { + var currentEntityOwner = Entities.getEntityProperties(currentEntityID, ['owningAvatarID']).owningAvatarID; + if (currentEntityOwner === MyAvatar.sessionUUID && currentEntityID !== camera) { + Entities.deleteEntity(currentEntityID); + } + }); } // Function Name: spectatorCameraOff()