diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index cd1bd9e2c2..4490474599 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -222,7 +222,8 @@ void Agent::requestScript() { return; } - auto request = DependencyManager::get()->createResourceRequest(this, scriptURL); + auto request = DependencyManager::get()->createResourceRequest( + this, scriptURL, true, -1, "Agent::requestScript"); if (!request) { qWarning() << "Could not create ResourceRequest for Agent script at" << scriptURL.toString(); diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 2b5ff51b49..7d47c8e713 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -21,7 +21,6 @@ #include #include -#include #include #include #include diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp index 426f3ce6fc..76ff5ab2ed 100644 --- a/assignment-client/src/AssignmentClient.cpp +++ b/assignment-client/src/AssignmentClient.cpp @@ -35,6 +35,7 @@ #include "AssignmentClientLogging.h" #include "AssignmentFactory.h" +#include "ResourceRequestObserver.h" const QString ASSIGNMENT_CLIENT_TARGET_NAME = "assignment-client"; const long long ASSIGNMENT_REQUEST_INTERVAL_MSECS = 1 * 1000; @@ -49,6 +50,7 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); auto addressManager = DependencyManager::set(); diff --git a/cmake/externals/serverless-content/CMakeLists.txt b/cmake/externals/serverless-content/CMakeLists.txt index 12e2b7a4c6..8bb49f0973 100644 --- a/cmake/externals/serverless-content/CMakeLists.txt +++ b/cmake/externals/serverless-content/CMakeLists.txt @@ -4,8 +4,8 @@ set(EXTERNAL_NAME serverless-content) ExternalProject_Add( ${EXTERNAL_NAME} - URL http://cdn.highfidelity.com/content-sets/serverless-tutorial-RC73.zip - URL_MD5 0c5edfb63cafb042311d3cf25261fbf2 + URL http://cdn.highfidelity.com/content-sets/serverless-tutorial-RC75.zip + URL_MD5 b4225d058952e17976ac228330ce8d51 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/interface/resources/avatar/network-animation.json b/interface/resources/avatar/network-animation.json new file mode 100644 index 0000000000..0ba4ea465c --- /dev/null +++ b/interface/resources/avatar/network-animation.json @@ -0,0 +1,140 @@ +{ + "version": "1.1", + "root": { + "id": "userAnimStateMachine", + "type": "stateMachine", + "data": { + "currentState": "idleAnim", + "states": [ + { + "id": "idleAnim", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "postTransitAnim", "state": "postTransitAnim" }, + { "var": "preTransitAnim", "state": "preTransitAnim" } + ] + }, + { + "id": "preTransitAnim", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "idleAnim", "state": "idleAnim" }, + { "var": "transitAnim", "state": "transitAnim" } + ] + }, + { + "id": "transitAnim", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "preTransitAnim", "state": "preTransitAnim" }, + { "var": "postTransitAnim", "state": "postTransitAnim" } + ] + }, + { + "id": "postTransitAnim", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "transitAnim", "state": "transitAnim" }, + { "var": "idleAnim", "state": "idleAnim" } + ] + }, + { + "id": "userAnimA", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "idleAnim", "state": "idleAnim" }, + { "var": "userAnimB", "state": "userAnimB" } + ] + }, + { + "id": "userAnimB", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "idleAnim", "state": "idleAnim" }, + { "var": "userAnimA", "state": "userAnimA" } + ] + } + ] + }, + "children": [ + { + "id": "idleAnim", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "preTransitAnim", + "type": "clip", + "data": { + "url": "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx", + "startFrame": 0.0, + "endFrame": 10.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "transitAnim", + "type": "clip", + "data": { + "url": "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx", + "startFrame": 11.0, + "endFrame": 11.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "postTransitAnim", + "type": "clip", + "data": { + "url": "https://hifi-content.s3.amazonaws.com/luis/test_scripts/transitApp/animations/teleport01_warp.fbx", + "startFrame": 22.0, + "endFrame": 49.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "userAnimA", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "userAnimB", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + } +} diff --git a/interface/resources/qml/+android/Stats.qml b/interface/resources/qml/+android/Stats.qml index 0dcb07e730..e9a2aa47eb 100644 --- a/interface/resources/qml/+android/Stats.qml +++ b/interface/resources/qml/+android/Stats.qml @@ -10,6 +10,7 @@ Item { property int modality: Qt.NonModal implicitHeight: row.height implicitWidth: row.width + visible: false Component.onCompleted: { stats.parentChanged.connect(fill); diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index 71bfbb084d..3446191163 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -31,7 +31,7 @@ Rectangle { scaleSlider.notify = false; scaleSlider.value = Math.round(avatarScale * 10); - scaleSlider.notify = true;; + scaleSlider.notify = true; if (settings.dominantHand === 'left') { leftHandRadioButton.checked = true; diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml new file mode 100644 index 0000000000..27277a28f4 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/ItemUnderTest.qml @@ -0,0 +1,364 @@ +// +// ItemUnderTest +// qml/hifi/commerce/marketplaceItemTester +// +// Load items not in the marketplace for testing purposes +// +// Created by Kerry Ivan Kurian on 2018-10-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 +// + +import QtQuick 2.10 +import QtQuick.Controls 2.3 +import Hifi 1.0 as Hifi +import "qrc:////qml//styles-uit" as HifiStylesUit +import "qrc:////qml//controls-uit" as HifiControlsUit + +Rectangle { + id: root; + color: hifi.colors.baseGray + width: parent.width - 16 + height: childrenRect.height + itemHeaderContainer.anchors.topMargin + detailsContainer.anchors.topMargin + + property var detailsExpanded: false + + property var actions: { + "forward": function(resource, assetType, resourceObjectId){ + switch(assetType) { + case "application": + Commerce.installApp(resource, true); + break; + case "avatar": + MyAvatar.useFullAvatarURL(resource); + break; + case "content set": + urlHandler.handleUrl("hifi://localhost/0,0,0"); + Commerce.replaceContentSet(toUrl(resource), ""); + break; + case "entity": + case "wearable": + rezEntity(resource, assetType, resourceObjectId); + break; + default: + print("Marketplace item tester unsupported assetType " + assetType); + } + }, + "trash": function(resource, assetType){ + if ("application" === assetType) { + Commerce.uninstallApp(resource); + } + sendToScript({ + method: "tester_deleteResourceObject", + objectId: resourceListModel.get(index).resourceObjectId}); + resourceListModel.remove(index); + } + } + + Item { + id: itemHeaderContainer + anchors.top: parent.top + anchors.topMargin: 8 + anchors.left: parent.left + anchors.leftMargin: 8 + width: parent.width - 16 + height: childrenRect.height + + Item { + id: itemNameContainer + width: parent.width * 0.5 + height: childrenRect.height + + HifiStylesUit.RalewaySemiBold { + id: resourceName + height: paintedHeight + width: parent.width + text: { + var match = resource.match(/\/([^/]*)$/); + return match ? match[1] : resource; + } + size: 14 + color: hifi.colors.white + wrapMode: Text.WrapAnywhere + } + + HifiStylesUit.RalewayRegular { + id: resourceUrl + anchors.top: resourceName.bottom; + anchors.topMargin: 4; + height: paintedHeight + width: parent.width + text: resource + size: 12 + color: hifi.colors.faintGray; + wrapMode: Text.WrapAnywhere + } + } + + HifiControlsUit.ComboBox { + id: comboBox + anchors.left: itemNameContainer.right + anchors.leftMargin: 4 + anchors.verticalCenter: itemNameContainer.verticalCenter + height: 30 + width: parent.width * 0.3 - anchors.leftMargin + + model: [ + "application", + "avatar", + "content set", + "entity", + "wearable", + "unknown" + ] + + currentIndex: (("entity or wearable" === assetType) ? + model.indexOf("unknown") : model.indexOf(assetType)) + + Component.onCompleted: { + onCurrentIndexChanged.connect(function() { + assetType = model[currentIndex]; + sendToScript({ + method: "tester_updateResourceObjectAssetType", + objectId: resourceListModel.get(index)["resourceObjectId"], + assetType: assetType }); + }); + } + } + + Button { + id: actionButton + property var glyphs: { + "application": hifi.glyphs.install, + "avatar": hifi.glyphs.avatar, + "content set": hifi.glyphs.globe, + "entity": hifi.glyphs.wand, + "trash": hifi.glyphs.trash, + "unknown": hifi.glyphs.circleSlash, + "wearable": hifi.glyphs.hat + } + property int color: hifi.buttons.blue; + property int colorScheme: hifi.colorSchemes.dark; + anchors.left: comboBox.right + anchors.leftMargin: 4 + anchors.verticalCenter: itemNameContainer.verticalCenter + width: parent.width * 0.10 - anchors.leftMargin + height: width + enabled: comboBox.model[comboBox.currentIndex] !== "unknown" + + onClicked: { + if (model.currentlyRecordingResources) { + model.currentlyRecordingResources = false; + } else { + model.resourceAccessEventText = ""; + model.currentlyRecordingResources = true; + root.actions["forward"](resource, comboBox.currentText, resourceObjectId); + } + sendToScript({ + method: "tester_updateResourceRecordingStatus", + objectId: resourceListModel.get(index).resourceObjectId, + status: model.currentlyRecordingResources + }); + } + + background: Rectangle { + radius: 4; + gradient: Gradient { + GradientStop { + position: 0.2 + color: { + if (!actionButton.enabled) { + hifi.buttons.disabledColorStart[actionButton.colorScheme] + } else if (actionButton.pressed) { + hifi.buttons.pressedColor[actionButton.color] + } else if (actionButton.hovered) { + hifi.buttons.hoveredColor[actionButton.color] + } else { + hifi.buttons.colorStart[actionButton.color] + } + } + } + GradientStop { + position: 1.0 + color: { + if (!actionButton.enabled) { + hifi.buttons.disabledColorFinish[actionButton.colorScheme] + } else if (actionButton.pressed) { + hifi.buttons.pressedColor[actionButton.color] + } else if (actionButton.hovered) { + hifi.buttons.hoveredColor[actionButton.color] + } else { + hifi.buttons.colorFinish[actionButton.color] + } + } + } + } + } + + contentItem: Item { + HifiStylesUit.HiFiGlyphs { + id: rezIcon; + text: model.currentlyRecordingResources ? hifi.glyphs.scriptStop : actionButton.glyphs[comboBox.model[comboBox.currentIndex]]; + anchors.fill: parent + size: 30; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + color: enabled ? hifi.buttons.textColor[actionButton.color] + : hifi.buttons.disabledTextColor[actionButton.colorScheme] + } + } + } + + Button { + id: trashButton + property int color: hifi.buttons.red; + property int colorScheme: hifi.colorSchemes.dark; + anchors.left: actionButton.right + anchors.verticalCenter: itemNameContainer.verticalCenter + anchors.leftMargin: 4 + width: parent.width * 0.10 - anchors.leftMargin + height: width + + onClicked: { + root.actions["trash"](resource, comboBox.currentText, resourceObjectId); + } + + background: Rectangle { + radius: 4; + gradient: Gradient { + GradientStop { + position: 0.2 + color: { + if (!trashButton.enabled) { + hifi.buttons.disabledColorStart[trashButton.colorScheme] + } else if (trashButton.pressed) { + hifi.buttons.pressedColor[trashButton.color] + } else if (trashButton.hovered) { + hifi.buttons.hoveredColor[trashButton.color] + } else { + hifi.buttons.colorStart[trashButton.color] + } + } + } + GradientStop { + position: 1.0 + color: { + if (!trashButton.enabled) { + hifi.buttons.disabledColorFinish[trashButton.colorScheme] + } else if (trashButton.pressed) { + hifi.buttons.pressedColor[trashButton.color] + } else if (trashButton.hovered) { + hifi.buttons.hoveredColor[trashButton.color] + } else { + hifi.buttons.colorFinish[trashButton.color] + } + } + } + } + } + + contentItem: Item { + HifiStylesUit.HiFiGlyphs { + id: trashIcon; + text: hifi.glyphs.trash + anchors.fill: parent + size: 22; + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: enabled ? hifi.buttons.textColor[trashButton.color] + : hifi.buttons.disabledTextColor[trashButton.colorScheme] + } + } + } + } + + Item { + id: detailsContainer + + width: parent.width - 16 + height: root.detailsExpanded ? 300 : 26 + anchors.top: itemHeaderContainer.bottom + anchors.topMargin: 12 + anchors.left: parent.left + anchors.leftMargin: 8 + + HifiStylesUit.HiFiGlyphs { + id: detailsToggle + anchors.left: parent.left + anchors.leftMargin: -4 + anchors.top: parent.top + anchors.topMargin: -2 + width: 22 + text: root.detailsExpanded ? hifi.glyphs.minimize : hifi.glyphs.maximize + color: hifi.colors.white + size: 22 + MouseArea { + anchors.fill: parent + onClicked: root.detailsExpanded = !root.detailsExpanded + } + } + + ScrollView { + id: detailsTextContainer + anchors.top: parent.top + anchors.left: detailsToggle.right + anchors.leftMargin: 4 + anchors.right: parent.right + height: detailsContainer.height - (root.detailsExpanded ? (copyToClipboardButton.height + copyToClipboardButton.anchors.topMargin) : 0) + clip: true + + TextArea { + id: detailsText + readOnly: true + color: hifi.colors.white + text: { + var numUniqueResources = (model.resourceAccessEventText.split("\n").length - 1); + if (root.detailsExpanded && numUniqueResources > 0) { + return model.resourceAccessEventText + } else { + return numUniqueResources.toString() + " unique source/resource url pair" + (numUniqueResources === 1 ? "" : "s") + " recorded" + } + } + font: Qt.font({ family: "Courier", pointSize: 8, weight: Font.Normal }) + wrapMode: TextEdit.NoWrap + + background: Rectangle { + anchors.fill: parent; + color: hifi.colors.baseGrayShadow; + border.width: 0; + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (root.detailsExpanded) { + detailsText.selectAll(); + } else { + root.detailsExpanded = true; + } + } + } + } + + HifiControlsUit.Button { + id: copyToClipboardButton; + visible: root.detailsExpanded + color: hifi.buttons.noneBorderlessWhite + colorScheme: hifi.colorSchemes.dark + + anchors.top: detailsTextContainer.bottom + anchors.topMargin: 8 + anchors.right: parent.right + width: 160 + height: 30 + text: "Copy to Clipboard" + + onClicked: { + Window.copyToClipboard(detailsText.text); + } + } + } +} diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml index c3d87ca2f5..2a4f2d0e22 100644 --- a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml @@ -4,21 +4,19 @@ // // Load items not in the marketplace for testing purposes // -// Created by Zach Fox on 2018-09-05 +// Created by Kerry Ivan Kurian on 2018-09-05 // 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.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import QtQuick.Dialogs 1.0 +import QtQuick 2.10 import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 import Hifi 1.0 as Hifi -import "../../../styles-uit" as HifiStylesUit -import "../../../controls-uit" as HifiControlsUit +import "qrc:////qml//styles-uit" as HifiStylesUit +import "qrc:////qml//controls-uit" as HifiControlsUit @@ -27,33 +25,223 @@ Rectangle { property string installedApps property var nextResourceObjectId: 0 - signal sendToScript(var message) HifiStylesUit.HifiConstants { id: hifi } ListModel { id: resourceListModel } - color: hifi.colors.white + color: hifi.colors.darkGray - AnimatedImage { - id: spinner; - source: "spinner.gif" - width: 74; - height: width; - anchors.verticalCenter: parent.verticalCenter; - anchors.horizontalCenter: parent.horizontalCenter; + // + // TITLE BAR START + // + Item { + id: titleBarContainer + // Size + width: root.width + height: 50 + // Anchors + anchors.left: parent.left + anchors.top: parent.top + + // Title bar text + HifiStylesUit.RalewaySemiBold { + id: titleBarText + text: "Marketplace Item Tester" + // Text size + size: 24 + // Anchors + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: 16 + width: paintedWidth + // Style + color: hifi.colors.lightGrayText + // Alignment + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignVCenter + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: 1 + } + } + // + // TITLE BAR END + // + + Rectangle { + id: spinner + z: 999 + anchors.top: titleBarContainer.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: buttonContainer.top + color: hifi.colors.darkGray + + AnimatedImage { + source: "spinner.gif" + width: 74 + height: width + anchors.centerIn: parent + } + } + + Rectangle { + id: instructionsContainer + z: 998 + color: hifi.colors.darkGray + visible: resourceListModel.count === 0 && !spinner.visible + anchors.top: titleBarContainer.bottom + anchors.topMargin: 20 + anchors.left: parent.left + anchors.leftMargin: 20 + anchors.right: parent.right + anchors.rightMargin: 20 + anchors.bottom: buttonContainer.top + anchors.bottomMargin: 20 + + HifiStylesUit.RalewayRegular { + text: "Use Marketplace Item Tester to test out your items before submitting them to the Marketplace." + + "\n\nUse one of the buttons below to load your item." + // Text size + size: 20 + // Anchors + anchors.fill: parent + // Style + color: hifi.colors.lightGrayText + wrapMode: Text.Wrap + // Alignment + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + + ListView { + id: itemList + visible: !instructionsContainer.visible + anchors.top: titleBarContainer.bottom + anchors.topMargin: 20 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: buttonContainer.top + anchors.bottomMargin: 20 + ScrollBar.vertical: ScrollBar { + visible: !instructionsContainer.visible + policy: ScrollBar.AlwaysOn + parent: itemList.parent + anchors.top: itemList.top + anchors.right: itemList.right + anchors.bottom: itemList.bottom + width: 16 + } + clip: true + model: resourceListModel + spacing: 8 + + delegate: ItemUnderTest { } + } + + Item { + id: buttonContainer + + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.bottom: parent.bottom + anchors.bottomMargin: 12 + height: 40 + + property string currentAction + property var actions: { + "Load File": function() { + buttonContainer.currentAction = "load file"; + Window.browseChanged.connect(onResourceSelected); + Window.browseAsync("Please select a file (*.app.json *.json *.fst *.json.gz)", "", "Assets (*.app.json *.json *.fst *.json.gz)"); + }, + "Load URL": function() { + buttonContainer.currentAction = "load url"; + Window.promptTextChanged.connect(onResourceSelected); + Window.promptAsync("Please enter a URL", ""); + } + } + + function onResourceSelected(resource) { + // It is possible that we received the present signal + // from something other than our browserAsync window. + // Alas, there is nothing we can do about that so charge + // ahead as though we are sure the present signal is one + // we expect. + print("!!!! resource selected"); + switch(currentAction) { + case "load file": + Window.browseChanged.disconnect(onResourceSelected); + break + case "load url": + Window.promptTextChanged.disconnect(onResourceSelected); + break; + } + if (resource) { + print("!!!! building resource object"); + var resourceObj = buildResourceObj(resource); + print("!!!! notifying script of resource object"); + sendToScript({ + method: 'tester_newResourceObject', + resourceObject: resourceObj + }); + } + } + + HifiControlsUit.Button { + enabled: !spinner.visible + anchors.right: parent.horizontalCenter + anchors.rightMargin: width/4 + anchors.verticalCenter: parent.verticalCenter + color: hifi.buttons.blue + fontSize: 20 + text: "Load File" + width: parent.width / 3 + height: parent.height + onClicked: buttonContainer.actions[text]() + } + + HifiControlsUit.Button { + enabled: !spinner.visible + anchors.left: parent.horizontalCenter + anchors.leftMargin: width/4 + anchors.verticalCenter: parent.verticalCenter + color: hifi.buttons.blue + fontSize: 20 + text: "Load URL" + width: parent.width / 3 + height: parent.height + onClicked: buttonContainer.actions[text]() + } } function fromScript(message) { switch (message.method) { case "newResourceObjectInTest": var resourceObject = message.resourceObject; + resourceListModel.clear(); // REMOVE THIS once we support specific referrers resourceListModel.append(resourceObject); spinner.visible = false; break; case "nextObjectIdInTest": + print("!!!! message from script! " + JSON.stringify(message)); nextResourceObjectId = message.id; spinner.visible = false; break; + case "resourceRequestEvent": + // When we support multiple items under test simultaneously, + // we'll have to replace "0" with the correct index. + resourceListModel.setProperty(0, "resourceAccessEventText", message.resourceAccessEventText); + break; } } @@ -64,227 +252,26 @@ Rectangle { resource.match(/\.json\.gz$/) ? "content set" : resource.match(/\.json$/) ? "entity or wearable" : "unknown"); - return { "id": nextResourceObjectId++, + // Uncomment this once we support more than one item in test at the same time + //nextResourceObjectId++; + return { "resourceObjectId": nextResourceObjectId, "resource": resource, "assetType": assetType }; } - function installResourceObj(resourceObj) { - if ("application" === resourceObj.assetType) { - Commerce.installApp(resourceObj.resource); - } - } - - function addAllInstalledAppsToList() { - var i, apps = Commerce.getInstalledApps().split(","), len = apps.length; - for(i = 0; i < len - 1; ++i) { - if (i in apps) { - resourceListModel.append(buildResourceObj(apps[i])); - } - } - } - function toUrl(resource) { var httpPattern = /^http/i; return httpPattern.test(resource) ? resource : "file:///" + resource; } - function rezEntity(resource, entityType) { + function rezEntity(resource, entityType, resourceObjectId) { + print("!!!! tester_rezClicked"); sendToScript({ method: 'tester_rezClicked', itemHref: toUrl(resource), - itemType: entityType}); + itemType: entityType, + itemId: resourceObjectId }); } - ListView { - anchors.fill: parent - anchors.leftMargin: 12 - anchors.bottomMargin: 40 - anchors.rightMargin: 12 - model: resourceListModel - spacing: 5 - interactive: false - - delegate: RowLayout { - anchors.left: parent.left - width: parent.width - spacing: 5 - - property var actions: { - "forward": function(resource, assetType){ - switch(assetType) { - case "application": - Commerce.openApp(resource); - break; - case "avatar": - MyAvatar.useFullAvatarURL(resource); - break; - case "content set": - urlHandler.handleUrl("hifi://localhost/0,0,0"); - Commerce.replaceContentSet(toUrl(resource), ""); - break; - case "entity": - case "wearable": - rezEntity(resource, assetType); - break; - default: - print("Marketplace item tester unsupported assetType " + assetType); - } - }, - "trash": function(resource, assetType){ - if ("application" === assetType) { - Commerce.uninstallApp(resource); - } - sendToScript({ - method: "tester_deleteResourceObject", - objectId: resourceListModel.get(index).id}); - resourceListModel.remove(index); - } - } - - Column { - Layout.preferredWidth: root.width * .6 - spacing: 5 - Text { - text: { - var match = resource.match(/\/([^/]*)$/); - return match ? match[1] : resource; - } - font.pointSize: 12 - horizontalAlignment: Text.AlignBottom - } - Text { - text: resource - font.pointSize: 8 - width: root.width * .6 - horizontalAlignment: Text.AlignBottom - wrapMode: Text.WrapAnywhere - } - } - - ComboBox { - id: comboBox - - Layout.preferredWidth: root.width * .2 - - model: [ - "application", - "avatar", - "content set", - "entity", - "wearable", - "unknown" - ] - - currentIndex: (("entity or wearable" === assetType) ? - model.indexOf("unknown") : model.indexOf(assetType)) - - Component.onCompleted: { - onCurrentIndexChanged.connect(function() { - assetType = model[currentIndex]; - sendToScript({ - method: "tester_updateResourceObjectAssetType", - objectId: resourceListModel.get(index)["id"], - assetType: assetType }); - }); - } - } - - Repeater { - model: [ "forward", "trash" ] - - HifiStylesUit.HiFiGlyphs { - property var glyphs: { - "application": hifi.glyphs.install, - "avatar": hifi.glyphs.avatar, - "content set": hifi.glyphs.globe, - "entity": hifi.glyphs.wand, - "trash": hifi.glyphs.trash, - "unknown": hifi.glyphs.circleSlash, - "wearable": hifi.glyphs.hat, - } - text: (("trash" === modelData) ? - glyphs.trash : - glyphs[comboBox.model[comboBox.currentIndex]]) - size: ("trash" === modelData) ? 22 : 30 - color: hifi.colors.black - horizontalAlignment: Text.AlignHCenter - MouseArea { - anchors.fill: parent - onClicked: { - actions[modelData](resource, comboBox.currentText); - } - } - } - } - } - - headerPositioning: ListView.OverlayHeader - header: HifiStylesUit.RalewayRegular { - id: rootHeader - text: "Marketplace Item Tester" - height: 80 - width: paintedWidth - size: 22 - color: hifi.colors.black - anchors.left: parent.left - anchors.leftMargin: 12 - } - - footerPositioning: ListView.OverlayFooter - footer: Row { - id: rootActions - spacing: 20 - anchors.horizontalCenter: parent.horizontalCenter - - property string currentAction - property var actions: { - "Load File": function(){ - rootActions.currentAction = "load file"; - Window.browseChanged.connect(onResourceSelected); - Window.browseAsync("Please select a file (*.app.json *.json *.fst *.json.gz)", "", "Assets (*.app.json *.json *.fst *.json.gz)"); - }, - "Load URL": function(){ - rootActions.currentAction = "load url"; - Window.promptTextChanged.connect(onResourceSelected); - Window.promptAsync("Please enter a URL", ""); - } - } - - function onResourceSelected(resource) { - // It is possible that we received the present signal - // from something other than our browserAsync window. - // Alas, there is nothing we can do about that so charge - // ahead as though we are sure the present signal is one - // we expect. - switch(currentAction) { - case "load file": - Window.browseChanged.disconnect(onResourceSelected); - break - case "load url": - Window.promptTextChanged.disconnect(onResourceSelected); - break; - } - if (resource) { - var resourceObj = buildResourceObj(resource); - installResourceObj(resourceObj); - sendToScript({ - method: 'tester_newResourceObject', - resourceObject: resourceObj }); - } - } - - Repeater { - model: [ "Load File", "Load URL" ] - HifiControlsUit.Button { - color: hifi.buttons.blue - fontSize: 20 - text: modelData - width: root.width / 3 - height: 40 - onClicked: actions[text]() - } - } - } - } + signal sendToScript(var message) } diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif b/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif index 00f75ae62f..0536bd1884 100644 Binary files a/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif and b/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif differ diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e2c4df22db..e515a22403 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -226,6 +226,7 @@ #include "commerce/Ledger.h" #include "commerce/Wallet.h" #include "commerce/QmlCommerce.h" +#include "ResourceRequestObserver.h" #include "webbrowser/WebBrowserSuggestionsEngine.h" #include @@ -947,6 +948,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -3129,6 +3131,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); surfaceContext->setContextProperty("Wallet", DependencyManager::get().data()); surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); + surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); @@ -5022,12 +5025,12 @@ void Application::saveSettings() const { PluginManager::getInstance()->saveSettings(); } -bool Application::importEntities(const QString& urlOrFilename) { +bool Application::importEntities(const QString& urlOrFilename, const bool isObservable, const qint64 callerId) { bool success = false; _entityClipboard->withWriteLock([&] { _entityClipboard->eraseAllOctreeElements(); - success = _entityClipboard->readFromURL(urlOrFilename); + success = _entityClipboard->readFromURL(urlOrFilename, isObservable, callerId); if (success) { _entityClipboard->reaverageOctreeElements(); } @@ -6811,6 +6814,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("Wallet", DependencyManager::get().data()); scriptEngine->registerGlobalObject("AddressManager", DependencyManager::get().data()); scriptEngine->registerGlobalObject("HifiAbout", AboutUtil::getInstance()); + scriptEngine->registerGlobalObject("ResourceRequestObserver", DependencyManager::get().data()); qScriptRegisterMetaType(scriptEngine.data(), OverlayIDtoScriptValue, OverlayIDfromScriptValue); @@ -7197,7 +7201,8 @@ void Application::addAssetToWorldFromURL(QString url) { addAssetToWorldInfo(filename, "Downloading model file " + filename + "."); - auto request = DependencyManager::get()->createResourceRequest(nullptr, QUrl(url)); + auto request = DependencyManager::get()->createResourceRequest( + nullptr, QUrl(url), true, -1, "Application::addAssetToWorldFromURL"); connect(request, &ResourceRequest::finished, this, &Application::addAssetToWorldFromURLRequestFinished); request->send(); } diff --git a/interface/src/Application.h b/interface/src/Application.h index ed51e085b2..c3896a64e4 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -342,7 +342,7 @@ public slots: QVector pasteEntities(float x, float y, float z); bool exportEntities(const QString& filename, const QVector& entityIDs, const glm::vec3* givenOffset = nullptr); bool exportEntities(const QString& filename, float x, float y, float z, float scale); - bool importEntities(const QString& url); + bool importEntities(const QString& url, const bool isObservable = true, const qint64 callerId = -1); void updateThreadPoolCount() const; void updateSystemTabletMode(); void goToErrorDomainURL(QUrl errorDomainURL); diff --git a/interface/src/assets/ATPAssetMigrator.cpp b/interface/src/assets/ATPAssetMigrator.cpp index 45ac80b054..be7f2014cc 100644 --- a/interface/src/assets/ATPAssetMigrator.cpp +++ b/interface/src/assets/ATPAssetMigrator.cpp @@ -53,7 +53,8 @@ void ATPAssetMigrator::loadEntityServerFile() { auto migrateResources = [=](QUrl migrationURL, QJsonValueRef jsonValue, bool isModelURL) { auto request = - DependencyManager::get()->createResourceRequest(this, migrationURL); + DependencyManager::get()->createResourceRequest( + this, migrationURL, true, -1, "ATPAssetMigrator::loadEntityServerFile"); if (request) { qCDebug(asset_migrator) << "Requesting" << migrationURL << "for ATP asset migration"; diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 230f8aa64b..53074ac4ba 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -135,7 +135,7 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm:: glm::vec3 palmPosition; glm::quat palmRotation; - bool isTransitingWithAvatar = holdingAvatar->getTransit()->isTransiting(); + bool isTransitingWithAvatar = holdingAvatar->getTransit()->isActive(); if (isTransitingWithAvatar != _isTransitingWithAvatar) { _isTransitingWithAvatar = isTransitingWithAvatar; auto ownerEntity = _ownerEntity.lock(); @@ -424,7 +424,7 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) { if (ownerEntity) { ownerEntity->setDynamicDataDirty(true); ownerEntity->setDynamicDataNeedsTransmit(true); - ownerEntity->setTransitingWithAvatar(myAvatar->getTransit()->isTransiting()); + ownerEntity->setTransitingWithAvatar(myAvatar->getTransit()->isActive()); } }); } diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index c9b3449cec..dad4c44f4b 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -71,14 +71,12 @@ AvatarManager::AvatarManager(QObject* parent) : } }); - const float AVATAR_TRANSIT_TRIGGER_DISTANCE = 1.0f; - const int AVATAR_TRANSIT_FRAME_COUNT = 11; // Based on testing - const int AVATAR_TRANSIT_FRAMES_PER_METER = 1; // Based on testing - _transitConfig._totalFrames = AVATAR_TRANSIT_FRAME_COUNT; - _transitConfig._triggerDistance = AVATAR_TRANSIT_TRIGGER_DISTANCE; + _transitConfig._minTriggerDistance = AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE; + _transitConfig._maxTriggerDistance = AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE; _transitConfig._framesPerMeter = AVATAR_TRANSIT_FRAMES_PER_METER; - _transitConfig._isDistanceBased = true; + _transitConfig._isDistanceBased = AVATAR_TRANSIT_DISTANCE_BASED; + _transitConfig._abortDistance = AVATAR_TRANSIT_ABORT_DISTANCE; } AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { @@ -126,13 +124,39 @@ void AvatarManager::setSpace(workload::SpacePointer& space ) { _space = space; } +void AvatarManager::handleTransitAnimations(AvatarTransit::Status status) { + switch (status) { + case AvatarTransit::Status::STARTED: + _myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("preTransitAnim"); + break; + case AvatarTransit::Status::START_TRANSIT: + _myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("transitAnim"); + break; + case AvatarTransit::Status::END_TRANSIT: + _myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("postTransitAnim"); + break; + case AvatarTransit::Status::ENDED: + _myAvatar->getSkeletonModel()->getRig().triggerNetworkAnimation("idleAnim"); + break; + case AvatarTransit::Status::PRE_TRANSIT: + break; + case AvatarTransit::Status::POST_TRANSIT: + break; + case AvatarTransit::Status::IDLE: + break; + case AvatarTransit::Status::TRANSITING: + break; + case AvatarTransit::Status::ABORT_TRANSIT: + break; + } +} + void AvatarManager::updateMyAvatar(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "AvatarManager::updateMyAvatar()"); - AvatarTransit::Status status = _myAvatar->updateTransit(deltaTime, _myAvatar->getNextPosition(), _transitConfig); - bool sendFirstTransitPackage = (status == AvatarTransit::Status::START_TRANSIT); - bool blockTransitData = (status == AvatarTransit::Status::TRANSITING); + AvatarTransit::Status status = _myAvatar->updateTransit(deltaTime, _myAvatar->getNextPosition(), _myAvatar->getSensorToWorldScale(), _transitConfig); + handleTransitAnimations(status); _myAvatar->update(deltaTime); render::Transaction transaction; @@ -142,18 +166,13 @@ void AvatarManager::updateMyAvatar(float deltaTime) { quint64 now = usecTimestampNow(); quint64 dt = now - _lastSendAvatarDataTime; - - if (sendFirstTransitPackage || (dt > MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS && !_myAvatarDataPacketsPaused && !blockTransitData)) { + if (dt > MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS && !_myAvatarDataPacketsPaused) { // send head/hand data to the avatar mixer and voxel server - PerformanceTimer perfTimer("send"); - if (sendFirstTransitPackage) { - _myAvatar->overrideNextPackagePositionData(_myAvatar->getTransit()->getEndPosition()); - } + PerformanceTimer perfTimer("send"); _myAvatar->sendAvatarDataPacket(); _lastSendAvatarDataTime = now; _myAvatarSendRate.increment(); } - } @@ -267,7 +286,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { if (inView && avatar->hasNewJointData()) { numAvatarsUpdated++; } - auto transitStatus = avatar->_transit.update(deltaTime, avatar->_globalPosition, _transitConfig); + auto transitStatus = avatar->_transit.update(deltaTime, avatar->_serverPosition, _transitConfig); if (avatar->getIsNewAvatar() && (transitStatus == AvatarTransit::Status::START_TRANSIT || transitStatus == AvatarTransit::Status::ABORT_TRANSIT)) { avatar->_transit.reset(); avatar->setIsNewAvatar(false); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 34aa4f2cc0..78dac2d3ad 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -221,6 +221,7 @@ private: // frequently grabs a read lock on the hash to get a given avatar by ID void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason) override; + void handleTransitAnimations(AvatarTransit::Status status); QVector _avatarsToFade; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index a7c8c3e179..3299bd10e7 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -463,10 +464,74 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { } } +void MyAvatar::updateSitStandState(float newHeightReading, float dt) { + const float STANDING_HEIGHT_MULTIPLE = 1.2f; + const float SITTING_HEIGHT_MULTIPLE = 0.833f; + const float SITTING_TIMEOUT = 4.0f; // 4 seconds + const float STANDING_TIMEOUT = 0.3333f; // 1/3 second + const float SITTING_UPPER_BOUND = 1.52f; + if (!getIsSitStandStateLocked()) { + if (!getIsAway() && qApp->isHMDMode()) { + if (getIsInSittingState()) { + if (newHeightReading > (STANDING_HEIGHT_MULTIPLE * _tippingPoint)) { + // if we recenter upwards then no longer in sitting state + _sitStandStateTimer += dt; + if (_sitStandStateTimer > STANDING_TIMEOUT) { + _averageUserHeightSensorSpace = newHeightReading; + _tippingPoint = newHeightReading; + setIsInSittingState(false); + } + } else if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) { + // if we are mis labelled as sitting but we are standing in the real world this will + // make sure that a real sit is still recognized so we won't be stuck in sitting unable to change state + _sitStandStateTimer += dt; + if (_sitStandStateTimer > SITTING_TIMEOUT) { + _averageUserHeightSensorSpace = newHeightReading; + _tippingPoint = newHeightReading; + // here we stay in sit state but reset the average height + setIsInSittingState(true); + } + } else { + // sanity check if average height greater than 5ft they are not sitting(or get off your dangerous barstool please) + if (_averageUserHeightSensorSpace > SITTING_UPPER_BOUND) { + setIsInSittingState(false); + } else { + // tipping point is average height when sitting. + _tippingPoint = _averageUserHeightSensorSpace; + _sitStandStateTimer = 0.0f; + } + } + } else { + // in the standing state + if (newHeightReading < (SITTING_HEIGHT_MULTIPLE * _tippingPoint)) { + _sitStandStateTimer += dt; + if (_sitStandStateTimer > SITTING_TIMEOUT) { + _averageUserHeightSensorSpace = newHeightReading; + _tippingPoint = newHeightReading; + setIsInSittingState(true); + } + } else { + // use the mode height for the tipping point when we are standing. + _tippingPoint = getCurrentStandingHeight(); + _sitStandStateTimer = 0.0f; + } + } + } else { + //if you are away then reset the average and set state to standing. + _averageUserHeightSensorSpace = _userHeight.get(); + _tippingPoint = _userHeight.get(); + setIsInSittingState(false); + } + } +} + void MyAvatar::update(float deltaTime) { // update moving average of HMD facing in xz plane. const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); const float PERCENTAGE_WEIGHT_HEAD_VS_SHOULDERS_AZIMUTH = 0.0f; // 100 percent shoulders + const float COSINE_THIRTY_DEGREES = 0.866f; + const float SQUATTY_TIMEOUT = 30.0f; // 30 seconds + const float HEIGHT_FILTER_COEFFICIENT = 0.01f; float tau = deltaTime / HMD_FACING_TIMESCALE; setHipToHandController(computeHandAzimuth()); @@ -493,11 +558,36 @@ 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))); + controller::Pose newHeightReading = getControllerPoseInSensorFrame(controller::Action::HEAD); + if (newHeightReading.isValid()) { + int newHeightReadingInCentimeters = glm::floor(newHeightReading.getTranslation().y * CENTIMETERS_PER_METER); + _averageUserHeightSensorSpace = lerp(_averageUserHeightSensorSpace, newHeightReading.getTranslation().y, HEIGHT_FILTER_COEFFICIENT); + _recentModeReadings.insert(newHeightReadingInCentimeters); + setCurrentStandingHeight(computeStandingHeightMode(newHeightReading)); + setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); + } + + // if the spine is straight and the head is below the default position by 5 cm then increment squatty count. + const float SQUAT_THRESHOLD = 0.05f; + glm::vec3 headDefaultPositionAvatarSpace = getAbsoluteDefaultJointTranslationInObjectFrame(getJointIndex("Head")); + glm::quat spine2OrientationAvatarSpace = getAbsoluteJointRotationInObjectFrame(getJointIndex("Spine2")); + glm::vec3 upSpine2 = spine2OrientationAvatarSpace * glm::vec3(0.0f, 1.0f, 0.0f); + if (glm::length(upSpine2) > 0.0f) { + upSpine2 = glm::normalize(upSpine2); + } + float angleSpine2 = glm::dot(upSpine2, glm::vec3(0.0f, 1.0f, 0.0f)); + if (getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y < (headDefaultPositionAvatarSpace.y - SQUAT_THRESHOLD) && (angleSpine2 > COSINE_THIRTY_DEGREES)) { + _squatTimer += deltaTime; + if (_squatTimer > SQUATTY_TIMEOUT) { + _squatTimer = 0.0f; + _follow._squatDetected = true; + } + } else { + _squatTimer = 0.0f; + } + + // put update sit stand state counts here + updateSitStandState(newHeightReading.getTranslation().y, deltaTime); if (_drawAverageFacingEnabled) { auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); @@ -968,7 +1058,6 @@ void MyAvatar::updateSensorToWorldMatrix() { updateJointFromController(controller::Action::RIGHT_HAND, _controllerRightHandMatrixCache); if (hasSensorToWorldScaleChanged) { - setTransitScale(sensorToWorldScale); emit sensorToWorldScaleChanged(sensorToWorldScale); } @@ -3557,12 +3646,9 @@ glm::vec3 MyAvatar::computeCounterBalance() { glm::vec3 counterBalancedCg = (1.0f / DEFAULT_AVATAR_HIPS_MASS) * counterBalancedForHead; // find the height of the hips - const float UPPER_LEG_FRACTION = 0.3333f; glm::vec3 xzDiff((cgHeadMass.position.x - counterBalancedCg.x), 0.0f, (cgHeadMass.position.z - counterBalancedCg.z)); float headMinusHipXz = glm::length(xzDiff); float headHipDefault = glm::length(tposeHead - tposeHips); - float hipFootDefault = tposeHips.y - tposeRightFoot.y; - float sitSquatThreshold = tposeHips.y - (UPPER_LEG_FRACTION * hipFootDefault); float hipHeight = 0.0f; if (headHipDefault > headMinusHipXz) { hipHeight = sqrtf((headHipDefault * headHipDefault) - (headMinusHipXz * headMinusHipXz)); @@ -3574,10 +3660,6 @@ glm::vec3 MyAvatar::computeCounterBalance() { if (counterBalancedCg.y > (tposeHips.y + 0.05f)) { // if the height is higher than default hips, clamp to default hips counterBalancedCg.y = tposeHips.y + 0.05f; - } else if (counterBalancedCg.y < sitSquatThreshold) { - //do a height reset - setResetMode(true); - _follow.activate(FollowHelper::Vertical); } return counterBalancedCg; } @@ -3818,6 +3900,18 @@ bool MyAvatar::getIsInWalkingState() const { return _isInWalkingState; } +bool MyAvatar::getIsInSittingState() const { + return _isInSittingState.get(); +} + +MyAvatar::SitStandModelType MyAvatar::getUserRecenterModel() const { + return _userRecenterModel.get(); +} + +bool MyAvatar::getIsSitStandStateLocked() const { + return _lockSitStandState.get(); +} + float MyAvatar::getWalkSpeed() const { return _walkSpeed.get() * _walkSpeedScalar; } @@ -3838,6 +3932,61 @@ void MyAvatar::setIsInWalkingState(bool isWalking) { _isInWalkingState = isWalking; } +void MyAvatar::setIsInSittingState(bool isSitting) { + _sitStandStateTimer = 0.0f; + _squatTimer = 0.0f; + // on reset height we need the count to be more than one in case the user sits and stands up quickly. + _isInSittingState.set(isSitting); + setResetMode(true); + if (isSitting) { + setCenterOfGravityModelEnabled(false); + } else { + setCenterOfGravityModelEnabled(true); + } + setSitStandStateChange(true); +} + +void MyAvatar::setUserRecenterModel(MyAvatar::SitStandModelType modelName) { + + _userRecenterModel.set(modelName); + + switch (modelName) { + case MyAvatar::SitStandModelType::ForceSit: + setHMDLeanRecenterEnabled(true); + setIsInSittingState(true); + setIsSitStandStateLocked(true); + break; + case MyAvatar::SitStandModelType::ForceStand: + setHMDLeanRecenterEnabled(true); + setIsInSittingState(false); + setIsSitStandStateLocked(true); + break; + case MyAvatar::SitStandModelType::Auto: + default: + setHMDLeanRecenterEnabled(true); + setIsInSittingState(false); + setIsSitStandStateLocked(false); + break; + case MyAvatar::SitStandModelType::DisableHMDLean: + setHMDLeanRecenterEnabled(false); + setIsInSittingState(false); + setIsSitStandStateLocked(false); + break; + } +} + +void MyAvatar::setIsSitStandStateLocked(bool isLocked) { + _lockSitStandState.set(isLocked); + _sitStandStateTimer = 0.0f; + _squatTimer = 0.0f; + _averageUserHeightSensorSpace = _userHeight.get(); + _tippingPoint = _userHeight.get(); + if (!isLocked) { + // always start the auto transition mode in standing state. + setIsInSittingState(false); + } +} + void MyAvatar::setWalkSpeed(float value) { _walkSpeed.set(value); } @@ -3854,6 +4003,14 @@ float MyAvatar::getSprintSpeed() const { return _sprintSpeed.get(); } +void MyAvatar::setSitStandStateChange(bool stateChanged) { + _sitStandStateChange = stateChanged; +} + +float MyAvatar::getSitStandStateChange() const { + return _sitStandStateChange; +} + QVector MyAvatar::getScriptUrls() { QVector scripts = _skeletonModel->isLoaded() ? _skeletonModel->getFBXGeometry().scripts : QVector(); return scripts; @@ -3997,6 +4154,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, // x axis of currentBodyMatrix in world space. glm::vec3 right = glm::normalize(glm::vec3(currentBodyMatrix[0][0], currentBodyMatrix[1][0], currentBodyMatrix[2][0])); glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix); + controller::Pose currentHeadPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD); float forwardLeanAmount = glm::dot(forward, offset); float lateralLeanAmount = glm::dot(right, offset); @@ -4005,14 +4163,19 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, const float MAX_FORWARD_LEAN = 0.15f; const float MAX_BACKWARD_LEAN = 0.1f; - - if (forwardLeanAmount > 0 && forwardLeanAmount > MAX_FORWARD_LEAN) { - return true; + bool stepDetected = false; + if (myAvatar.getIsInSittingState()) { + if (!withinBaseOfSupport(currentHeadPose)) { + stepDetected = true; + } + } else if (forwardLeanAmount > 0 && forwardLeanAmount > MAX_FORWARD_LEAN) { + stepDetected = true; } else if (forwardLeanAmount < 0 && forwardLeanAmount < -MAX_BACKWARD_LEAN) { - return true; + stepDetected = true; + } else { + stepDetected = fabs(lateralLeanAmount) > MAX_LATERAL_LEAN; } - - return fabs(lateralLeanAmount) > MAX_LATERAL_LEAN; + return stepDetected; } bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) const { @@ -4021,6 +4184,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons 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); + controller::Pose currentHeadSensorPose = myAvatar.getControllerPoseInSensorFrame(controller::Action::HEAD); bool stepDetected = false; float myScale = myAvatar.getAvatarScale(); @@ -4030,7 +4194,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons } else { if (!withinBaseOfSupport(currentHeadPose) && headAngularVelocityBelowThreshold(currentHeadPose) && - isWithinThresholdHeightMode(currentHeadPose, myAvatar.getCurrentStandingHeight(), myScale) && + isWithinThresholdHeightMode(currentHeadSensorPose, myAvatar.getCurrentStandingHeight(), myScale) && handDirectionMatchesHeadDirection(currentLeftHandPose, currentRightHandPose, currentHeadPose) && handAngularVelocityBelowThreshold(currentLeftHandPose, currentRightHandPose) && headVelocityGreaterThanThreshold(currentHeadPose) && @@ -4046,6 +4210,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons glm::vec3 currentHeadPosition = currentHeadPose.getTranslation(); float anatomicalHeadToHipsDistance = glm::length(defaultHeadPosition - defaultHipsPosition); if (!isActive(Horizontal) && + (!isActive(Vertical)) && (glm::length(currentHeadPosition - defaultHipsPosition) > (anatomicalHeadToHipsDistance + (DEFAULT_AVATAR_SPINE_STRETCH_LIMIT * anatomicalHeadToHipsDistance)))) { myAvatar.setResetMode(true); stepDetected = true; @@ -4061,10 +4226,32 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) cons bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { const float CYLINDER_TOP = 0.1f; const float CYLINDER_BOTTOM = -1.5f; + const float SITTING_BOTTOM = -0.02f; glm::vec3 offset = extractTranslation(desiredBodyMatrix) - extractTranslation(currentBodyMatrix); + bool returnValue = false; - return (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + if (myAvatar.getSitStandStateChange()) { + returnValue = true; + } else { + if (myAvatar.getIsInSittingState()) { + if (myAvatar.getIsSitStandStateLocked()) { + returnValue = (offset.y > CYLINDER_TOP); + } + if (offset.y < SITTING_BOTTOM) { + // we recenter more easily when in sitting state. + returnValue = true; + } + } else { + // in the standing state + returnValue = (offset.y > CYLINDER_TOP) || (offset.y < CYLINDER_BOTTOM); + // finally check for squats in standing + if (_squatDetected) { + returnValue = true; + } + } + } + return returnValue; } void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, @@ -4085,9 +4272,10 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } } } else { + // center of gravity model is not enabled if (!isActive(Horizontal) && (shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Horizontal); - if (myAvatar.getEnableStepResetRotation()) { + if (myAvatar.getEnableStepResetRotation() && !myAvatar.getIsInSittingState()) { activate(Rotation); myAvatar.setHeadControllerFacingMovingAverage(myAvatar.getHeadControllerFacing()); } @@ -4095,6 +4283,9 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat } if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Vertical); + if (_squatDetected) { + _squatDetected = false; + } } } else { if (!isActive(Rotation) && getForceActivateRotation()) { @@ -4144,7 +4335,7 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat myAvatar.getCharacterController()->setFollowParameters(followWorldPose, getMaxTimeRemaining()); } -glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(const MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix) { +glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix) { if (isActive()) { float dt = myAvatar.getCharacterController()->getFollowTime(); decrementTimeRemaining(dt); @@ -4161,6 +4352,11 @@ glm::mat4 MyAvatar::FollowHelper::postPhysicsUpdate(const MyAvatar& myAvatar, co glm::mat4 newBodyMat = createMatFromQuatAndPos(sensorAngularDisplacement * glmExtractRotation(currentBodyMatrix), sensorLinearDisplacement + extractTranslation(currentBodyMatrix)); + if (myAvatar.getSitStandStateChange()) { + myAvatar.setSitStandStateChange(false); + deactivate(Vertical); + setTranslation(newBodyMat, extractTranslation(myAvatar.deriveBodyFromHMDSensor())); + } return newBodyMat; } else { return currentBodyMatrix; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 08a7c09fa4..799427530a 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -21,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -142,6 +141,8 @@ class MyAvatar : public Avatar { * @property {number} walkSpeed * @property {number} walkBackwardSpeed * @property {number} sprintSpeed + * @property {number} isInSittingState + * @property {number} userRecenterModel * * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the * registration point of the 3D model. @@ -242,6 +243,9 @@ class MyAvatar : public Avatar { Q_PROPERTY(float walkSpeed READ getWalkSpeed WRITE setWalkSpeed); Q_PROPERTY(float walkBackwardSpeed READ getWalkBackwardSpeed WRITE setWalkBackwardSpeed); Q_PROPERTY(float sprintSpeed READ getSprintSpeed WRITE setSprintSpeed); + Q_PROPERTY(bool isInSittingState READ getIsInSittingState WRITE setIsInSittingState); + Q_PROPERTY(MyAvatar::SitStandModelType userRecenterModel READ getUserRecenterModel WRITE setUserRecenterModel); + Q_PROPERTY(bool isSitStandStateLocked READ getIsSitStandStateLocked WRITE setIsSitStandStateLocked); const QString DOMINANT_LEFT_HAND = "left"; const QString DOMINANT_RIGHT_HAND = "right"; @@ -262,6 +266,15 @@ public: }; Q_ENUM(DriveKeys) + enum SitStandModelType { + ForceSit = 0, + ForceStand, + Auto, + DisableHMDLean, + NumSitStandTypes + }; + Q_ENUM(SitStandModelType) + explicit MyAvatar(QThread* thread); virtual ~MyAvatar(); @@ -1121,12 +1134,21 @@ public: void setIsInWalkingState(bool isWalking); bool getIsInWalkingState() const; + void setIsInSittingState(bool isSitting); + bool getIsInSittingState() const; + void setUserRecenterModel(MyAvatar::SitStandModelType modelName); + MyAvatar::SitStandModelType getUserRecenterModel() const; + void setIsSitStandStateLocked(bool isLocked); + bool getIsSitStandStateLocked() const; void setWalkSpeed(float value); float getWalkSpeed() const; void setWalkBackwardSpeed(float value); float getWalkBackwardSpeed() const; void setSprintSpeed(float value); float getSprintSpeed() const; + void setSitStandStateChange(bool stateChanged); + float getSitStandStateChange() const; + void updateSitStandState(float newHeightReading, float dt); QVector getScriptUrls(); @@ -1532,6 +1554,7 @@ signals: */ void disableHandTouchForIDChanged(const QUuid& entityID, bool disable); + private slots: void leaveDomain(); void updateCollisionCapsuleCache(); @@ -1742,7 +1765,7 @@ private: 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); + glm::mat4 postPhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix); bool getForceActivateRotation() const; void setForceActivateRotation(bool val); bool getForceActivateVertical() const; @@ -1751,6 +1774,7 @@ private: void setForceActivateHorizontal(bool val); bool getToggleHipsFollowing() const; void setToggleHipsFollowing(bool followHead); + bool _squatDetected { false }; std::atomic _forceActivateRotation { false }; std::atomic _forceActivateVertical { false }; std::atomic _forceActivateHorizontal { false }; @@ -1820,10 +1844,13 @@ private: std::mutex _pinnedJointsMutex; std::vector _pinnedJoints; + void updateChildCauterization(SpatiallyNestablePointer object, bool cauterize); + // height of user in sensor space, when standing erect. ThreadSafeValueCache _userHeight { DEFAULT_AVATAR_HEIGHT }; - - void updateChildCauterization(SpatiallyNestablePointer object, bool cauterize); + float _averageUserHeightSensorSpace { _userHeight.get() }; + bool _sitStandStateChange { false }; + ThreadSafeValueCache _lockSitStandState { false }; // max unscaled forward movement speed ThreadSafeValueCache _walkSpeed { DEFAULT_AVATAR_MAX_WALKING_SPEED }; @@ -1831,6 +1858,11 @@ private: ThreadSafeValueCache _sprintSpeed { AVATAR_SPRINT_SPEED_SCALAR }; float _walkSpeedScalar { AVATAR_WALK_SPEED_SCALAR }; bool _isInWalkingState { false }; + ThreadSafeValueCache _isInSittingState { false }; + ThreadSafeValueCache _userRecenterModel { MyAvatar::SitStandModelType::Auto }; + float _sitStandStateTimer { 0.0f }; + float _squatTimer { 0.0f }; + float _tippingPoint { _userHeight.get() }; // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index c1a49d7a10..e5b9df7dd1 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() && !isFlying && !(myAvatar->getIsInWalkingState()) && myAvatar->getHMDLeanRecenterEnabled()) { + if (myAvatar->getCenterOfGravityModelEnabled() && !isFlying && !(myAvatar->getIsInWalkingState()) && !(myAvatar->getIsInSittingState()) && myAvatar->getHMDLeanRecenterEnabled()) { // then we use center of gravity model hipsMat = myAvatar->deriveBodyUsingCgModel(); } else { @@ -250,6 +250,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { bool headExists = _rig.getAbsoluteJointPoseInRigFrame(_rig.indexOfJoint("Head"), currentHeadPose); bool hipsExists = _rig.getAbsoluteJointPoseInRigFrame(_rig.indexOfJoint("Hips"), currentHipsPose); if (spine2Exists && headExists && hipsExists) { + AnimPose rigSpaceYaw(myAvatar->getSpine2RotationRigSpace()); glm::vec3 u, v, w; glm::vec3 fwd = rigSpaceYaw.rot() * glm::vec3(0.0f, 0.0f, 1.0f); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index aa39fdc1b9..ffe89ffc5b 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -315,7 +315,7 @@ QString QmlCommerce::getInstalledApps(const QString& justInstalledAppID) { return installedAppsFromMarketplace; } -bool QmlCommerce::installApp(const QString& itemHref) { +bool QmlCommerce::installApp(const QString& itemHref, const bool& alsoOpenImmediately) { if (!QDir(_appsPath).exists()) { if (!QDir().mkdir(_appsPath)) { qCDebug(commerce) << "Couldn't make _appsPath directory."; @@ -325,7 +325,8 @@ bool QmlCommerce::installApp(const QString& itemHref) { QUrl appHref(itemHref); - auto request = DependencyManager::get()->createResourceRequest(this, appHref); + auto request = + DependencyManager::get()->createResourceRequest(this, appHref, true, -1, "QmlCommerce::installApp"); if (!request) { qCDebug(commerce) << "Couldn't create resource request for app."; @@ -357,13 +358,22 @@ bool QmlCommerce::installApp(const QString& itemHref) { QJsonObject appFileJsonObject = appFileJsonDocument.object(); QString scriptUrl = appFileJsonObject["scriptURL"].toString(); - if ((DependencyManager::get()->loadScript(scriptUrl.trimmed())).isNull()) { - qCDebug(commerce) << "Couldn't load script."; - return false; + // Don't try to re-load (install) a script if it's already running + QStringList runningScripts = DependencyManager::get()->getRunningScripts(); + if (!runningScripts.contains(scriptUrl)) { + if ((DependencyManager::get()->loadScript(scriptUrl.trimmed())).isNull()) { + qCDebug(commerce) << "Couldn't load script."; + return false; + } + + QFileInfo appFileInfo(appFile); + emit appInstalled(appFileInfo.baseName()); + } + + if (alsoOpenImmediately) { + QmlCommerce::openApp(itemHref); } - QFileInfo appFileInfo(appFile); - emit appInstalled(appFileInfo.baseName()); return true; }); request->send(); diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index bee30e1b62..2e3c0ec24d 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -88,7 +88,7 @@ protected: Q_INVOKABLE void replaceContentSet(const QString& itemHref, const QString& certificateID); Q_INVOKABLE QString getInstalledApps(const QString& justInstalledAppID = ""); - Q_INVOKABLE bool installApp(const QString& appHref); + Q_INVOKABLE bool installApp(const QString& appHref, const bool& alsoOpenImmediately = false); Q_INVOKABLE bool uninstallApp(const QString& appHref); Q_INVOKABLE bool openApp(const QString& appHref); diff --git a/interface/src/scripting/ClipboardScriptingInterface.cpp b/interface/src/scripting/ClipboardScriptingInterface.cpp index c2d2b69883..c14f4ea895 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.cpp +++ b/interface/src/scripting/ClipboardScriptingInterface.cpp @@ -46,11 +46,17 @@ bool ClipboardScriptingInterface::exportEntities(const QString& filename, float return retVal; } -bool ClipboardScriptingInterface::importEntities(const QString& filename) { +bool ClipboardScriptingInterface::importEntities( + const QString& filename, + const bool isObservable, + const qint64 callerId +) { bool retVal; BLOCKING_INVOKE_METHOD(qApp, "importEntities", Q_RETURN_ARG(bool, retVal), - Q_ARG(const QString&, filename)); + Q_ARG(const QString&, filename), + Q_ARG(const bool, isObservable), + Q_ARG(const qint64, callerId)); return retVal; } diff --git a/interface/src/scripting/ClipboardScriptingInterface.h b/interface/src/scripting/ClipboardScriptingInterface.h index 32b8c64a7d..60b6ca2e03 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.h +++ b/interface/src/scripting/ClipboardScriptingInterface.h @@ -50,9 +50,11 @@ public: * You can generate a JSON file using {@link Clipboard.exportEntities}. * @function Clipboard.importEntities * @param {string} filename Path and name of file to import. + * @param {boolean} does the ResourceRequestObserver observe this request? + * @param {number} optional internal id of object causing this import. * @returns {boolean} true if the import was successful, otherwise false. */ - Q_INVOKABLE bool importEntities(const QString& filename); + Q_INVOKABLE bool importEntities(const QString& filename, const bool isObservable = true, const qint64 callerId = -1); /**jsdoc * Export the entities specified to a JSON file. diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 5eccef5e9d..34d80f50cf 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -259,6 +259,39 @@ void setupPreferences() { auto preference = new CheckPreference(VR_MOVEMENT, "Show room boundaries while teleporting", getter, setter); preferences->addPreference(preference); } + { + auto getter = [myAvatar]()->int { + switch (myAvatar->getUserRecenterModel()) { + case MyAvatar::SitStandModelType::Auto: + default: + return 0; + case MyAvatar::SitStandModelType::ForceSit: + return 1; + case MyAvatar::SitStandModelType::DisableHMDLean: + return 2; + } + }; + auto setter = [myAvatar](int value) { + switch (value) { + case 0: + default: + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::Auto); + break; + case 1: + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::ForceSit); + break; + case 2: + myAvatar->setUserRecenterModel(MyAvatar::SitStandModelType::DisableHMDLean); + break; + } + }; + auto preference = new RadioButtonsPreference(VR_MOVEMENT, "Auto / Force Sit / Disable Recenter", getter, setter); + QStringList items; + items << "Auto - turns on avatar leaning when standing in real world" << "Seated - disables all avatar leaning while sitting in real world" << "Disabled - allows avatar sitting on the floor [Experimental]"; + preference->setHeading("Avatar leaning behavior"); + preference->setItems(items); + preferences->addPreference(preference); + } { auto getter = [=]()->float { return myAvatar->getUserHeight(); }; auto setter = [=](float value) { myAvatar->setUserHeight(value); }; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 9d55c91ef3..084615cae2 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -59,6 +59,7 @@ #include "raypick/PointerScriptingInterface.h" #include #include "AboutUtil.h" +#include "ResourceRequestObserver.h" static int MAX_WINDOW_SIZE = 4096; static const float METERS_TO_INCHES = 39.3701f; @@ -269,6 +270,7 @@ void Web3DOverlay::setupQmlSurface(bool isTablet) { _webSurface->getSurfaceContext()->setContextProperty("Window", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); _webSurface->getSurfaceContext()->setContextProperty("HiFiAbout", AboutUtil::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); // Override min fps for tablet UI, for silky smooth scrolling setMaxFPS(90); diff --git a/libraries/animation/src/AnimClip.h b/libraries/animation/src/AnimClip.h index eba361fd4c..92f4c01dcc 100644 --- a/libraries/animation/src/AnimClip.h +++ b/libraries/animation/src/AnimClip.h @@ -51,6 +51,8 @@ public: bool getMirrorFlag() const { return _mirrorFlag; } void setMirrorFlag(bool mirrorFlag) { _mirrorFlag = mirrorFlag; } + float getFrame() const { return _frame; } + void loadURL(const QString& url); protected: diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 341b554949..83d375b55a 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -31,6 +31,7 @@ #include "AnimSkeleton.h" #include "AnimUtil.h" #include "IKTarget.h" +#include "PathUtils.h" static int nextRigId = 1; @@ -133,6 +134,30 @@ void Rig::overrideAnimation(const QString& url, float fps, bool loop, float firs _animVars.set("userAnimB", clipNodeEnum == UserAnimState::B); } +void Rig::triggerNetworkAnimation(const QString& animName) { + _networkVars.set("idleAnim", false); + _networkVars.set("preTransitAnim", false); + _networkVars.set("transitAnim", false); + _networkVars.set("postTransitAnim", false); + _sendNetworkNode = true; + + if (animName == "idleAnim") { + _networkVars.set("idleAnim", true); + _networkAnimState.clipNodeEnum = NetworkAnimState::Idle; + _sendNetworkNode = false; + } else if (animName == "preTransitAnim") { + _networkVars.set("preTransitAnim", true); + _networkAnimState.clipNodeEnum = NetworkAnimState::PreTransit; + } else if (animName == "transitAnim") { + _networkVars.set("transitAnim", true); + _networkAnimState.clipNodeEnum = NetworkAnimState::Transit; + } else if (animName == "postTransitAnim") { + _networkVars.set("postTransitAnim", true); + _networkAnimState.clipNodeEnum = NetworkAnimState::PostTransit; + } + +} + void Rig::restoreAnimation() { if (_userAnimState.clipNodeEnum != UserAnimState::None) { _userAnimState.clipNodeEnum = UserAnimState::None; @@ -144,6 +169,16 @@ void Rig::restoreAnimation() { } } +void Rig::restoreNetworkAnimation() { + if (_networkAnimState.clipNodeEnum != NetworkAnimState::Idle) { + _networkAnimState.clipNodeEnum = NetworkAnimState::Idle; + _networkVars.set("idleAnim", true); + _networkVars.set("preTransitAnim", false); + _networkVars.set("transitAnim", false); + _networkVars.set("postTransitAnim", false); + } +} + QStringList Rig::getAnimationRoles() const { if (_animNode) { QStringList list; @@ -208,11 +243,17 @@ void Rig::restoreRoleAnimation(const QString& role) { void Rig::destroyAnimGraph() { _animSkeleton.reset(); _animLoader.reset(); + _networkLoader.reset(); _animNode.reset(); _internalPoseSet._relativePoses.clear(); _internalPoseSet._absolutePoses.clear(); _internalPoseSet._overridePoses.clear(); _internalPoseSet._overrideFlags.clear(); + _networkNode.reset(); + _networkPoseSet._relativePoses.clear(); + _networkPoseSet._absolutePoses.clear(); + _networkPoseSet._overridePoses.clear(); + _networkPoseSet._overrideFlags.clear(); _numOverrides = 0; _leftEyeJointChildren.clear(); _rightEyeJointChildren.clear(); @@ -229,14 +270,24 @@ void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOff _internalPoseSet._relativePoses.clear(); _internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); + _networkPoseSet._relativePoses.clear(); + _networkPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); + buildAbsoluteRigPoses(_networkPoseSet._relativePoses, _networkPoseSet._absolutePoses); _internalPoseSet._overridePoses.clear(); _internalPoseSet._overridePoses = _animSkeleton->getRelativeDefaultPoses(); _internalPoseSet._overrideFlags.clear(); _internalPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false); + + _networkPoseSet._overridePoses.clear(); + _networkPoseSet._overridePoses = _animSkeleton->getRelativeDefaultPoses(); + + _networkPoseSet._overrideFlags.clear(); + _networkPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false); + _numOverrides = 0; buildAbsoluteRigPoses(_animSkeleton->getRelativeDefaultPoses(), _absoluteDefaultPoses); @@ -270,6 +321,18 @@ void Rig::reset(const FBXGeometry& geometry) { _internalPoseSet._overrideFlags.clear(); _internalPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false); + + _networkPoseSet._relativePoses.clear(); + _networkPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); + + buildAbsoluteRigPoses(_networkPoseSet._relativePoses, _networkPoseSet._absolutePoses); + + _networkPoseSet._overridePoses.clear(); + _networkPoseSet._overridePoses = _animSkeleton->getRelativeDefaultPoses(); + + _networkPoseSet._overrideFlags.clear(); + _networkPoseSet._overrideFlags.resize(_animSkeleton->getNumJoints(), false); + _numOverrides = 0; buildAbsoluteRigPoses(_animSkeleton->getRelativeDefaultPoses(), _absoluteDefaultPoses); @@ -1049,26 +1112,56 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons updateAnimationStateHandlers(); _animVars.setRigToGeometryTransform(_rigToGeometryTransform); - + if (_networkNode) { + _networkVars.setRigToGeometryTransform(_rigToGeometryTransform); + } AnimContext context(_enableDebugDrawIKTargets, _enableDebugDrawIKConstraints, _enableDebugDrawIKChains, getGeometryToRigTransform(), rigToWorldTransform); // evaluate the animation AnimVariantMap triggersOut; - + AnimVariantMap networkTriggersOut; _internalPoseSet._relativePoses = _animNode->evaluate(_animVars, context, deltaTime, triggersOut); + if (_networkNode) { + _networkPoseSet._relativePoses = _networkNode->evaluate(_networkVars, context, deltaTime, networkTriggersOut); + const float NETWORK_ANIMATION_BLEND_FRAMES = 6.0f; + float alpha = 1.0f; + std::shared_ptr clip; + if (_networkAnimState.clipNodeEnum == NetworkAnimState::PreTransit) { + clip = std::dynamic_pointer_cast(_networkNode->findByName("preTransitAnim")); + if (clip) { + alpha = (clip->getFrame() - clip->getStartFrame()) / NETWORK_ANIMATION_BLEND_FRAMES; + } + } else if (_networkAnimState.clipNodeEnum == NetworkAnimState::PostTransit) { + clip = std::dynamic_pointer_cast(_networkNode->findByName("postTransitAnim")); + if (clip) { + alpha = (clip->getEndFrame() - clip->getFrame()) / NETWORK_ANIMATION_BLEND_FRAMES; + } + } + if (_sendNetworkNode) { + for (size_t i = 0; i < _networkPoseSet._relativePoses.size(); i++) { + _networkPoseSet._relativePoses[i].blend(_internalPoseSet._relativePoses[i], (alpha > 1.0f ? 1.0f : alpha)); + } + } + } if ((int)_internalPoseSet._relativePoses.size() != _animSkeleton->getNumJoints()) { // animations haven't fully loaded yet. _internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); } + if ((int)_networkPoseSet._relativePoses.size() != _animSkeleton->getNumJoints()) { + // animations haven't fully loaded yet. + _networkPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); + } _lastAnimVars = _animVars; _animVars.clearTriggers(); _animVars = triggersOut; + _networkVars.clearTriggers(); + _networkVars = networkTriggersOut; _lastContext = context; } applyOverridePoses(); buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); - + buildAbsoluteRigPoses(_networkPoseSet._relativePoses, _networkPoseSet._absolutePoses); // copy internal poses to external poses { QWriteLocker writeLock(&_externalPoseSetLock); @@ -1672,11 +1765,14 @@ void Rig::initAnimGraph(const QUrl& url) { _animGraphURL = url; _animNode.reset(); + _networkNode.reset(); // load the anim graph _animLoader.reset(new AnimNodeLoader(url)); + auto networkUrl = PathUtils::resourcesUrl("avatar/network-animation.json"); + _networkLoader.reset(new AnimNodeLoader(networkUrl)); std::weak_ptr weakSkeletonPtr = _animSkeleton; - connect(_animLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr](AnimNode::Pointer nodeIn) { + connect(_animLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr, url](AnimNode::Pointer nodeIn) { _animNode = nodeIn; // abort load if the previous skeleton was deleted. @@ -1703,7 +1799,33 @@ void Rig::initAnimGraph(const QUrl& url) { emit onLoadComplete(); }); connect(_animLoader.get(), &AnimNodeLoader::error, [url](int error, QString str) { - qCCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str; + qCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str; + }); + + connect(_networkLoader.get(), &AnimNodeLoader::success, [this, weakSkeletonPtr, networkUrl](AnimNode::Pointer nodeIn) { + _networkNode = nodeIn; + // abort load if the previous skeleton was deleted. + auto sharedSkeletonPtr = weakSkeletonPtr.lock(); + if (!sharedSkeletonPtr) { + return; + } + _networkNode->setSkeleton(sharedSkeletonPtr); + if (_networkAnimState.clipNodeEnum != NetworkAnimState::Idle) { + // restore the user animation we had before reset. + NetworkAnimState origState = _networkAnimState; + _networkAnimState = { NetworkAnimState::Idle, "", 30.0f, false, 0.0f, 0.0f }; + if (_networkAnimState.clipNodeEnum == NetworkAnimState::PreTransit) { + triggerNetworkAnimation("preTransitAnim"); + } else if (_networkAnimState.clipNodeEnum == NetworkAnimState::Transit) { + triggerNetworkAnimation("transitAnim"); + } else if (_networkAnimState.clipNodeEnum == NetworkAnimState::PostTransit) { + triggerNetworkAnimation("postTransitAnim"); + } + } + + }); + connect(_networkLoader.get(), &AnimNodeLoader::error, [networkUrl](int error, QString str) { + qCritical(animation) << "Error loading" << networkUrl.toDisplayString() << "code = " << error << "str =" << str; }); } } @@ -1782,13 +1904,13 @@ void Rig::copyJointsIntoJointData(QVector& jointDataVec) const { if (isIndexValid(i)) { // rotations are in absolute rig frame. glm::quat defaultAbsRot = geometryToRigPose.rot() * _animSkeleton->getAbsoluteDefaultPose(i).rot(); - data.rotation = _internalPoseSet._absolutePoses[i].rot(); + data.rotation = !_sendNetworkNode ? _internalPoseSet._absolutePoses[i].rot() : _networkPoseSet._absolutePoses[i].rot(); data.rotationIsDefaultPose = isEqual(data.rotation, defaultAbsRot); // translations are in relative frame but scaled so that they are in meters, // instead of geometry units. glm::vec3 defaultRelTrans = _geometryOffset.scale() * _animSkeleton->getRelativeDefaultPose(i).trans(); - data.translation = _geometryOffset.scale() * _internalPoseSet._relativePoses[i].trans(); + data.translation = _geometryOffset.scale() * (!_sendNetworkNode ? _internalPoseSet._relativePoses[i].trans() : _networkPoseSet._relativePoses[i].trans()); data.translationIsDefaultPose = isEqual(data.translation, defaultRelTrans); } else { data.translationIsDefaultPose = true; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index ed0b70d4b6..7efc58fb4d 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -113,7 +113,10 @@ public: void destroyAnimGraph(); void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); + void triggerNetworkAnimation(const QString& animName); void restoreAnimation(); + void restoreNetworkAnimation(); + QStringList getAnimationRoles() const; void overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); void restoreRoleAnimation(const QString& role); @@ -269,6 +272,7 @@ protected: // Only accessed by the main thread PoseSet _internalPoseSet; + PoseSet _networkPoseSet; // Copy of the _poseSet for external threads. PoseSet _externalPoseSet; @@ -300,9 +304,12 @@ protected: QUrl _animGraphURL; std::shared_ptr _animNode; + std::shared_ptr _networkNode; std::shared_ptr _animSkeleton; std::unique_ptr _animLoader; + std::unique_ptr _networkLoader; AnimVariantMap _animVars; + AnimVariantMap _networkVars; enum class RigRole { Idle = 0, @@ -315,6 +322,25 @@ protected: RigRole _state { RigRole::Idle }; RigRole _desiredState { RigRole::Idle }; float _desiredStateAge { 0.0f }; + + struct NetworkAnimState { + enum ClipNodeEnum { + Idle = 0, + PreTransit, + Transit, + PostTransit + }; + NetworkAnimState() : clipNodeEnum(NetworkAnimState::Idle) {} + NetworkAnimState(ClipNodeEnum clipNodeEnumIn, const QString& urlIn, float fpsIn, bool loopIn, float firstFrameIn, float lastFrameIn) : + clipNodeEnum(clipNodeEnumIn), url(urlIn), fps(fpsIn), loop(loopIn), firstFrame(firstFrameIn), lastFrame(lastFrameIn) {} + + ClipNodeEnum clipNodeEnum; + QString url; + float fps; + bool loop; + float firstFrame; + float lastFrame; + }; struct UserAnimState { enum ClipNodeEnum { @@ -349,6 +375,7 @@ protected: }; UserAnimState _userAnimState; + NetworkAnimState _networkAnimState; std::map _roleAnimStates; float _leftHandOverlayAlpha { 0.0f }; @@ -382,6 +409,7 @@ protected: int _rigId; bool _headEnabled { false }; + bool _sendNetworkNode { false }; AnimContext _lastContext; AnimVariantMap _lastAnimVars; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 6770cd7f96..4a06a5252a 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -114,27 +114,29 @@ void Avatar::setShowNamesAboveHeads(bool show) { } AvatarTransit::Status AvatarTransit::update(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config) { - glm::vec3 currentPosition = _isTransiting ? _currentPosition : avatarPosition; - float oneFrameDistance = glm::length(currentPosition - _lastPosition); - const float MAX_TRANSIT_DISTANCE = 30.0f; - float scaledMaxTransitDistance = MAX_TRANSIT_DISTANCE * _scale; - if (oneFrameDistance > config._triggerDistance && !_isTransiting) { - if (oneFrameDistance < scaledMaxTransitDistance) { - start(deltaTime, _lastPosition, currentPosition, config); + float oneFrameDistance = _isActive ? glm::length(avatarPosition - _endPosition) : glm::length(avatarPosition - _lastPosition); + if (oneFrameDistance > (config._minTriggerDistance * _scale)) { + if (oneFrameDistance < (config._maxTriggerDistance * _scale)) { + start(deltaTime, _lastPosition, avatarPosition, config); } else { - _lastPosition = currentPosition; - return Status::ABORT_TRANSIT; + _lastPosition = avatarPosition; + _status = Status::ABORT_TRANSIT; } } - _lastPosition = currentPosition; + _lastPosition = avatarPosition; _status = updatePosition(deltaTime); + + if (_isActive && oneFrameDistance > (config._abortDistance * _scale) && _status == Status::POST_TRANSIT) { + reset(); + _status = Status::ENDED; + } return _status; } void AvatarTransit::reset() { _lastPosition = _endPosition; _currentPosition = _endPosition; - _isTransiting = false; + _isActive = false; } void AvatarTransit::start(float deltaTime, const glm::vec3& startPosition, const glm::vec3& endPosition, const AvatarTransit::TransitConfig& config) { _startPosition = startPosition; @@ -143,12 +145,14 @@ void AvatarTransit::start(float deltaTime, const glm::vec3& startPosition, const _transitLine = endPosition - startPosition; _totalDistance = glm::length(_transitLine); _easeType = config._easeType; - const float REFERENCE_FRAMES_PER_SECOND = 30.0f; - + + _preTransitTime = AVATAR_PRE_TRANSIT_FRAME_COUNT / AVATAR_TRANSIT_FRAMES_PER_SECOND; + _postTransitTime = AVATAR_POST_TRANSIT_FRAME_COUNT / AVATAR_TRANSIT_FRAMES_PER_SECOND; int transitFrames = (!config._isDistanceBased) ? config._totalFrames : config._framesPerMeter * _totalDistance; - _totalTime = (float)transitFrames / REFERENCE_FRAMES_PER_SECOND; - _currentTime = 0.0f; - _isTransiting = true; + _transitTime = (float)transitFrames / AVATAR_TRANSIT_FRAMES_PER_SECOND; + _totalTime = _transitTime + _preTransitTime + _postTransitTime; + _currentTime = _isActive ? _preTransitTime : 0.0f; + _isActive = true; } float AvatarTransit::getEaseValue(AvatarTransit::EaseType type, float value) { @@ -171,31 +175,37 @@ float AvatarTransit::getEaseValue(AvatarTransit::EaseType type, float value) { AvatarTransit::Status AvatarTransit::updatePosition(float deltaTime) { Status status = Status::IDLE; - if (_isTransiting) { + if (_isActive) { float nextTime = _currentTime + deltaTime; - if (nextTime >= _totalTime) { - _currentPosition = _endPosition; - _isTransiting = false; - status = Status::END_TRANSIT; - } else { + if (nextTime < _preTransitTime) { + _currentPosition = _startPosition; + status = Status::PRE_TRANSIT; if (_currentTime == 0) { + status = Status::STARTED; + } + } else if (nextTime < _totalTime - _postTransitTime){ + status = Status::TRANSITING; + if (_currentTime <= _preTransitTime) { status = Status::START_TRANSIT; } else { - status = Status::TRANSITING; + float percentageIntoTransit = (nextTime - _preTransitTime) / _transitTime; + _currentPosition = _startPosition + getEaseValue(_easeType, percentageIntoTransit) * _transitLine; + } + } else { + status = Status::POST_TRANSIT; + _currentPosition = _endPosition; + if (nextTime >= _totalTime) { + _isActive = false; + status = Status::ENDED; + } else if (_currentTime < _totalTime - _postTransitTime) { + status = Status::END_TRANSIT; } - float percentageIntoTransit = nextTime / _totalTime; - _currentPosition = _startPosition + getEaseValue(_easeType, percentageIntoTransit) * _transitLine; } _currentTime = nextTime; } return status; } -bool AvatarTransit::getNextPosition(glm::vec3& nextPosition) { - nextPosition = _currentPosition; - return _isTransiting; -} - Avatar::Avatar(QThread* thread) : _voiceSphereID(GeometryCache::UNKNOWN_ID) { @@ -536,16 +546,10 @@ void Avatar::relayJointDataToChildren() { void Avatar::simulate(float deltaTime, bool inView) { PROFILE_RANGE(simulation, "simulate"); - - if (_transit.isTransiting()) { - glm::vec3 nextPosition; - if (_transit.getNextPosition(nextPosition)) { - _globalPosition = nextPosition; - _globalPositionChanged = usecTimestampNow(); - if (!hasParent()) { - setLocalPosition(nextPosition); - } - } + + _globalPosition = _transit.isActive() ? _transit.getCurrentPosition() : _serverPosition; + if (!hasParent()) { + setLocalPosition(_globalPosition); } _simulationRate.increment(); @@ -558,7 +562,7 @@ void Avatar::simulate(float deltaTime, bool inView) { PROFILE_RANGE(simulation, "updateJoints"); if (inView) { Head* head = getHead(); - if (_hasNewJointData || _transit.isTransiting()) { + if (_hasNewJointData || _transit.isActive()) { _skeletonModel->getRig().copyJointsFromJointData(_jointData); glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset()); _skeletonModel->getRig().computeExternalPoses(rootTransform); @@ -1731,6 +1735,7 @@ void Avatar::getCapsule(glm::vec3& start, glm::vec3& end, float& radius) { glm::vec3 Avatar::getWorldFeetPosition() { ShapeInfo shapeInfo; + computeShapeInfo(shapeInfo); glm::vec3 halfExtents = shapeInfo.getHalfExtents(); // x = radius, y = halfHeight glm::vec3 localFeet(0.0f, shapeInfo.getOffset().y - halfExtents.y - halfExtents.x, 0.0f); @@ -1994,22 +1999,12 @@ float Avatar::getUnscaledEyeHeightFromSkeleton() const { } } -AvatarTransit::Status Avatar::updateTransit(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config) { +AvatarTransit::Status Avatar::updateTransit(float deltaTime, const glm::vec3& avatarPosition, float avatarScale, const AvatarTransit::TransitConfig& config) { std::lock_guard lock(_transitLock); + _transit.setScale(avatarScale); return _transit.update(deltaTime, avatarPosition, config); } -void Avatar::setTransitScale(float scale) { - std::lock_guard lock(_transitLock); - return _transit.setScale(scale); -} - -void Avatar::overrideNextPackagePositionData(const glm::vec3& position) { - std::lock_guard lock(_transitLock); - _overrideGlobalPosition = true; - _globalPositionOverride = position; -} - void Avatar::addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) { std::lock_guard lock(_materialsLock); _materials[parentMaterialName].push(material); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 3f4cf7b8c2..ca063a7026 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -56,9 +56,13 @@ class AvatarTransit { public: enum Status { IDLE = 0, + STARTED, + PRE_TRANSIT, START_TRANSIT, TRANSITING, END_TRANSIT, + POST_TRANSIT, + ENDED, ABORT_TRANSIT }; @@ -72,20 +76,20 @@ public: struct TransitConfig { TransitConfig() {}; int _totalFrames { 0 }; - int _framesPerMeter { 0 }; + float _framesPerMeter { 0.0f }; bool _isDistanceBased { false }; - float _triggerDistance { 0 }; + float _minTriggerDistance { 0.0f }; + float _maxTriggerDistance { 0.0f }; + float _abortDistance{ 0.0f }; EaseType _easeType { EaseType::EASE_OUT }; }; AvatarTransit() {}; Status update(float deltaTime, const glm::vec3& avatarPosition, const TransitConfig& config); Status getStatus() { return _status; } - bool isTransiting() { return _isTransiting; } + bool isActive() { return _isActive; } glm::vec3 getCurrentPosition() { return _currentPosition; } - bool getNextPosition(glm::vec3& nextPosition); glm::vec3 getEndPosition() { return _endPosition; } - float getTransitTime() { return _totalTime; } void setScale(float scale) { _scale = scale; } void reset(); @@ -93,7 +97,7 @@ private: Status updatePosition(float deltaTime); void start(float deltaTime, const glm::vec3& startPosition, const glm::vec3& endPosition, const TransitConfig& config); float getEaseValue(AvatarTransit::EaseType type, float value); - bool _isTransiting { false }; + bool _isActive { false }; glm::vec3 _startPosition; glm::vec3 _endPosition; @@ -103,7 +107,10 @@ private: glm::vec3 _transitLine; float _totalDistance { 0.0f }; + float _preTransitTime { 0.0f }; float _totalTime { 0.0f }; + float _transitTime { 0.0f }; + float _postTransitTime { 0.0f }; float _currentTime { 0.0f }; EaseType _easeType { EaseType::EASE_OUT }; Status _status { Status::IDLE }; @@ -431,11 +438,7 @@ public: virtual scriptable::ScriptableModelBase getScriptableModel() override; std::shared_ptr getTransit() { return std::make_shared(_transit); }; - - AvatarTransit::Status updateTransit(float deltaTime, const glm::vec3& avatarPosition, const AvatarTransit::TransitConfig& config); - void setTransitScale(float scale); - - void overrideNextPackagePositionData(const glm::vec3& position); + AvatarTransit::Status updateTransit(float deltaTime, const glm::vec3& avatarPosition, float avatarScale, const AvatarTransit::TransitConfig& config); signals: void targetScaleChanged(float targetScale); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index e1d2c58e81..e08f7fb6f8 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -44,6 +44,7 @@ #include "AvatarLogging.h" #include "AvatarTraits.h" #include "ClientTraitsHandler.h" +#include "ResourceRequestObserver.h" //#define WANT_DEBUG @@ -938,20 +939,22 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { offset = glm::vec3(row * SPACE_BETWEEN_AVATARS, 0.0f, col * SPACE_BETWEEN_AVATARS); } - auto newValue = glm::vec3(data->globalPosition[0], data->globalPosition[1], data->globalPosition[2]) + offset; - if (_globalPosition != newValue) { - _globalPosition = newValue; + _serverPosition = glm::vec3(data->globalPosition[0], data->globalPosition[1], data->globalPosition[2]) + offset; + auto oneStepDistance = glm::length(_globalPosition - _serverPosition); + if (oneStepDistance <= AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE || oneStepDistance >= AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE) { + _globalPosition = _serverPosition; + // if we don't have a parent, make sure to also set our local position + if (!hasParent()) { + setLocalPosition(_serverPosition); + } + } + if (_globalPosition != _serverPosition) { _globalPositionChanged = now; } sourceBuffer += sizeof(AvatarDataPacket::AvatarGlobalPosition); int numBytesRead = sourceBuffer - startSection; _globalPositionRate.increment(numBytesRead); _globalPositionUpdateRate.increment(); - - // if we don't have a parent, make sure to also set our local position - if (!hasParent()) { - setLocalPosition(newValue); - } } if (hasAvatarBoundingBox) { @@ -2167,10 +2170,6 @@ void AvatarData::sendAvatarDataPacket(bool sendAll) { } } - if (_overrideGlobalPosition) { - _overrideGlobalPosition = false; - } - doneEncoding(cullSmallData); static AvatarDataSequenceNumber sequenceNumber = 0; @@ -2214,11 +2213,21 @@ void AvatarData::updateJointMappings() { } if (_skeletonModelURL.fileName().toLower().endsWith(".fst")) { + //// + // TODO: Should we rely upon HTTPResourceRequest for ResourceRequestObserver instead? + // HTTPResourceRequest::doSend() covers all of the following and + // then some. It doesn't cover the connect() call, so we may + // want to add a HTTPResourceRequest::doSend() method that does + // connects. QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest networkRequest = QNetworkRequest(_skeletonModelURL); networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + DependencyManager::get()->update( + _skeletonModelURL, -1, "AvatarData::updateJointMappings"); QNetworkReply* networkReply = networkAccessManager.get(networkRequest); + // + //// connect(networkReply, &QNetworkReply::finished, this, &AvatarData::setJointMappingsFromNetworkReply); } } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 1f5b7093b9..8533f142bb 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -338,6 +338,17 @@ const float AVATAR_DISTANCE_LEVEL_5 = 200.0f; // meters // This is the start location in the Sandbox (xyz: 6270, 211, 6000). const glm::vec3 START_LOCATION(6270, 211, 6000); +// Avatar Transit Constants +const float AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE = 1.0f; +const float AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE = 30.0f; +const int AVATAR_TRANSIT_FRAME_COUNT = 11; +const float AVATAR_TRANSIT_FRAMES_PER_METER = 0.5f; +const float AVATAR_TRANSIT_ABORT_DISTANCE = 0.1f; +const bool AVATAR_TRANSIT_DISTANCE_BASED = true; +const float AVATAR_TRANSIT_FRAMES_PER_SECOND = 30.0f; +const float AVATAR_PRE_TRANSIT_FRAME_COUNT = 10.0f; +const float AVATAR_POST_TRANSIT_FRAME_COUNT = 27.0f; + enum KeyState { NO_KEY_DOWN = 0, INSERT_KEY_DOWN, @@ -1393,8 +1404,7 @@ protected: // where Entities are located. This is currently only used by the mixer to decide how often to send // updates about one avatar to another. glm::vec3 _globalPosition { 0, 0, 0 }; - glm::vec3 _globalPositionOverride { 0, 0, 0 }; - bool _overrideGlobalPosition { false }; + glm::vec3 _serverPosition { 0, 0, 0 }; quint64 _globalPositionChanged { 0 }; quint64 _avatarBoundingBoxChanged { 0 }; diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index a06b53da7c..f8247d9e52 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -31,7 +31,27 @@ ClientTraitsHandler::ClientTraitsHandler(AvatarData* owningAvatar) : nodeList->getPacketReceiver().registerListener(PacketType::SetAvatarTraits, this, "processTraitOverride"); } +void ClientTraitsHandler::markTraitUpdated(AvatarTraits::TraitType updatedTrait) { + Lock lock(_traitLock); + _traitStatuses[updatedTrait] = Updated; + _hasChangedTraits = true; +} + +void ClientTraitsHandler::markInstancedTraitUpdated(AvatarTraits::TraitType traitType, QUuid updatedInstanceID) { + Lock lock(_traitLock); + _traitStatuses.instanceInsert(traitType, updatedInstanceID, Updated); + _hasChangedTraits = true; +} + +void ClientTraitsHandler::markInstancedTraitDeleted(AvatarTraits::TraitType traitType, QUuid deleteInstanceID) { + Lock lock(_traitLock); + _traitStatuses.instanceInsert(traitType, deleteInstanceID, Deleted); + _hasChangedTraits = true; +} + void ClientTraitsHandler::resetForNewMixer() { + Lock lock(_traitLock); + // re-set the current version to 0 _currentTraitVersion = AvatarTraits::DEFAULT_TRAIT_VERSION; @@ -46,6 +66,8 @@ void ClientTraitsHandler::resetForNewMixer() { } void ClientTraitsHandler::sendChangedTraitsToMixer() { + Lock lock(_traitLock); + if (hasChangedTraits() || _shouldPerformInitialSend) { // we have at least one changed trait to send @@ -113,6 +135,7 @@ void ClientTraitsHandler::sendChangedTraitsToMixer() { void ClientTraitsHandler::processTraitOverride(QSharedPointer message, SharedNodePointer sendingNode) { if (sendingNode->getType() == NodeType::AvatarMixer) { + Lock lock(_traitLock); while (message->getBytesLeftToRead()) { AvatarTraits::TraitType traitType; message->readPrimitive(&traitType); diff --git a/libraries/avatars/src/ClientTraitsHandler.h b/libraries/avatars/src/ClientTraitsHandler.h index 27ba58d46b..3900268101 100644 --- a/libraries/avatars/src/ClientTraitsHandler.h +++ b/libraries/avatars/src/ClientTraitsHandler.h @@ -26,14 +26,11 @@ public: void sendChangedTraitsToMixer(); - bool hasChangedTraits() { return _hasChangedTraits; } + bool hasChangedTraits() const { return _hasChangedTraits; } - void markTraitUpdated(AvatarTraits::TraitType updatedTrait) - { _traitStatuses[updatedTrait] = Updated; _hasChangedTraits = true; } - void markInstancedTraitUpdated(AvatarTraits::TraitType traitType, QUuid updatedInstanceID) - { _traitStatuses.instanceInsert(traitType, updatedInstanceID, Updated); _hasChangedTraits = true; } - void markInstancedTraitDeleted(AvatarTraits::TraitType traitType, QUuid deleteInstanceID) - { _traitStatuses.instanceInsert(traitType, deleteInstanceID, Deleted); _hasChangedTraits = true; } + void markTraitUpdated(AvatarTraits::TraitType updatedTrait); + void markInstancedTraitUpdated(AvatarTraits::TraitType traitType, QUuid updatedInstanceID); + void markInstancedTraitDeleted(AvatarTraits::TraitType traitType, QUuid deleteInstanceID); void resetForNewMixer(); @@ -41,17 +38,21 @@ public slots: void processTraitOverride(QSharedPointer message, SharedNodePointer sendingNode); private: + using Mutex = std::recursive_mutex; + using Lock = std::lock_guard; + enum ClientTraitStatus { Unchanged, Updated, Deleted }; - AvatarData* _owningAvatar; + AvatarData* const _owningAvatar; + Mutex _traitLock; AvatarTraits::AssociatedTraitValues _traitStatuses; - AvatarTraits::TraitVersion _currentTraitVersion { AvatarTraits::DEFAULT_TRAIT_VERSION }; + AvatarTraits::TraitVersion _currentTraitVersion { AvatarTraits::DEFAULT_TRAIT_VERSION }; AvatarTraits::TraitVersion _currentSkeletonVersion { AvatarTraits::NULL_TRAIT_VERSION }; bool _shouldPerformInitialSend { false }; diff --git a/libraries/entities/src/EntityEditFilters.cpp b/libraries/entities/src/EntityEditFilters.cpp index 6f7e012bc4..4865c0ba1e 100644 --- a/libraries/entities/src/EntityEditFilters.cpp +++ b/libraries/entities/src/EntityEditFilters.cpp @@ -200,7 +200,8 @@ void EntityEditFilters::addFilter(EntityItemID entityID, QString filterURL) { _filterDataMap.insert(entityID, filterData); _lock.unlock(); - auto scriptRequest = DependencyManager::get()->createResourceRequest(this, scriptURL); + auto scriptRequest = DependencyManager::get()->createResourceRequest( + this, scriptURL, true, -1, "EntityEditFilters::addFilter"); if (!scriptRequest) { qWarning() << "Could not create ResourceRequest for Entity Edit filter script at" << scriptURL.toString(); scriptRequestFinished(entityID); diff --git a/libraries/fbx/src/GLTFReader.cpp b/libraries/fbx/src/GLTFReader.cpp index 317342b886..b93dc3541b 100644 --- a/libraries/fbx/src/GLTFReader.cpp +++ b/libraries/fbx/src/GLTFReader.cpp @@ -953,7 +953,8 @@ bool GLTFReader::doesResourceExist(const QString& url) { } std::tuple GLTFReader::requestData(QUrl& url) { - auto request = DependencyManager::get()->createResourceRequest(nullptr, url); + auto request = DependencyManager::get()->createResourceRequest( + nullptr, url, true, -1, "GLTFReader::requestData"); if (!request) { return std::make_tuple(false, QByteArray()); diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index c46a1e234c..7a2cbff497 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -443,7 +443,8 @@ void OBJReader::parseTextureLine(const QByteArray& textureLine, QByteArray& file } std::tuple requestData(QUrl& url) { - auto request = DependencyManager::get()->createResourceRequest(nullptr, url); + auto request = DependencyManager::get()->createResourceRequest( + nullptr, url, true, -1, "(OBJReader) requestData"); if (!request) { return std::make_tuple(false, QByteArray()); diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index e8aec5e60e..740af44591 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -456,7 +456,8 @@ void NetworkTexture::makeRequest() { // Add a fragment to the base url so we can identify the section of the ktx being requested when debugging // The actual requested url is _activeUrl and will not contain the fragment _url.setFragment("head"); - _ktxHeaderRequest = DependencyManager::get()->createResourceRequest(this, _activeUrl); + _ktxHeaderRequest = DependencyManager::get()->createResourceRequest( + this, _activeUrl, true, -1, "NetworkTexture::makeRequest"); if (!_ktxHeaderRequest) { qCDebug(networking).noquote() << "Failed to get request for" << _url.toDisplayString(); @@ -617,7 +618,8 @@ void NetworkTexture::startMipRangeRequest(uint16_t low, uint16_t high) { bool isHighMipRequest = low == NULL_MIP_LEVEL && high == NULL_MIP_LEVEL; - _ktxMipRequest = DependencyManager::get()->createResourceRequest(this, _activeUrl); + _ktxMipRequest = DependencyManager::get()->createResourceRequest( + this, _activeUrl, true, -1, "NetworkTexture::startMipRangeRequest"); if (!_ktxMipRequest) { qCWarning(networking).noquote() << "Failed to get request for" << _url.toDisplayString(); diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index 6d5bbb3ac5..6f5b13f98d 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -24,8 +24,12 @@ static const int DOWNLOAD_PROGRESS_LOG_INTERVAL_SECONDS = 5; -AssetResourceRequest::AssetResourceRequest(const QUrl& url) : - ResourceRequest(url) +AssetResourceRequest::AssetResourceRequest( + const QUrl& url, + const bool isObservable, + const qint64 callerId, + const QString& extra) : + ResourceRequest(url, isObservable, callerId, extra) { _lastProgressDebug = p_high_resolution_clock::now() - std::chrono::seconds(DOWNLOAD_PROGRESS_LOG_INTERVAL_SECONDS); } diff --git a/libraries/networking/src/AssetResourceRequest.h b/libraries/networking/src/AssetResourceRequest.h index 2fe79040ca..07baca5416 100644 --- a/libraries/networking/src/AssetResourceRequest.h +++ b/libraries/networking/src/AssetResourceRequest.h @@ -22,7 +22,11 @@ class AssetResourceRequest : public ResourceRequest { Q_OBJECT public: - AssetResourceRequest(const QUrl& url); + AssetResourceRequest( + const QUrl& url, + const bool isObservable = true, + const qint64 callerId = -1, + const QString& extra = ""); virtual ~AssetResourceRequest() override; protected: diff --git a/libraries/networking/src/AtpReply.cpp b/libraries/networking/src/AtpReply.cpp index b2b7e8bee7..3ec9b23f5f 100644 --- a/libraries/networking/src/AtpReply.cpp +++ b/libraries/networking/src/AtpReply.cpp @@ -14,7 +14,8 @@ #include "ResourceManager.h" AtpReply::AtpReply(const QUrl& url, QObject* parent) : - _resourceRequest(DependencyManager::get()->createResourceRequest(parent, url)) { + _resourceRequest(DependencyManager::get()->createResourceRequest( + parent, url, true, -1, "AtpReply::AtpReply")) { setOperation(QNetworkAccessManager::GetOperation); connect(_resourceRequest, &AssetResourceRequest::progress, this, &AtpReply::downloadProgress); diff --git a/libraries/networking/src/FileResourceRequest.h b/libraries/networking/src/FileResourceRequest.h index 547b754cb5..fa9a1617a9 100644 --- a/libraries/networking/src/FileResourceRequest.h +++ b/libraries/networking/src/FileResourceRequest.h @@ -19,7 +19,12 @@ class FileResourceRequest : public ResourceRequest { Q_OBJECT public: - FileResourceRequest(const QUrl& url) : ResourceRequest(url) { } + FileResourceRequest( + const QUrl& url, + const bool isObservable = true, + const qint64 callerId = -1, + const QString& extra = "" + ) : ResourceRequest(url, isObservable, callerId, extra) { } protected: virtual void doSend() override; diff --git a/libraries/networking/src/HTTPResourceRequest.h b/libraries/networking/src/HTTPResourceRequest.h index cc628d8855..c725934f2f 100644 --- a/libraries/networking/src/HTTPResourceRequest.h +++ b/libraries/networking/src/HTTPResourceRequest.h @@ -21,7 +21,12 @@ class HTTPResourceRequest : public ResourceRequest { Q_OBJECT public: - HTTPResourceRequest(const QUrl& url) : ResourceRequest(url) { } + HTTPResourceRequest( + const QUrl& url, + const bool isObservable = true, + const qint64 callerId = -1, + const QString& = "" + ) : ResourceRequest(url, isObservable, callerId) { } ~HTTPResourceRequest(); protected: diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 076db44ea6..7aad8d468a 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -10,6 +10,7 @@ // #include "ResourceCache.h" +#include "ResourceRequestObserver.h" #include #include @@ -252,7 +253,9 @@ ResourceCache::ResourceCache(QObject* parent) : QObject(parent) { } } -ResourceCache::~ResourceCache() {} +ResourceCache::~ResourceCache() { + clearUnusedResources(); +} void ResourceCache::clearATPAssets() { { @@ -338,28 +341,31 @@ QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl& } if (resource) { removeUnusedResource(resource); - return resource; } - if (!url.isValid() && !url.isEmpty() && fallback.isValid()) { - return getResource(fallback, QUrl()); + if (!resource && !url.isValid() && !url.isEmpty() && fallback.isValid()) { + resource = getResource(fallback, QUrl()); } - resource = createResource( - url, - fallback.isValid() ? getResource(fallback, QUrl()) : QSharedPointer(), - extra); - resource->setSelf(resource); - resource->setCache(this); - resource->moveToThread(qApp->thread()); - connect(resource.data(), &Resource::updateSize, this, &ResourceCache::updateTotalSize); - { - QWriteLocker locker(&_resourcesLock); - _resources.insert(url, resource); + if (!resource) { + resource = createResource( + url, + fallback.isValid() ? getResource(fallback, QUrl()) : QSharedPointer(), + extra); + resource->setSelf(resource); + resource->setCache(this); + resource->moveToThread(qApp->thread()); + connect(resource.data(), &Resource::updateSize, this, &ResourceCache::updateTotalSize); + { + QWriteLocker locker(&_resourcesLock); + _resources.insert(url, resource); + } + removeUnusedResource(resource); + resource->ensureLoading(); } - removeUnusedResource(resource); - resource->ensureLoading(); + DependencyManager::get()->update( + resource->getURL(), -1, "ResourceCache::getResource"); return resource; } @@ -682,7 +688,8 @@ void Resource::makeRequest() { PROFILE_ASYNC_BEGIN(resource, "Resource:" + getType(), QString::number(_requestID), { { "url", _url.toString() }, { "activeURL", _activeUrl.toString() } }); - _request = DependencyManager::get()->createResourceRequest(this, _activeUrl); + _request = DependencyManager::get()->createResourceRequest( + this, _activeUrl, true, -1, "Resource::makeRequest"); if (!_request) { qCDebug(networking).noquote() << "Failed to get request for" << _url.toDisplayString(); diff --git a/libraries/networking/src/ResourceManager.cpp b/libraries/networking/src/ResourceManager.cpp index 553f0d0a61..9539a10c2d 100644 --- a/libraries/networking/src/ResourceManager.cpp +++ b/libraries/networking/src/ResourceManager.cpp @@ -112,22 +112,28 @@ void ResourceManager::cleanup() { _thread.wait(); } -ResourceRequest* ResourceManager::createResourceRequest(QObject* parent, const QUrl& url) { +ResourceRequest* ResourceManager::createResourceRequest( + QObject* parent, + const QUrl& url, + const bool isObservable, + const qint64 callerId, + const QString& extra +) { auto normalizedURL = normalizeURL(url); auto scheme = normalizedURL.scheme(); ResourceRequest* request = nullptr; if (scheme == URL_SCHEME_FILE || scheme == URL_SCHEME_QRC) { - request = new FileResourceRequest(normalizedURL); + request = new FileResourceRequest(normalizedURL, isObservable, callerId, extra); } else if (scheme == URL_SCHEME_HTTP || scheme == URL_SCHEME_HTTPS || scheme == URL_SCHEME_FTP) { - request = new HTTPResourceRequest(normalizedURL); + request = new HTTPResourceRequest(normalizedURL, isObservable, callerId, extra); } else if (scheme == URL_SCHEME_ATP) { if (!_atpSupportEnabled) { qCDebug(networking) << "ATP support not enabled, unable to create request for URL: " << url.url(); return nullptr; } - request = new AssetResourceRequest(normalizedURL); + request = new AssetResourceRequest(normalizedURL, isObservable, callerId, extra); } else { qCDebug(networking) << "Unknown scheme (" << scheme << ") for URL: " << url.url(); return nullptr; @@ -163,7 +169,7 @@ bool ResourceManager::resourceExists(const QUrl& url) { return reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200; } else if (scheme == URL_SCHEME_ATP && _atpSupportEnabled) { - auto request = new AssetResourceRequest(url); + auto request = new AssetResourceRequest(url, ResourceRequest::IS_NOT_OBSERVABLE); ByteRange range; range.fromInclusive = 1; range.toExclusive = 1; diff --git a/libraries/networking/src/ResourceManager.h b/libraries/networking/src/ResourceManager.h index a79222d2d8..adaa1cf886 100644 --- a/libraries/networking/src/ResourceManager.h +++ b/libraries/networking/src/ResourceManager.h @@ -34,7 +34,12 @@ public: QString normalizeURL(const QString& urlString); QUrl normalizeURL(const QUrl& url); - ResourceRequest* createResourceRequest(QObject* parent, const QUrl& url); + ResourceRequest* createResourceRequest( + QObject* parent, + const QUrl& url, + const bool isObservable = true, + const qint64 callerId = -1, + const QString& extra = ""); void init(); void cleanup(); diff --git a/libraries/networking/src/ResourceRequest.cpp b/libraries/networking/src/ResourceRequest.cpp index 115e665b77..c63bd4c563 100644 --- a/libraries/networking/src/ResourceRequest.cpp +++ b/libraries/networking/src/ResourceRequest.cpp @@ -10,13 +10,13 @@ // #include "ResourceRequest.h" +#include "ResourceRequestObserver.h" #include #include #include -ResourceRequest::ResourceRequest(const QUrl& url) : _url(url) { } void ResourceRequest::send() { if (QThread::currentThread() != thread()) { @@ -24,6 +24,10 @@ void ResourceRequest::send() { return; } + if (_isObservable) { + DependencyManager::get()->update(_url, _callerId, _extra + " => ResourceRequest::send"); + } + Q_ASSERT(_state == NotStarted); _state = InProgress; diff --git a/libraries/networking/src/ResourceRequest.h b/libraries/networking/src/ResourceRequest.h index 8dd09ccc49..eb306ca5be 100644 --- a/libraries/networking/src/ResourceRequest.h +++ b/libraries/networking/src/ResourceRequest.h @@ -40,7 +40,20 @@ const QString STAT_FILE_RESOURCE_TOTAL_BYTES = "FILEBytesDownloaded"; class ResourceRequest : public QObject { Q_OBJECT public: - ResourceRequest(const QUrl& url); + static const bool IS_OBSERVABLE = true; + static const bool IS_NOT_OBSERVABLE = false; + + ResourceRequest( + const QUrl& url, + const bool isObservable = IS_OBSERVABLE, + const qint64 callerId = -1, + const QString& extra = "" + ) : _url(url), + _isObservable(isObservable), + _callerId(callerId), + _extra(extra) + { } + virtual ~ResourceRequest() = default; enum State { @@ -99,6 +112,9 @@ protected: bool _rangeRequestSuccessful { false }; uint64_t _totalSizeOfResource { 0 }; int64_t _lastRecordedBytesDownloaded { 0 }; + bool _isObservable; + qint64 _callerId; + QString _extra; }; #endif diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 3d387e0956..06db92bcf7 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -734,11 +734,17 @@ QString getMarketplaceID(const QString& urlString) { return QString(); } -bool Octree::readFromURL(const QString& urlString) { +bool Octree::readFromURL( + const QString& urlString, + const bool isObservable, + const qint64 callerId +) { QString trimmedUrl = urlString.trimmed(); QString marketplaceID = getMarketplaceID(trimmedUrl); - auto request = - std::unique_ptr(DependencyManager::get()->createResourceRequest(this, trimmedUrl)); + qDebug() << "!!!!! going to createResourceRequest " << callerId; + auto request = std::unique_ptr( + DependencyManager::get()->createResourceRequest( + this, trimmedUrl, isObservable, callerId, "Octree::readFromURL")); if (!request) { return false; @@ -768,7 +774,11 @@ bool Octree::readFromURL(const QString& urlString) { } -bool Octree::readFromStream(uint64_t streamLength, QDataStream& inputStream, const QString& marketplaceID) { +bool Octree::readFromStream( + uint64_t streamLength, + QDataStream& inputStream, + const QString& marketplaceID +) { // decide if this is binary SVO or JSON-formatted SVO QIODevice *device = inputStream.device(); char firstChar; @@ -809,7 +819,11 @@ QJsonDocument addMarketplaceIDToDocumentEntities(QJsonDocument& doc, const QStri const int READ_JSON_BUFFER_SIZE = 2048; -bool Octree::readJSONFromStream(uint64_t streamLength, QDataStream& inputStream, const QString& marketplaceID /*=""*/) { +bool Octree::readJSONFromStream( + uint64_t streamLength, + QDataStream& inputStream, + const QString& marketplaceID /*=""*/ +) { // if the data is gzipped we may not have a useful bytesAvailable() result, so just keep reading until // we get an eof. Leave streamLength parameter for consistency. diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index a2b2f227cb..44b429582a 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -210,7 +210,7 @@ public: // Octree importers bool readFromFile(const char* filename); - bool readFromURL(const QString& url); // will support file urls as well... + bool readFromURL(const QString& url, const bool isObservable = true, const qint64 callerId = -1); // will support file urls as well... bool readFromStream(uint64_t streamLength, QDataStream& inputStream, const QString& marketplaceID=""); bool readSVOFromStream(uint64_t streamLength, QDataStream& inputStream); bool readJSONFromStream(uint64_t streamLength, QDataStream& inputStream, const QString& marketplaceID=""); diff --git a/libraries/script-engine/src/FileScriptingInterface.cpp b/libraries/script-engine/src/FileScriptingInterface.cpp index 3bf044fd8b..4e07877d57 100644 --- a/libraries/script-engine/src/FileScriptingInterface.cpp +++ b/libraries/script-engine/src/FileScriptingInterface.cpp @@ -138,7 +138,8 @@ QString FileScriptingInterface::convertUrlToPath(QUrl url) { // this function is not in use void FileScriptingInterface::downloadZip(QString path, const QString link) { QUrl url = QUrl(link); - auto request = DependencyManager::get()->createResourceRequest(nullptr, url); + auto request = DependencyManager::get()->createResourceRequest( + nullptr, url, true, -1, "FileScriptingInterface::downloadZip"); connect(request, &ResourceRequest::finished, this, [this, path]{ unzipFile(path, ""); // so intellisense isn't mad }); diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index dba2db0458..8acf88a7ce 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -109,7 +109,8 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "about to call: ResourceManager::createResourceRequest(this, url); on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; #endif - auto request = DependencyManager::get()->createResourceRequest(nullptr, url); + auto request = DependencyManager::get()->createResourceRequest( + nullptr, url, true, -1, "ScriptCache::getScriptContents"); Q_ASSERT(request); request->setCacheEnabled(!forceDownload); connect(request, &ResourceRequest::finished, this, [=]{ scriptContentAvailable(maxRetries); }); @@ -166,7 +167,8 @@ void ScriptCache::scriptContentAvailable(int maxRetries) { qCDebug(scriptengine) << QString("Retrying script request [%1 / %2]: %3") .arg(attempt).arg(maxRetries).arg(url.toString()); - auto request = DependencyManager::get()->createResourceRequest(nullptr, url); + auto request = DependencyManager::get()->createResourceRequest( + nullptr, url, true, -1, "ScriptCache::scriptContentAvailable"); Q_ASSERT(request); // We've already made a request, so the cache must be disabled or it wasn't there, so enabling diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index ebc459b2d1..a74d185c6a 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -21,6 +21,7 @@ #include #include +#include "ResourceRequestObserver.h" #include "ScriptEngine.h" const QString METAVERSE_API_URL = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/api/"; @@ -189,7 +190,7 @@ void XMLHttpRequestClass::send(const QScriptValue& data) { } void XMLHttpRequestClass::doSend() { - + DependencyManager::get()->update(_url, -1, "XMLHttpRequestClass::doSend"); _reply = NetworkAccessManager::getInstance().sendCustomRequest(_request, _method.toLatin1(), _sendData); connectToReply(_reply); diff --git a/libraries/shared/src/ResourceRequestObserver.cpp b/libraries/shared/src/ResourceRequestObserver.cpp new file mode 100644 index 0000000000..5e0925520a --- /dev/null +++ b/libraries/shared/src/ResourceRequestObserver.cpp @@ -0,0 +1,28 @@ +// +// ResourceAccessMonitor.h +// libraries/networking/src +// +// Created by Kerry Ivan Kurian on 9/27/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 +// + + +#include +#include +#include +#include +#include "ResourceRequestObserver.h" + +void ResourceRequestObserver::update(const QUrl& requestUrl, + const qint64 callerId, + const QString& extra) { + QJsonArray array; + QJsonObject data { { "url", requestUrl.toString() }, + { "callerId", callerId }, + { "extra", extra } + }; + emit resourceRequestEvent(data.toVariantMap()); +} diff --git a/libraries/shared/src/ResourceRequestObserver.h b/libraries/shared/src/ResourceRequestObserver.h new file mode 100644 index 0000000000..1b1bc322e5 --- /dev/null +++ b/libraries/shared/src/ResourceRequestObserver.h @@ -0,0 +1,29 @@ +// +// ResourceRequestObserver.h +// libraries/commerce/src +// +// Created by Kerry Ivan Kurian on 9/27/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 +// + + +#include +#include +#include + +#include "DependencyManager.h" + + +class ResourceRequestObserver : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + void update(const QUrl& requestUrl, const qint64 callerId = -1, const QString& extra = ""); + +signals: + void resourceRequestEvent(QVariantMap result); +}; diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index 511984c657..402b05f39c 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -85,6 +85,11 @@ private: if (!OVR_SUCCESS(ovr_Create(&session, &luid))) { qCWarning(oculusLog) << "Failed to acquire Oculus session" << ovr::getError(); return; + } else { + ovrResult setFloorLevelOrigin = ovr_SetTrackingOriginType(session, ovrTrackingOrigin::ovrTrackingOrigin_FloorLevel); + if (!OVR_SUCCESS(setFloorLevelOrigin)) { + qCWarning(oculusLog) << "Failed to set the Oculus tracking origin to floor level" << ovr::getError(); + } } } diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index ef0ac65c2a..99c861871d 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -463,7 +463,7 @@ bool OpenVrDisplayPlugin::internalActivate() { auto chaperone = vr::VRChaperone(); if (chaperone) { float const UI_RADIUS = 1.0f; - float const UI_HEIGHT = 1.6f; + float const UI_HEIGHT = 0.0f; float const UI_Z_OFFSET = 0.5; float xSize, zSize; diff --git a/scripts/system/edit.js b/scripts/system/edit.js index b911541f79..15068cf097 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -42,6 +42,9 @@ var TITLE_OFFSET = 60; var CREATE_TOOLS_WIDTH = 490; var MAX_DEFAULT_ENTITY_LIST_HEIGHT = 942; +var IMAGE_MODEL = "https://hifi-content.s3.amazonaws.com/DomainContent/production/default-image-model.fbx"; +var DEFAULT_IMAGE = "https://hifi-content.s3.amazonaws.com/DomainContent/production/no-image.jpg"; + var createToolsWindow = new CreateWindow( Script.resourcesPath() + "qml/hifi/tablet/EditTools.qml", 'Create Tools', @@ -294,6 +297,202 @@ function checkEditPermissionsAndUpdate() { } } +const DEFAULT_ENTITY_PROPERTIES = { + All: { + description: "", + rotation: { x: 0, y: 0, z: 0, w: 1 }, + collidesWith: "static,dynamic,kinematic,otherAvatar", + collisionSoundURL: "", + cloneable: false, + ignoreIK: true, + canCastShadow: true, + href: "", + script: "", + serverScripts:"", + velocity: { + x: 0, + y: 0, + z: 0 + }, + damping: 0, + angularVelocity: { + x: 0, + y: 0, + z: 0 + }, + angularDamping: 0, + restitution: 0.5, + friction: 0.5, + density: 1000, + gravity: { + x: 0, + y: 0, + z: 0 + }, + acceleration: { + x: 0, + y: 0, + z: 0 + }, + dynamic: false, + }, + Shape: { + shape: "Box", + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + color: { red: 0, green: 180, blue: 239 }, + }, + Text: { + text: "Text", + dimensions: { + x: 0.65, + y: 0.3, + z: 0.01 + }, + textColor: { red: 255, green: 255, blue: 255 }, + backgroundColor: { red: 0, green: 0, blue: 0 }, + lineHeight: 0.06, + faceCamera: false, + }, + Zone: { + dimensions: { + x: 10, + y: 10, + z: 10 + }, + flyingAllowed: true, + ghostingAllowed: true, + filter: "", + keyLightMode: "inherit", + keyLightColor: { red: 255, green: 255, blue: 255 }, + keyLight: { + intensity: 1.0, + direction: { + x: 0.0, + y: -0.707106769084930, // 45 degrees + z: 0.7071067690849304 + }, + castShadows: true + }, + ambientLightMode: "inherit", + ambientLight: { + ambientIntensity: 0.5, + ambientURL: "" + }, + hazeMode: "inherit", + haze: { + hazeRange: 1000, + hazeAltitudeEffect: false, + hazeBaseRef: 0, + hazeColor: { + red: 128, + green: 154, + blue: 179 + }, + hazeBackgroundBlend: 0, + hazeEnableGlare: false, + hazeGlareColor: { + red: 255, + green: 229, + blue: 179 + }, + }, + bloomMode: "inherit" + }, + Model: { + collisionShape: "none", + compoundShapeURL: "", + animation: { + url: "", + running: false, + allowTranslation: false, + loop: true, + hold: false, + currentFrame: 0, + firstFrame: 0, + lastFrame: 100000, + fps: 30.0, + } + }, + Image: { + dimensions: { + x: 0.5385, + y: 0.2819, + z: 0.0092 + }, + shapeType: "box", + collisionless: true, + modelURL: IMAGE_MODEL, + textures: JSON.stringify({ "tex.picture": "" }) + }, + Web: { + dimensions: { + x: 1.6, + y: 0.9, + z: 0.01 + }, + sourceUrl: "https://highfidelity.com/", + dpi: 30, + }, + ParticleEffect: { + lifespan: 1.5, + maxParticles: 10, + textures: "https://content.highfidelity.com/DomainContent/production/Particles/wispy-smoke.png", + emitRate: 5.5, + emitSpeed: 0, + speedSpread: 0, + emitDimensions: { x: 0, y: 0, z: 0 }, + emitOrientation: { x: 0, y: 0, z: 0, w: 1 }, + emitterShouldTrail: true, + particleRadius: 0.25, + radiusStart: 0, + radiusFinish: 0.1, + radiusSpread: 0, + particleColor: { + red: 255, + green: 255, + blue: 255 + }, + colorSpread: { + red: 0, + green: 0, + blue: 0 + }, + alpha: 0, + alphaStart: 1, + alphaFinish: 0, + alphaSpread: 0, + emitAcceleration: { + x: 0, + y: 2.5, + z: 0 + }, + accelerationSpread: { + x: 0, + y: 0, + z: 0 + }, + particleSpin: 0, + spinStart: 0, + spinFinish: 0, + spinSpread: 0, + rotateWithEntity: false, + polarStart: 0, + polarFinish: 0, + azimuthStart: -Math.PI, + azimuthFinish: Math.PI + }, + Light: { + color: { red: 255, green: 255, blue: 255 }, + intensity: 5.0, + dimensions: DEFAULT_LIGHT_DIMENSIONS, + falloffRadius: 1.0, + isSpotlight: false, + exponent: 1.0, + cutoff: 75.0, + dimensions: { x: 20, y: 20, z: 20 }, + }, +}; + var toolBar = (function () { var EDIT_SETTING = "io.highfidelity.isEditing"; // for communication with other scripts var that = {}, @@ -303,11 +502,29 @@ var toolBar = (function () { dialogWindow = null, tablet = null; + function applyProperties(originalProperties, newProperties) { + for (var key in newProperties) { + originalProperties[key] = newProperties[key]; + } + } function createNewEntity(properties) { var dimensions = properties.dimensions ? properties.dimensions : DEFAULT_DIMENSIONS; var position = getPositionToCreateEntity(); var entityID = null; + applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.All); + + var type = properties.type; + if (type == "Box" || type == "Sphere") { + applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.Shape); + } else if (type == "Image") { + properties.type = "Model"; + applyProperties(properties, DEFAULT_ENTITY_PROPERTIES.Image); + } else { + applyProperties(properties, DEFAULT_ENTITY_PROPERTIES[type]); + } + + if (position !== null && position !== undefined) { var direction; if (Camera.mode === "entity" || Camera.mode === "independent") { @@ -385,7 +602,7 @@ var toolBar = (function () { Entities.editEntity(entityID, { position: position }); - selectionManager._update(); + selectionManager._update(false, this); } else if (dimensionsCheckCount < MAX_DIMENSIONS_CHECKS) { Script.setTimeout(dimensionsCheckFunction, DIMENSIONS_CHECK_INTERVAL); } @@ -397,9 +614,9 @@ var toolBar = (function () { properties.type + " would be out of bounds."); } - selectionManager.clearSelections(); + selectionManager.clearSelections(this); entityListTool.sendUpdate(); - selectionManager.setSelections([entityID]); + selectionManager.setSelections([entityID], this); Window.setFocus(); @@ -550,7 +767,7 @@ var toolBar = (function () { } deletedEntityTimer = Script.setTimeout(function () { if (entitiesToDelete.length > 0) { - selectionManager.removeEntities(entitiesToDelete); + selectionManager.removeEntities(entitiesToDelete, this); } entityListTool.removeEntities(entitiesToDelete, selectionManager.selections); entitiesToDelete = []; @@ -679,14 +896,12 @@ var toolBar = (function () { addButton("newLightButton", function () { createNewEntity({ type: "Light", - dimensions: DEFAULT_LIGHT_DIMENSIONS, isSpotlight: false, color: { red: 150, green: 150, blue: 150 }, - constantAttenuation: 1, linearAttenuation: 0, quadraticAttenuation: 0, @@ -698,116 +913,30 @@ var toolBar = (function () { addButton("newTextButton", function () { createNewEntity({ type: "Text", - dimensions: { - x: 0.65, - y: 0.3, - z: 0.01 - }, - backgroundColor: { - red: 64, - green: 64, - blue: 64 - }, - textColor: { - red: 255, - green: 255, - blue: 255 - }, - text: "some text", - lineHeight: 0.06 }); }); addButton("newImageButton", function () { - var IMAGE_MODEL = "https://hifi-content.s3.amazonaws.com/DomainContent/production/default-image-model.fbx"; - var DEFAULT_IMAGE = "https://hifi-content.s3.amazonaws.com/DomainContent/production/no-image.jpg"; createNewEntity({ - type: "Model", - dimensions: { - x: 0.5385, - y: 0.2819, - z: 0.0092 - }, - shapeType: "box", - collisionless: true, - modelURL: IMAGE_MODEL, - textures: JSON.stringify({ "tex.picture": DEFAULT_IMAGE }) + type: "Image", }); }); addButton("newWebButton", function () { createNewEntity({ type: "Web", - dimensions: { - x: 1.6, - y: 0.9, - z: 0.01 - }, - sourceUrl: "https://highfidelity.com/" }); }); addButton("newZoneButton", function () { createNewEntity({ type: "Zone", - dimensions: { - x: 10, - y: 10, - z: 10 - } }); }); addButton("newParticleButton", function () { createNewEntity({ type: "ParticleEffect", - isEmitting: true, - emitterShouldTrail: true, - color: { - red: 200, - green: 200, - blue: 200 - }, - colorSpread: { - red: 0, - green: 0, - blue: 0 - }, - colorStart: { - red: 200, - green: 200, - blue: 200 - }, - colorFinish: { - red: 0, - green: 0, - blue: 0 - }, - emitAcceleration: { - x: -0.5, - y: 2.5, - z: -0.5 - }, - accelerationSpread: { - x: 0.5, - y: 1, - z: 0.5 - }, - emitRate: 5.5, - emitSpeed: 0, - speedSpread: 0, - lifespan: 1.5, - maxParticles: 10, - particleRadius: 0.25, - radiusStart: 0, - radiusFinish: 0.1, - radiusSpread: 0, - alpha: 0, - alphaStart: 1, - alphaFinish: 0, - polarStart: 0, - polarFinish: 0, - textures: "https://content.highfidelity.com/DomainContent/production/Particles/wispy-smoke.png" }); }); @@ -866,7 +995,7 @@ var toolBar = (function () { gridTool.setVisible(false); grid.setEnabled(false); propertiesTool.setVisible(false); - selectionManager.clearSelections(); + selectionManager.clearSelections(this); cameraManager.disable(); selectionDisplay.disableTriggerMapping(); tablet.landscape = false; @@ -994,7 +1123,7 @@ function handleOverlaySelectionToolUpdates(channel, message, sender) { var entity = entityIconOverlayManager.findEntity(data.overlayID); if (entity !== null) { - selectionManager.setSelections([entity]); + selectionManager.setSelections([entity], this); } } } @@ -1141,7 +1270,7 @@ function mouseClickEvent(event) { if (result === null || result === undefined) { if (!event.isShifted) { - selectionManager.clearSelections(); + selectionManager.clearSelections(this); } return; } @@ -1193,9 +1322,9 @@ function mouseClickEvent(event) { } if (!event.isShifted) { - selectionManager.setSelections([foundEntity]); + selectionManager.setSelections([foundEntity], this); } else { - selectionManager.addEntity(foundEntity, true); + selectionManager.addEntity(foundEntity, true, this); } if (wantDebug) { @@ -1493,7 +1622,7 @@ function selectAllEtitiesInCurrentSelectionBox(keepIfTouching) { } } } - selectionManager.setSelections(entities); + selectionManager.setSelections(entities, this); } } @@ -1633,7 +1762,7 @@ function deleteSelectedEntities() { } if (savedProperties.length > 0) { - SelectionManager.clearSelections(); + SelectionManager.clearSelections(this); pushCommandForSelections([], savedProperties); entityListTool.deleteEntities(deletedIDs); } @@ -1650,7 +1779,7 @@ function toggleSelectedEntitiesLocked() { }); } entityListTool.sendUpdate(); - selectionManager._update(); + selectionManager._update(false, this); } } @@ -1664,7 +1793,7 @@ function toggleSelectedEntitiesVisible() { }); } entityListTool.sendUpdate(); - selectionManager._update(); + selectionManager._update(false, this); } } @@ -1861,7 +1990,7 @@ function importSVO(importURL) { } if (isActive) { - selectionManager.setSelections(pastedEntityIDs); + selectionManager.setSelections(pastedEntityIDs, this); } } else { Window.notifyEditError("Can't import entities: entities would be out of bounds."); @@ -1909,7 +2038,7 @@ function deleteKey(value) { } function deselectKey(value) { if (value === 0) { // on release - selectionManager.clearSelections(); + selectionManager.clearSelections(this); } } function toggleKey(value) { @@ -2069,7 +2198,7 @@ function applyEntityProperties(data) { // 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); + selectionManager.setSelections(selectedEntityIDs, this); } } @@ -2272,7 +2401,7 @@ var PropertiesTool = function (opts) { } } pushCommandForSelections(); - selectionManager._update(); + selectionManager._update(false, this); } else if (data.type === 'parent') { parentSelectedEntities(); } else if (data.type === 'unparent') { @@ -2301,7 +2430,7 @@ var PropertiesTool = function (opts) { }); } pushCommandForSelections(); - selectionManager._update(); + selectionManager._update(false, this); } } else if (data.action === "moveAllToGrid") { if (selectionManager.hasSelection()) { @@ -2321,7 +2450,7 @@ var PropertiesTool = function (opts) { }); } pushCommandForSelections(); - selectionManager._update(); + selectionManager._update(false, this); } } else if (data.action === "resetToNaturalDimensions") { if (selectionManager.hasSelection()) { @@ -2342,7 +2471,7 @@ var PropertiesTool = function (opts) { } } pushCommandForSelections(); - selectionManager._update(); + selectionManager._update(false, this); } } else if (data.action === "previewCamera") { if (selectionManager.hasSelection()) { @@ -2360,7 +2489,7 @@ var PropertiesTool = function (opts) { }); } pushCommandForSelections(); - selectionManager._update(); + selectionManager._update(false, this); } } else if (data.action === "reloadClientScripts") { if (selectionManager.hasSelection()) { diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 8e7b3f1ad5..956ae529ab 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1801,3 +1801,43 @@ input[type=button]#export { body#entity-list-body { padding-bottom: 0; } + +.context-menu { + display: none; + position: fixed; + color: #000000; + background-color: #afafaf; + padding: 5px 0 5px 0; + cursor: default; +} +.context-menu li { + list-style-type: none; + padding: 4px 18px 4px 18px; + margin: 0; + white-space: nowrap; +} +.context-menu li:hover { + background-color: #e3e3e3; +} +.context-menu li.separator { + border-top: 1px solid #333333; + margin: 5px 5px; + padding: 0 0; +} +.context-menu li.disabled { + color: #333333; +} +.context-menu li.separator:hover, .context-menu li.disabled:hover { + background-color: #afafaf; +} + +input.rename-entity { + height: 100%; + width: 100%; + border: none; + font-family: FiraSans-SemiBold; + font-size: 15px; + /* need this to show the text cursor when the input field is empty */ + padding-left: 2px; +} + diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index c62c785c99..ae994b56a4 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -16,6 +16,7 @@ + diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 0ced016d26..4beff83b0e 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -13,7 +13,7 @@ const DESCENDING_STRING = '▾'; const LOCKED_GLYPH = ""; const VISIBLE_GLYPH = ""; const TRANSPARENCY_GLYPH = ""; -const BAKED_GLYPH = "" +const BAKED_GLYPH = ""; const SCRIPT_GLYPH = "k"; const BYTES_PER_MEGABYTE = 1024 * 1024; const IMAGE_MODEL_NAME = 'default-image-model.fbx'; @@ -54,10 +54,10 @@ const COMPARE_ASCENDING = function(a, b) { } return 1; -} +}; const COMPARE_DESCENDING = function(a, b) { return COMPARE_ASCENDING(b, a); -} +}; // List of all entities var entities = []; @@ -70,6 +70,11 @@ var selectedEntities = []; var entityList = null; // The ListView +/** + * @type EntityListContextMenu + */ +var entityListContextMenu = null; + var currentSortColumn = 'type'; var currentSortOrder = ASCENDING_SORT; var isFilterInView = false; @@ -156,22 +161,22 @@ function loaded() { }; elRefresh.onclick = function() { refreshEntities(); - } + }; elToggleLocked.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleLocked' })); - } + }; elToggleVisible.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleVisible' })); - } + }; elExport.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'export'})); - } + }; elPal.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'pal' })); - } + }; elDelete.onclick = function() { EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); - } + }; elFilter.onkeyup = refreshEntityList; elFilter.onpaste = refreshEntityList; elFilter.onchange = onFilterChange; @@ -184,17 +189,105 @@ function loaded() { entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow, createRow, updateRow, clearRow, WINDOW_NONVARIABLE_HEIGHT); - + + entityListContextMenu = new EntityListContextMenu(); + + + function startRenamingEntity(entityID) { + let entity = entitiesByID[entityID]; + if (!entity || entity.locked || !entity.elRow) { + return; + } + + let elCell = entity.elRow.childNodes[COLUMN_INDEX.NAME]; + let elRenameInput = document.createElement("input"); + elRenameInput.setAttribute('class', 'rename-entity'); + elRenameInput.value = entity.name; + let ignoreClicks = function(event) { + event.stopPropagation(); + }; + elRenameInput.onclick = ignoreClicks; + elRenameInput.ondblclick = ignoreClicks; + elRenameInput.onkeyup = function(keyEvent) { + if (keyEvent.key === "Enter") { + elRenameInput.blur(); + } + }; + + elRenameInput.onblur = function(event) { + let value = elRenameInput.value; + EventBridge.emitWebEvent(JSON.stringify({ + type: 'rename', + entityID: entityID, + name: value + })); + entity.name = value; + elCell.innerText = value; + }; + + elCell.innerHTML = ""; + elCell.appendChild(elRenameInput); + + elRenameInput.select(); + } + + entityListContextMenu.setOnSelectedCallback(function(optionName, selectedEntityID) { + switch (optionName) { + case "Cut": + EventBridge.emitWebEvent(JSON.stringify({ type: 'cut' })); + break; + case "Copy": + EventBridge.emitWebEvent(JSON.stringify({ type: 'copy' })); + break; + case "Paste": + EventBridge.emitWebEvent(JSON.stringify({ type: 'paste' })); + break; + case "Rename": + startRenamingEntity(selectedEntityID); + break; + case "Duplicate": + EventBridge.emitWebEvent(JSON.stringify({ type: 'duplicate' })); + break; + case "Delete": + EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); + break; + } + }); + + function onRowContextMenu(clickEvent) { + let entityID = this.dataset.entityID; + + if (!selectedEntities.includes(entityID)) { + let selection = [entityID]; + updateSelectedEntities(selection); + + EventBridge.emitWebEvent(JSON.stringify({ + type: "selectionUpdate", + focus: false, + entityIds: selection, + })); + } + + let enabledContextMenuItems = ['Copy', 'Paste', 'Duplicate']; + if (entitiesByID[entityID] && !entitiesByID[entityID].locked) { + enabledContextMenuItems.push('Cut'); + enabledContextMenuItems.push('Rename'); + enabledContextMenuItems.push('Delete'); + } + + entityListContextMenu.open(clickEvent, entityID, enabledContextMenuItems); + } + function onRowClicked(clickEvent) { let entityID = this.dataset.entityID; let selection = [entityID]; - + if (clickEvent.ctrlKey) { let selectedIndex = selectedEntities.indexOf(entityID); if (selectedIndex >= 0) { selection = []; selection = selection.concat(selectedEntities); - selection.splice(selectedIndex, 1) + selection.splice(selectedIndex, 1); } else { selection = selection.concat(selectedEntities); } @@ -221,28 +314,29 @@ function loaded() { } } } else if (!clickEvent.ctrlKey && !clickEvent.shiftKey && selectedEntities.length === 1) { - // if reselecting the same entity then deselect it + // if reselecting the same entity then start renaming it if (selectedEntities[0] === entityID) { - selection = []; + startRenamingEntity(entityID); } } - updateSelectedEntities(selection); + updateSelectedEntities(selection, false); EventBridge.emitWebEvent(JSON.stringify({ type: "selectionUpdate", focus: false, entityIds: selection, })); - - refreshFooter(); } function onRowDoubleClicked() { + let selection = [this.dataset.entityID]; + updateSelectedEntities(selection, false); + EventBridge.emitWebEvent(JSON.stringify({ type: "selectionUpdate", focus: true, - entityIds: [this.dataset.entityID], + entityIds: selection, })); } @@ -289,7 +383,7 @@ function loaded() { hasScript: entity.hasScript, elRow: null, // if this entity has a visible row element assigned to it selected: false // if this entity is selected for edit regardless of having a visible row - } + }; entities.push(entityData); entitiesByID[entityData.id] = entityData; @@ -418,7 +512,7 @@ function loaded() { isBaked: document.querySelector('#entity-isBaked .sort-order'), drawCalls: document.querySelector('#entity-drawCalls .sort-order'), hasScript: document.querySelector('#entity-hasScript .sort-order'), - } + }; function setSortColumn(column) { PROFILE("set-sort-column", function() { if (currentSortColumn === column) { @@ -453,7 +547,7 @@ function loaded() { } } - function updateSelectedEntities(selectedIDs) { + function updateSelectedEntities(selectedIDs, autoScroll) { let notFound = false; // reset all currently selected entities and their rows first @@ -482,6 +576,26 @@ function loaded() { } }); + if (autoScroll && selectedIDs.length > 0) { + let firstItem = Number.MAX_VALUE; + let lastItem = -1; + let itemFound = false; + visibleEntities.forEach(function(entity, index) { + if (selectedIDs.indexOf(entity.id) !== -1) { + if (firstItem > index) { + firstItem = index; + } + if (lastItem < index) { + lastItem = index; + } + itemFound = true; + } + }); + if (itemFound) { + entityList.scrollToRow(firstItem, lastItem); + } + } + refreshFooter(); return notFound; @@ -502,6 +616,7 @@ function loaded() { } row.appendChild(column); } + row.oncontextmenu = onRowContextMenu; row.onclick = onRowClicked; row.ondblclick = onRowDoubleClicked; return row; @@ -641,7 +756,7 @@ function loaded() { if (data.type === "clearEntityList") { clearEntities(); } else if (data.type === "selectionUpdate") { - let notFound = updateSelectedEntities(data.selectedIDs); + let notFound = updateSelectedEntities(data.selectedIDs, true); if (notFound) { refreshEntities(); } @@ -653,13 +768,13 @@ function loaded() { clearEntities(); } else { updateEntityData(newEntities); - updateSelectedEntities(data.selectedIDs); + updateSelectedEntities(data.selectedIDs, true); } } }); } else if (data.type === "removeEntities" && data.deletedIDs !== undefined && data.selectedIDs !== undefined) { removeEntities(data.deletedIDs); - updateSelectedEntities(data.selectedIDs); + updateSelectedEntities(data.selectedIDs, true); } else if (data.type === "deleted" && data.ids) { removeEntities(data.ids); } @@ -672,8 +787,15 @@ function loaded() { augmentSpinButtons(); - // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked document.addEventListener("contextmenu", function (event) { + entityListContextMenu.close(); + + // Disable default right-click context menu which is not visible in the HMD and makes it seem like the app has locked event.preventDefault(); }, false); + + // close context menu when switching focus to another window + $(window).blur(function() { + entityListContextMenu.close(); + }); } diff --git a/scripts/system/html/js/entityListContextMenu.js b/scripts/system/html/js/entityListContextMenu.js new file mode 100644 index 0000000000..d71719f252 --- /dev/null +++ b/scripts/system/html/js/entityListContextMenu.js @@ -0,0 +1,163 @@ +// +// entityListContextMenu.js +// +// exampleContextMenus.js was originally created by David Rowe on 22 Aug 2018. +// Modified to entityListContextMenu.js by Thijs Wenker on 10 Oct 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 +// + +/* eslint-env browser */ +const CONTEXT_MENU_CLASS = "context-menu"; + +/** + * ContextMenu class for EntityList + * @constructor + */ +function EntityListContextMenu() { + this._elContextMenu = null; + this._onSelectedCallback = null; + this._listItems = []; + this._initialize(); +} + +EntityListContextMenu.prototype = { + + /** + * @private + */ + _elContextMenu: null, + + /** + * @private + */ + _onSelectedCallback: null, + + /** + * @private + */ + _selectedEntityID: null, + + /** + * @private + */ + _listItems: null, + + /** + * Close the context menu + */ + close: function() { + if (this.isContextMenuOpen()) { + this._elContextMenu.style.display = "none"; + } + }, + + isContextMenuOpen: function() { + return this._elContextMenu.style.display === "block"; + }, + + /** + * Open the context menu + * @param clickEvent + * @param selectedEntityID + * @param enabledOptions + */ + open: function(clickEvent, selectedEntityID, enabledOptions) { + this._selectedEntityID = selectedEntityID; + + this._listItems.forEach(function(listItem) { + let enabled = enabledOptions.includes(listItem.label); + listItem.enabled = enabled; + listItem.element.setAttribute('class', enabled ? '' : 'disabled'); + }); + + this._elContextMenu.style.display = "block"; + this._elContextMenu.style.left + = Math.min(clickEvent.pageX, document.body.offsetWidth - this._elContextMenu.offsetWidth).toString() + "px"; + this._elContextMenu.style.top + = Math.min(clickEvent.pageY, document.body.clientHeight - this._elContextMenu.offsetHeight).toString() + "px"; + clickEvent.stopPropagation(); + }, + + /** + * Set the callback for when a menu item is selected + * @param onSelectedCallback + */ + setOnSelectedCallback: function(onSelectedCallback) { + this._onSelectedCallback = onSelectedCallback; + }, + + /** + * Add a labeled item to the context menu + * @param itemLabel + * @private + */ + _addListItem: function(itemLabel) { + let elListItem = document.createElement("li"); + elListItem.innerText = itemLabel; + + let listItem = { + label: itemLabel, + element: elListItem, + enabled: false + }; + + elListItem.addEventListener("click", function () { + if (listItem.enabled && this._onSelectedCallback) { + this._onSelectedCallback.call(this, itemLabel, this._selectedEntityID); + } + }.bind(this), false); + + elListItem.setAttribute('class', 'disabled'); + + this._listItems.push(listItem); + this._elContextMenu.appendChild(elListItem); + }, + + /** + * Add a separator item to the context menu + * @private + */ + _addListSeparator: function() { + let elListItem = document.createElement("li"); + elListItem.setAttribute('class', 'separator'); + this._elContextMenu.appendChild(elListItem); + }, + + /** + * Initialize the context menu. + * @private + */ + _initialize: function() { + this._elContextMenu = document.createElement("ul"); + this._elContextMenu.setAttribute("class", CONTEXT_MENU_CLASS); + document.body.appendChild(this._elContextMenu); + + this._addListItem("Cut"); + this._addListItem("Copy"); + this._addListItem("Paste"); + this._addListSeparator(); + this._addListItem("Rename"); + this._addListItem("Duplicate"); + this._addListItem("Delete"); + + // Ignore clicks on context menu background or separator. + this._elContextMenu.addEventListener("click", function(event) { + // Sink clicks on context menu background or separator but let context menu item clicks through. + if (event.target.classList.contains(CONTEXT_MENU_CLASS)) { + event.stopPropagation(); + } + }); + + // Provide means to close context menu without clicking menu item. + document.body.addEventListener("click", this.close.bind(this)); + document.body.addEventListener("keydown", function(event) { + // Close context menu with Esc key. + if (this.isContextMenuOpen() && event.key === "Escape") { + this.close(); + } + }.bind(this)); + } +}; diff --git a/scripts/system/html/js/listView.js b/scripts/system/html/js/listView.js index 10dc37efba..62163ffe16 100644 --- a/scripts/system/html/js/listView.js +++ b/scripts/system/html/js/listView.js @@ -38,7 +38,7 @@ function ListView(elTableBody, elTableScroll, elTableHeaderRow, createRowFunctio this.lastRowShiftScrollTop = 0; this.initialize(); -}; +} ListView.prototype = { getNumRows: function() { @@ -152,6 +152,30 @@ ListView.prototype = { this.refresh(); } }, + + /** + * Scrolls firstRowIndex with least effort, also tries to make the window include the other selections in case lastRowIndex is set. + * In the case that firstRowIndex and lastRowIndex are already within the visible bounds then nothing will happen. + * @param {number} firstRowIndex - The row that will be scrolled to. + * @param {number} lastRowIndex - The last index of the bound. + */ + scrollToRow: function (firstRowIndex, lastRowIndex) { + lastRowIndex = lastRowIndex ? lastRowIndex : firstRowIndex; + let boundingTop = firstRowIndex * this.rowHeight; + let boundingBottom = (lastRowIndex * this.rowHeight) + this.rowHeight; + if ((boundingBottom - boundingTop) > this.elTableScroll.clientHeight) { + boundingBottom = boundingTop + this.elTableScroll.clientHeight; + } + + let currentVisibleAreaTop = this.elTableScroll.scrollTop; + let currentVisibleAreaBottom = currentVisibleAreaTop + this.elTableScroll.clientHeight; + + if (boundingTop < currentVisibleAreaTop) { + this.elTableScroll.scrollTop = boundingTop; + } else if (boundingBottom > currentVisibleAreaBottom) { + this.elTableScroll.scrollTop = boundingBottom - (this.elTableScroll.clientHeight); + } + }, refresh: function() { // block refreshing before rows are initialized diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 30e952723f..585820d32f 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -15,7 +15,7 @@ var PROFILING_ENABLED = false; var profileIndent = ''; const PROFILE_NOOP = function(_name, fn, args) { fn.apply(this, args); -} ; +}; PROFILE = !PROFILING_ENABLED ? PROFILE_NOOP : function(name, fn, args) { console.log("PROFILE-Script " + profileIndent + "(" + name + ") Begin"); var previousIndent = profileIndent; @@ -98,7 +98,11 @@ EntityListTool = function(shouldUseEditTabletApp) { that.setVisible(!visible); }; - selectionManager.addEventListener(function() { + selectionManager.addEventListener(function(isSelectionUpdate, caller) { + if (caller === that) { + // ignore events that we emitted from the entity list itself + return; + } var selectedIDs = []; for (var i = 0; i < selectionManager.selections.length; i++) { @@ -224,7 +228,7 @@ EntityListTool = function(shouldUseEditTabletApp) { for (var i = 0; i < ids.length; i++) { entityIDs.push(ids[i]); } - selectionManager.setSelections(entityIDs); + selectionManager.setSelections(entityIDs, that); if (data.focus) { cameraManager.enable(); cameraManager.focus(selectionManager.worldPosition, @@ -245,7 +249,7 @@ EntityListTool = function(shouldUseEditTabletApp) { Window.saveAsync("Select Where to Save", "", "*.json"); } } else if (data.type === "pal") { - var sessionIds = {}; // Collect the sessionsIds of all selected entitities, w/o duplicates. + var sessionIds = {}; // Collect the sessionsIds of all selected entities, w/o duplicates. selectionManager.selections.forEach(function (id) { var lastEditedBy = Entities.getEntityProperties(id, 'lastEditedBy').lastEditedBy; if (lastEditedBy) { @@ -271,6 +275,19 @@ EntityListTool = function(shouldUseEditTabletApp) { filterInView = data.filterInView === true; } else if (data.type === "radius") { searchRadius = data.radius; + } else if (data.type === "cut") { + SelectionManager.cutSelectedEntities(); + } else if (data.type === "copy") { + SelectionManager.copySelectedEntities(); + } else if (data.type === "paste") { + SelectionManager.pasteEntities(); + } else if (data.type === "duplicate") { + SelectionManager.duplicateSelection(); + that.sendUpdate(); + } else if (data.type === "rename") { + Entities.editEntity(data.entityID, {name: data.name}); + // make sure that the name also gets updated in the properties window + SelectionManager._update(); } }; diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 843d3e986f..3bb36d632e 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -40,7 +40,7 @@ SelectionManager = (function() { Messages.messageReceived.connect(handleEntitySelectionToolUpdates); } - // FUNCTION: HANDLE ENTITY SELECTION TOOL UDPATES + // FUNCTION: HANDLE ENTITY SELECTION TOOL UPDATES function handleEntitySelectionToolUpdates(channel, message, sender) { if (channel !== 'entityToolUpdates') { return; @@ -63,7 +63,7 @@ SelectionManager = (function() { if (wantDebug) { print("setting selection to " + messageParsed.entityID); } - that.setSelections([messageParsed.entityID]); + that.setSelections([messageParsed.entityID], that); } } else if (messageParsed.method === "clearSelection") { if (!SelectionDisplay.triggered() || SelectionDisplay.triggeredHand === messageParsed.hand) { @@ -136,7 +136,7 @@ SelectionManager = (function() { return that.selections.length > 0; }; - that.setSelections = function(entityIDs) { + that.setSelections = function(entityIDs, caller) { that.selections = []; for (var i = 0; i < entityIDs.length; i++) { var entityID = entityIDs[i]; @@ -144,10 +144,10 @@ SelectionManager = (function() { Selection.addToSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID); } - that._update(true); + that._update(true, caller); }; - that.addEntity = function(entityID, toggleSelection) { + that.addEntity = function(entityID, toggleSelection, caller) { if (entityID) { var idx = -1; for (var i = 0; i < that.selections.length; i++) { @@ -165,7 +165,7 @@ SelectionManager = (function() { } } - that._update(true); + that._update(true, caller); }; function removeEntityByID(entityID) { @@ -176,21 +176,21 @@ SelectionManager = (function() { } } - that.removeEntity = function (entityID) { + that.removeEntity = function (entityID, caller) { removeEntityByID(entityID); - that._update(true); + that._update(true, caller); }; - that.removeEntities = function(entityIDs) { + that.removeEntities = function(entityIDs, caller) { for (var i = 0, length = entityIDs.length; i < length; i++) { removeEntityByID(entityIDs[i]); } - that._update(true); + that._update(true, caller); }; - that.clearSelections = function() { + that.clearSelections = function(caller) { that.selections = []; - that._update(true); + that._update(true, caller); }; that.addChildrenEntities = function(parentEntityID, entityList) { @@ -353,12 +353,12 @@ SelectionManager = (function() { } return createdEntityIDs; - } + }; that.cutSelectedEntities = function() { - copySelectedEntities(); + that.copySelectedEntities(); deleteSelectedEntities(); - } + }; that.copySelectedEntities = function() { var entityProperties = Entities.getMultipleEntityProperties(that.selections); @@ -434,7 +434,7 @@ SelectionManager = (function() { z: brn.z + entityClipboard.dimensions.z / 2 }; } - } + }; that.pasteEntities = function() { var dimensions = entityClipboard.dimensions; @@ -442,7 +442,7 @@ SelectionManager = (function() { var pastePosition = getPositionToCreateEntity(maxDimension); var deltaPosition = Vec3.subtract(pastePosition, entityClipboard.position); - var copiedProperties = [] + var copiedProperties = []; var ids = []; entityClipboard.entities.forEach(function(originalProperties) { var properties = deepCopy(originalProperties); @@ -475,9 +475,9 @@ SelectionManager = (function() { redo(copiedProperties); undoHistory.pushCommand(undo, copiedProperties, redo, copiedProperties); - } + }; - that._update = function(selectionUpdated) { + that._update = function(selectionUpdated, caller) { var properties = null; if (that.selections.length === 0) { that.localDimensions = null; @@ -542,7 +542,7 @@ SelectionManager = (function() { for (var j = 0; j < listeners.length; j++) { try { - listeners[j](selectionUpdated === true); + listeners[j](selectionUpdated === true, caller); } catch (e) { print("ERROR: entitySelectionTool.update got exception: " + JSON.stringify(e)); } @@ -985,7 +985,7 @@ SelectionDisplay = (function() { that.pressedHand = NO_HAND; that.triggered = function() { return that.triggeredHand !== NO_HAND; - } + }; function pointingAtDesktopWindowOrTablet(hand) { var pointingAtDesktopWindow = (hand === Controller.Standard.RightHand && SelectionManager.pointingAtDesktopWindowRight) || @@ -1032,7 +1032,7 @@ SelectionDisplay = (function() { that.disableTriggerMapping = function() { that.triggerClickMapping.disable(); that.triggerPressMapping.disable(); - } + }; Script.scriptEnding.connect(that.disableTriggerMapping); // FUNCTION DEF(s): Intersection Check Helpers @@ -1234,7 +1234,7 @@ SelectionDisplay = (function() { if (wantDebug) { print(" Trigger SelectionManager::update"); } - SelectionManager._update(); + SelectionManager._update(false, that); if (wantDebug) { print("=============== eST::MouseMoveEvent END ======================="); @@ -1299,7 +1299,7 @@ SelectionDisplay = (function() { lastMouseEvent.isControl = event.isControl; lastMouseEvent.isAlt = event.isAlt; activeTool.onMove(lastMouseEvent); - SelectionManager._update(); + SelectionManager._update(false, this); } }; @@ -1315,7 +1315,7 @@ SelectionDisplay = (function() { lastMouseEvent.isControl = event.isControl; lastMouseEvent.isAlt = event.isAlt; activeTool.onMove(lastMouseEvent); - SelectionManager._update(); + SelectionManager._update(false, this); } }; @@ -2179,7 +2179,7 @@ SelectionDisplay = (function() { } } - SelectionManager._update(); + SelectionManager._update(false, this); } }); } @@ -2301,7 +2301,7 @@ SelectionDisplay = (function() { previousPickRay = pickRay; - SelectionManager._update(); + SelectionManager._update(false, this); } }); } @@ -2488,7 +2488,7 @@ SelectionDisplay = (function() { previousPickRay = pickRay; - SelectionManager._update(); + SelectionManager._update(false, this); } }); } @@ -2599,7 +2599,7 @@ SelectionDisplay = (function() { previousPickRay = pickRay; - SelectionManager._update(); + SelectionManager._update(false, this); } }); } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index cca535a064..3085145176 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -49,6 +49,42 @@ var NO_BUTTON = 0; // QMessageBox::NoButton var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server."; + +var resourceRequestEvents = []; +function signalResourceRequestEvent(data) { + // Once we can tie resource request events to specific resources, + // we will have to update the "0" in here. + var resourceData = "from: " + data.extra + ": " + data.url.toString().replace("__NONE__,", ""); + + if (resourceObjectsInTest[0].resourceDataArray.indexOf(resourceData) === -1) { + resourceObjectsInTest[0].resourceDataArray.push(resourceData); + + resourceObjectsInTest[0].resourceAccessEventText += "[" + data.date.toISOString() + "] " + + resourceData + "\n"; + + ui.tablet.sendToQml({ + method: "resourceRequestEvent", + data: data, + resourceAccessEventText: resourceObjectsInTest[0].resourceAccessEventText + }); + } +} + +function onResourceRequestEvent(data) { + // Once we can tie resource request events to specific resources, + // we will have to update the "0" in here. + if (resourceObjectsInTest[0] && resourceObjectsInTest[0].currentlyRecordingResources) { + var resourceRequestEvent = { + "date": new Date(), + "url": data.url, + "callerId": data.callerId, + "extra": data.extra + }; + resourceRequestEvents.push(resourceRequestEvent); + signalResourceRequestEvent(resourceRequestEvent); + } +} + function onMessageBoxClosed(id, button) { if (id === messageBox && button === CANCEL_BUTTON) { isDownloadBeingCancelled = true; @@ -522,13 +558,18 @@ function getPositionToCreateEntity(extra) { return position; } -function rezEntity(itemHref, itemType) { +function defaultFor(arg, val) { + return typeof arg !== 'undefined' ? arg : val; +} + +function rezEntity(itemHref, itemType, marketplaceItemTesterId) { var isWearable = itemType === "wearable"; - var success = Clipboard.importEntities(itemHref); + var success = Clipboard.importEntities(itemHref, true, marketplaceItemTesterId); var wearableLocalPosition = null; var wearableLocalRotation = null; var wearableLocalDimensions = null; var wearableDimensions = null; + marketplaceItemTesterId = defaultFor(marketplaceItemTesterId, -1); if (itemType === "contentSet") { console.log("Item is a content set; codepath shouldn't go here."); @@ -816,7 +857,8 @@ var resourceObjectsInTest = []; function signalNewResourceObjectInTest(resourceObject) { ui.tablet.sendToQml({ method: "newResourceObjectInTest", - resourceObject: resourceObject }); + resourceObject: resourceObject + }); } var onQmlMessageReceived = function onQmlMessageReceived(message) { @@ -877,11 +919,15 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'checkout_rezClicked': case 'purchases_rezClicked': case 'tester_rezClicked': - rezEntity(message.itemHref, message.itemType); + rezEntity(message.itemHref, message.itemType, message.itemId); break; case 'tester_newResourceObject': var resourceObject = message.resourceObject; - resourceObjectsInTest[resourceObject.id] = resourceObject; + resourceObjectsInTest = []; // REMOVE THIS once we support specific referrers + resourceObject.currentlyRecordingResources = false; + resourceObject.resourceAccessEventText = ""; + resourceObjectsInTest[resourceObject.resourceObjectId] = resourceObject; + resourceObjectsInTest[resourceObject.resourceObjectId].resourceDataArray = []; signalNewResourceObjectInTest(resourceObject); break; case 'tester_updateResourceObjectAssetType': @@ -890,6 +936,13 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'tester_deleteResourceObject': delete resourceObjectsInTest[message.objectId]; break; + case 'tester_updateResourceRecordingStatus': + resourceObjectsInTest[message.objectId].currentlyRecordingResources = message.status; + if (message.status) { + resourceObjectsInTest[message.objectId].resourceDataArray = []; + resourceObjectsInTest[message.objectId].resourceAccessEventText = ""; + } + break; case 'header_marketplaceImageClicked': case 'purchases_backClicked': openMarketplace(message.referrerURL); @@ -1029,16 +1082,22 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { }; function pushResourceObjectsInTest() { - var maxObjectId = -1; - for (var objectId in resourceObjectsInTest) { - signalNewResourceObjectInTest(resourceObjectsInTest[objectId]); - maxObjectId = (maxObjectId < objectId) ? parseInt(objectId) : maxObjectId; + var maxResourceObjectId = -1; + var length = resourceObjectsInTest.length; + for (var i = 0; i < length; i++) { + if (i in resourceObjectsInTest) { + signalNewResourceObjectInTest(resourceObjectsInTest[i]); + var resourceObjectId = resourceObjectsInTest[i].resourceObjectId; + maxResourceObjectId = (maxResourceObjectId < resourceObjectId) ? parseInt(resourceObjectId) : maxResourceObjectId; + } } // N.B. Thinking about removing the following sendToQml? Be sure // that the marketplace item tester QML has heard from us, at least // so that it can indicate to the user that all of the resoruce // objects in test have been transmitted to it. - ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: maxObjectId + 1 }); + //ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: maxResourceObjectId + 1 }); + // Since, for now, we only support 1 object in test, always send id: 0 + ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: 0 }); } // Function Name: onTabletScreenChanged() @@ -1193,6 +1252,7 @@ function startup() { ui.tablet.webEventReceived.connect(onWebEventReceived); Wallet.walletStatusChanged.connect(sendCommerceSettings); Window.messageBoxClosed.connect(onMessageBoxClosed); + ResourceRequestObserver.resourceRequestEvent.connect(onResourceRequestEvent); Wallet.refreshWalletStatus(); } @@ -1226,6 +1286,7 @@ function shutdown() { GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); ContextOverlay.contextOverlayClicked.disconnect(openInspectionCertificateQML); + ResourceRequestObserver.resourceRequestEvent.disconnect(onResourceRequestEvent); off(); } diff --git a/server-console/src/main.js b/server-console/src/main.js index f731bacaa6..dc3fbd4333 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -874,10 +874,6 @@ function onContentLoaded() { hasShownUpdateNotification = true; } }); - notifier.on('click', function(notifierObject, options) { - log.debug("Got click", options.url); - shell.openExternal(options.url); - }); } deleteOldFiles(logPath, DELETE_LOG_FILES_OLDER_THAN_X_SECONDS, LOG_FILE_REGEX); diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js index b1f337bbc3..8a812625b4 100644 --- a/server-console/src/modules/hf-notifications.js +++ b/server-console/src/modules/hf-notifications.js @@ -5,6 +5,8 @@ const process = require('process'); const hfApp = require('./hf-app'); const path = require('path'); const AccountInfo = require('./hf-acctinfo').AccountInfo; +const url = require('url'); +const shell = require('electron').shell; const GetBuildInfo = hfApp.getBuildInfo; const buildInfo = GetBuildInfo(); const osType = os.type(); @@ -154,8 +156,13 @@ function HifiNotifications(config, menuNotificationCallback) { var _menuNotificationCallback = menuNotificationCallback; notifier.on('click', function (notifierObject, options) { - StartInterface(options.url); - _menuNotificationCallback(options.notificationType, false); + const optUrl = url.parse(options.url); + if ((optUrl.protocol === "hifi:") || (optUrl.protocol === "hifiapp:")) { + StartInterface(options.url); + _menuNotificationCallback(options.notificationType, false); + } else { + shell.openExternal(options.url); + } }); }