diff --git a/android/build_android.sh b/android/build_android.sh
index f98bd1a4b2..189e6099a8 100755
--- a/android/build_android.sh
+++ b/android/build_android.sh
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -xeuo pipefail
-./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} setupDependencies
-./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} app:${ANDROID_BUILD_TARGET}
\ No newline at end of file
+./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} setupDependencies
+./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} app:${ANDROID_BUILD_TARGET}
\ No newline at end of file
diff --git a/android/containerized_build.sh b/android/containerized_build.sh
index cd6f15a92e..e5ec895146 100755
--- a/android/containerized_build.sh
+++ b/android/containerized_build.sh
@@ -9,14 +9,19 @@ docker run \
--rm \
--security-opt seccomp:unconfined \
-v "${WORKSPACE}":/home/jenkins/hifi \
- -e "RELEASE_NUMBER=${RELEASE_NUMBER}" \
- -e "RELEASE_TYPE=${RELEASE_TYPE}" \
- -e "ANDROID_BUILD_TARGET=assembleDebug" \
- -e "CMAKE_BACKTRACE_URL=${CMAKE_BACKTRACE_URL}" \
- -e "CMAKE_BACKTRACE_TOKEN=${CMAKE_BACKTRACE_TOKEN}" \
- -e "CMAKE_BACKTRACE_SYMBOLS_TOKEN=${CMAKE_BACKTRACE_SYMBOLS_TOKEN}" \
- -e "GA_TRACKING_ID=${GA_TRACKING_ID}" \
- -e "GIT_PR_COMMIT=${GIT_PR_COMMIT}" \
- -e "VERSION_CODE=${VERSION_CODE}" \
+ -e RELEASE_NUMBER \
+ -e RELEASE_TYPE \
+ -e ANDROID_BUILD_TARGET \
+ -e ANDROID_BUILD_DIR \
+ -e CMAKE_BACKTRACE_URL \
+ -e CMAKE_BACKTRACE_TOKEN \
+ -e CMAKE_BACKTRACE_SYMBOLS_TOKEN \
+ -e GA_TRACKING_ID \
+ -e OAUTH_CLIENT_SECRET \
+ -e OAUTH_CLIENT_ID \
+ -e OAUTH_REDIRECT_URI \
+ -e VERSION_CODE \
"${DOCKER_IMAGE_NAME}" \
sh -c "./build_android.sh"
+
+
diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp
index 5b72616e5f..cb2f0636b9 100644
--- a/assignment-client/src/avatars/AvatarMixer.cpp
+++ b/assignment-client/src/avatars/AvatarMixer.cpp
@@ -746,65 +746,27 @@ void AvatarMixer::sendStatsPacket() {
AvatarMixerSlaveStats aggregateStats;
- QJsonObject slavesObject;
- float secondsSinceLastStats = (float)(start - _lastStatsTime) / (float)USECS_PER_SECOND;
// gather stats
- int slaveNumber = 1;
_slavePool.each([&](AvatarMixerSlave& slave) {
- QJsonObject slaveObject;
AvatarMixerSlaveStats stats;
slave.harvestStats(stats);
- slaveObject["recevied_1_nodesProcessed"] = TIGHT_LOOP_STAT(stats.nodesProcessed);
- slaveObject["received_2_numPacketsReceived"] = TIGHT_LOOP_STAT(stats.packetsProcessed);
-
- slaveObject["sent_1_nodesBroadcastedTo"] = TIGHT_LOOP_STAT(stats.nodesBroadcastedTo);
- slaveObject["sent_2_numBytesSent"] = TIGHT_LOOP_STAT(stats.numBytesSent);
- slaveObject["sent_3_numPacketsSent"] = TIGHT_LOOP_STAT(stats.numPacketsSent);
- slaveObject["sent_4_numIdentityPackets"] = TIGHT_LOOP_STAT(stats.numIdentityPackets);
-
- float averageNodes = ((float)stats.nodesBroadcastedTo / (float)tightLoopFrames);
- float averageOutboundAvatarKbps = averageNodes ? ((stats.numBytesSent / secondsSinceLastStats) / BYTES_PER_KILOBIT) / averageNodes : 0.0f;
- slaveObject["sent_5_averageOutboundAvatarKbps"] = averageOutboundAvatarKbps;
-
- float averageOthersIncluded = averageNodes ? stats.numOthersIncluded / averageNodes : 0.0f;
- slaveObject["sent_6_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
-
- float averageOverBudgetAvatars = averageNodes ? stats.overBudgetAvatars / averageNodes : 0.0f;
- slaveObject["sent_7_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
-
- slaveObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(stats.processIncomingPacketsElapsedTime);
- slaveObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(stats.ignoreCalculationElapsedTime);
- slaveObject["timing_3_toByteArray"] = TIGHT_LOOP_STAT_UINT64(stats.toByteArrayElapsedTime);
- slaveObject["timing_4_avatarDataPacking"] = TIGHT_LOOP_STAT_UINT64(stats.avatarDataPackingElapsedTime);
- slaveObject["timing_5_packetSending"] = TIGHT_LOOP_STAT_UINT64(stats.packetSendingElapsedTime);
- slaveObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(stats.jobElapsedTime);
-
- slavesObject[QString::number(slaveNumber)] = slaveObject;
- slaveNumber++;
-
aggregateStats += stats;
});
QJsonObject slavesAggregatObject;
- slavesAggregatObject["recevied_1_nodesProcessed"] = TIGHT_LOOP_STAT(aggregateStats.nodesProcessed);
- slavesAggregatObject["received_2_numPacketsReceived"] = TIGHT_LOOP_STAT(aggregateStats.packetsProcessed);
+ slavesAggregatObject["received_1_nodesProcessed"] = TIGHT_LOOP_STAT(aggregateStats.nodesProcessed);
slavesAggregatObject["sent_1_nodesBroadcastedTo"] = TIGHT_LOOP_STAT(aggregateStats.nodesBroadcastedTo);
- slavesAggregatObject["sent_2_numBytesSent"] = TIGHT_LOOP_STAT(aggregateStats.numBytesSent);
- slavesAggregatObject["sent_3_numPacketsSent"] = TIGHT_LOOP_STAT(aggregateStats.numPacketsSent);
- slavesAggregatObject["sent_4_numIdentityPackets"] = TIGHT_LOOP_STAT(aggregateStats.numIdentityPackets);
float averageNodes = ((float)aggregateStats.nodesBroadcastedTo / (float)tightLoopFrames);
- float averageOutboundAvatarKbps = averageNodes ? ((aggregateStats.numBytesSent / secondsSinceLastStats) / BYTES_PER_KILOBIT) / averageNodes : 0.0f;
- slavesAggregatObject["sent_5_averageOutboundAvatarKbps"] = averageOutboundAvatarKbps;
float averageOthersIncluded = averageNodes ? aggregateStats.numOthersIncluded / averageNodes : 0.0f;
- slavesAggregatObject["sent_6_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
+ slavesAggregatObject["sent_2_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
float averageOverBudgetAvatars = averageNodes ? aggregateStats.overBudgetAvatars / averageNodes : 0.0f;
- slavesAggregatObject["sent_7_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
+ slavesAggregatObject["sent_3_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
slavesAggregatObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.processIncomingPacketsElapsedTime);
slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime);
@@ -814,7 +776,6 @@ void AvatarMixer::sendStatsPacket() {
slavesAggregatObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.jobElapsedTime);
statsObject["slaves_aggregate"] = slavesAggregatObject;
- statsObject["slaves_individual"] = slavesObject;
_handleViewFrustumPacketElapsedTime = 0;
_handleAvatarIdentityPacketElapsedTime = 0;
diff --git a/interface/resources/icons/checkmark-stroke.svg b/interface/resources/icons/checkmark-stroke.svg
new file mode 100644
index 0000000000..cc343c421b
--- /dev/null
+++ b/interface/resources/icons/checkmark-stroke.svg
@@ -0,0 +1,4 @@
+
diff --git a/interface/resources/icons/loader-snake-256-wf.gif b/interface/resources/icons/loader-snake-256-wf.gif
new file mode 100644
index 0000000000..c0d5eec1ef
Binary files /dev/null and b/interface/resources/icons/loader-snake-256-wf.gif differ
diff --git a/interface/resources/icons/loader-snake-256.gif b/interface/resources/icons/loader-snake-256.gif
new file mode 100644
index 0000000000..ebcbf54bd7
Binary files /dev/null and b/interface/resources/icons/loader-snake-256.gif differ
diff --git a/interface/resources/images/loader-snake-128.png b/interface/resources/images/loader-snake-128.png
new file mode 100644
index 0000000000..b8ee577664
Binary files /dev/null and b/interface/resources/images/loader-snake-128.png differ
diff --git a/interface/resources/qml/controlsUit/Button.qml b/interface/resources/qml/controlsUit/Button.qml
index c5c879a24c..3c5626e29e 100644
--- a/interface/resources/qml/controlsUit/Button.qml
+++ b/interface/resources/qml/controlsUit/Button.qml
@@ -32,6 +32,10 @@ Original.Button {
width: hifi.dimensions.buttonWidth
height: hifi.dimensions.controlLineHeight
+ property size implicitPadding: Qt.size(20, 16)
+ property int implicitWidth: buttonContentItem.implicitWidth + implicitPadding.width
+ property int implicitHeight: buttonContentItem.implicitHeight + implicitPadding.height
+
HifiConstants { id: hifi }
onHoveredChanged: {
@@ -94,6 +98,8 @@ Original.Button {
contentItem: Item {
id: buttonContentItem
+ implicitWidth: (buttonGlyph.visible ? buttonGlyph.implicitWidth : 0) + buttonText.implicitWidth
+ implicitHeight: buttonText.implicitHeight
TextMetrics {
id: buttonGlyphTextMetrics;
font: buttonGlyph.font;
diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml
index 57e4db062a..bfa37385a5 100644
--- a/interface/resources/qml/hifi/AvatarApp.qml
+++ b/interface/resources/qml/hifi/AvatarApp.qml
@@ -254,7 +254,8 @@ Rectangle {
onSaveClicked: function() {
var avatarSettings = {
dominantHand : settings.dominantHandIsLeft ? 'left' : 'right',
- collisionsEnabled : settings.avatarCollisionsOn,
+ collisionsEnabled : settings.environmentCollisionsOn,
+ otherAvatarsCollisionsEnabled : settings.otherAvatarsCollisionsOn,
animGraphOverrideUrl : settings.avatarAnimationOverrideJSON,
collisionSoundUrl : settings.avatarCollisionSoundUrl
};
diff --git a/interface/resources/qml/hifi/AvatarPackagerWindow.qml b/interface/resources/qml/hifi/AvatarPackagerWindow.qml
new file mode 100644
index 0000000000..82bcd3fa40
--- /dev/null
+++ b/interface/resources/qml/hifi/AvatarPackagerWindow.qml
@@ -0,0 +1,24 @@
+import QtQuick 2.6
+import "../stylesUit" 1.0
+import "../windows" as Windows
+import "avatarPackager" 1.0
+
+Windows.ScrollingWindow {
+ id: root
+ objectName: "AvatarPackager"
+ width: 480
+ height: 706
+ title: "Avatar Packager"
+ resizable: false
+ opacity: parent.opacity
+ destroyOnHidden: true
+ implicitWidth: 384; implicitHeight: 640
+ minSize: Qt.vector2d(480, 706)
+
+ HifiConstants { id: hifi }
+
+ AvatarPackagerApp {
+ height: pane.height
+ width: pane.width
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml
new file mode 100644
index 0000000000..b4293d5eee
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml
@@ -0,0 +1,396 @@
+import QtQuick 2.6
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+import QtQml.Models 2.1
+import QtGraphicalEffects 1.0
+import Hifi.AvatarPackager.AvatarProjectStatus 1.0
+import "../../controlsUit" 1.0 as HifiControls
+import "../../stylesUit" 1.0
+import "../../controls" 1.0
+import "../../dialogs"
+import "../avatarapp" 1.0 as AvatarApp
+
+Item {
+ id: windowContent
+
+ HifiConstants { id: hifi }
+
+ property alias desktopObject: avatarPackager.desktopObject
+
+ MouseArea {
+ anchors.fill: parent
+
+ onClicked: {
+ unfocusser.forceActiveFocus();
+ }
+ Item {
+ id: unfocusser
+ visible: false
+ }
+ }
+
+ InfoBox {
+ id: fileListPopup
+
+ title: "List of Files"
+
+ content: Rectangle {
+ id: fileList
+
+ color: "#404040"
+
+ anchors.fill: parent
+ anchors.topMargin: 10
+ anchors.bottomMargin: 10
+ anchors.leftMargin: 29
+ anchors.rightMargin: 29
+
+ clip: true
+
+ ListView {
+ anchors.fill: parent
+ model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles
+ delegate: Rectangle {
+ width: parent.width
+ height: fileText.implicitHeight + 8
+ color: "#404040"
+ RalewaySemiBold {
+ id: fileText
+ size: 16
+ elide: Text.ElideLeft
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: 16
+ anchors.rightMargin: 16
+ anchors.topMargin: 4
+ width: parent.width - 10
+ color: "white"
+ text: modelData
+ }
+ }
+ }
+ }
+ }
+
+ InfoBox {
+ id: errorPopup
+
+ property string errorMessage
+
+ boxWidth: 380
+ boxHeight: 293
+
+ content: RalewayRegular {
+
+ id: bodyMessage
+
+ anchors.fill: parent
+ anchors.bottomMargin: 10
+ anchors.leftMargin: 29
+ anchors.rightMargin: 29
+
+ size: 20
+ color: "white"
+ text: errorPopup.errorMessage
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+
+ function show(title, message) {
+ errorPopup.title = title;
+ errorMessage = message;
+ errorPopup.open();
+ }
+ }
+
+ Rectangle {
+ id: modalOverlay
+ anchors.fill: parent
+ z: 20
+ color: "#a15d5d5d"
+ visible: false
+
+ // This mouse area captures the cursor events while the modalOverlay is active
+ MouseArea {
+ anchors.fill: parent
+ propagateComposedEvents: false
+ hoverEnabled: true
+ }
+ }
+
+ AvatarApp.MessageBox {
+ id: popup
+ anchors.fill: parent
+ visible: false
+ closeOnClickOutside: true
+ }
+
+ Column {
+ id: avatarPackager
+ anchors.fill: parent
+ state: "main"
+ states: [
+ State {
+ name: AvatarPackagerState.main
+ PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); docsEnabled: true; backButtonVisible: false }
+ PropertyChanges { target: avatarPackagerMain; visible: true }
+ PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer }
+ },
+ State {
+ name: AvatarPackagerState.createProject
+ PropertyChanges { target: avatarPackagerHeader; title: qsTr("Create Project") }
+ PropertyChanges { target: createAvatarProject; visible: true }
+ PropertyChanges { target: avatarPackagerFooter; content: createAvatarProject.footer }
+ },
+ State {
+ name: AvatarPackagerState.project
+ PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; canRename: true }
+ PropertyChanges { target: avatarProject; visible: true }
+ PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer }
+ },
+ State {
+ name: AvatarPackagerState.projectUpload
+ PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; backButtonEnabled: false }
+ PropertyChanges { target: avatarUploader; visible: true }
+ PropertyChanges { target: avatarPackagerFooter; visible: false }
+ }
+ ]
+
+ property alias showModalOverlay: modalOverlay.visible
+
+ property var desktopObject: desktop
+
+ function openProject(path) {
+ let status = AvatarPackagerCore.openAvatarProject(path);
+ if (status !== AvatarProjectStatus.SUCCESS) {
+ displayErrorMessage(status);
+ return status;
+ }
+ avatarProject.reset();
+ avatarPackager.state = AvatarPackagerState.project;
+ return status;
+ }
+
+ function displayErrorMessage(status) {
+ if (status === AvatarProjectStatus.SUCCESS) {
+ return;
+ }
+ switch (status) {
+ case AvatarProjectStatus.ERROR_CREATE_PROJECT_NAME:
+ errorPopup.show("Project Folder Already Exists", "A folder with that name already exists at that location. Please choose a different project name or location.");
+ break;
+ case AvatarProjectStatus.ERROR_CREATE_CREATING_DIRECTORIES:
+ errorPopup.show("Project Folders Creation Error", "There was a problem creating the Avatar Project directory. Please check the project location and try again.");
+ break;
+ case AvatarProjectStatus.ERROR_CREATE_FIND_MODEL:
+ errorPopup.show("Cannot Find Model File", "There was a problem while trying to find the specified model file. Please verify that it exists at the specified location.");
+ break;
+ case AvatarProjectStatus.ERROR_CREATE_OPEN_MODEL:
+ errorPopup.show("Cannot Open Model File", "There was a problem while trying to open the specified model file.");
+ break;
+ case AvatarProjectStatus.ERROR_CREATE_READ_MODEL:
+ errorPopup.show("Error Read Model File", "There was a problem while trying to read the specified model file. Please check that the file is a valid FBX file and try again.");
+ break;
+ case AvatarProjectStatus.ERROR_CREATE_WRITE_FST:
+ errorPopup.show("Error Writing Project File", "There was a problem while trying to write the FST file.");
+ break;
+ case AvatarProjectStatus.ERROR_OPEN_INVALID_FILE_TYPE:
+ errorPopup.show("Invalid Project Path", "The avatar packager can only open FST files.");
+ break;
+ case AvatarProjectStatus.ERROR_OPEN_PROJECT_FOLDER:
+ errorPopup.show("Project Missing", "Project folder cannot be found. Please locate the folder and copy/move it to its original location.");
+ break;
+ case AvatarProjectStatus.ERROR_OPEN_FIND_FST:
+ errorPopup.show("File Missing", "We cannot find the project file (.fst) in the project folder. Please locate it and move it to the project folder.");
+ break;
+ case AvatarProjectStatus.ERROR_OPEN_OPEN_FST:
+ errorPopup.show("File Read Error", "We cannot read the project file (.fst).");
+ break;
+ case AvatarProjectStatus.ERROR_OPEN_FIND_MODEL:
+ errorPopup.show("File Missing", "We cannot find the avatar model file (.fbx) in the project folder. Please locate it and move it to the project folder.");
+ break;
+ default:
+ errorPopup.show("Error Message Missing", "Error message missing for status " + status);
+ }
+
+ }
+
+ function openDocs() {
+ Qt.openUrlExternally("https://docs.highfidelity.com/create/avatars/create-avatars#how-to-package-your-avatar");
+ }
+
+ AvatarPackagerHeader {
+ z: 100
+
+ id: avatarPackagerHeader
+ colorScheme: root.colorScheme
+ onBackButtonClicked: {
+ avatarPackager.state = AvatarPackagerState.main;
+ }
+ onDocsButtonClicked: {
+ avatarPackager.openDocs();
+ }
+ }
+
+ Item {
+ height: windowContent.height - avatarPackagerHeader.height - avatarPackagerFooter.height
+ width: windowContent.width
+
+ Rectangle {
+ anchors.fill: parent
+ color: "#404040"
+ }
+
+ AvatarProject {
+ id: avatarProject
+ colorScheme: root.colorScheme
+ anchors.fill: parent
+ }
+
+ AvatarProjectUpload {
+ id: avatarUploader
+ anchors.fill: parent
+ root: avatarProject
+ }
+
+ CreateAvatarProject {
+ id: createAvatarProject
+ colorScheme: root.colorScheme
+ anchors.fill: parent
+ }
+
+ Item {
+ id: avatarPackagerMain
+ visible: false
+ anchors.fill: parent
+
+ property var footer: Item {
+ anchors.fill: parent
+ anchors.rightMargin: 17
+ HifiControls.Button {
+ id: createProjectButton
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: openProjectButton.left
+ anchors.rightMargin: 22
+ height: 40
+ width: 134
+ text: qsTr("New Project")
+ colorScheme: root.colorScheme
+ onClicked: {
+ createAvatarProject.clearInputs();
+ avatarPackager.state = AvatarPackagerState.createProject;
+ }
+ }
+
+ HifiControls.Button {
+ id: openProjectButton
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: parent.right
+ height: 40
+ width: 133
+ text: qsTr("Open Project")
+ color: hifi.buttons.blue
+ colorScheme: root.colorScheme
+ onClicked: {
+ avatarPackager.showModalOverlay = true;
+
+ let browser = avatarPackager.desktopObject.fileDialog({
+ selectDirectory: false,
+ dir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH),
+ filter: "Avatar Project FST Files (*.fst)",
+ title: "Open Project (.fst)",
+ });
+
+ browser.canceled.connect(function() {
+ avatarPackager.showModalOverlay = false;
+ });
+
+ browser.selectedFile.connect(function(fileUrl) {
+ let fstFilePath = fileDialogHelper.urlToPath(fileUrl);
+ avatarPackager.showModalOverlay = false;
+ avatarPackager.openProject(fstFilePath);
+ });
+ }
+ }
+ }
+
+ Flow {
+ visible: AvatarPackagerCore.recentProjects.length === 0
+ anchors {
+ fill: parent
+ topMargin: 18
+ leftMargin: 16
+ rightMargin: 16
+ }
+ RalewayRegular {
+ size: 20
+ color: "white"
+ text: "Use a custom avatar of your choice."
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+ RalewayRegular {
+ size: 20
+ color: "white"
+ text: "Visit our docs to learn more about using the packager."
+ linkColor: "#00B4EF"
+ width: parent.width
+ wrapMode: Text.WordWrap
+ onLinkActivated: {
+ avatarPackager.openDocs();
+ }
+ }
+ }
+
+ Item {
+ anchors.fill: parent
+
+ visible: AvatarPackagerCore.recentProjects.length > 0
+
+ RalewayRegular {
+ id: recentProjectsText
+
+ color: 'white'
+
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.topMargin: 16
+ anchors.leftMargin: 16
+
+ size: 20
+
+ text: "Recent Projects"
+
+ onLinkActivated: fileListPopup.open()
+ }
+
+ Column {
+ anchors {
+ left: parent.left
+ right: parent.right
+ bottom: parent.bottom
+ top: recentProjectsText.bottom
+ topMargin: 16
+ leftMargin: 16
+ rightMargin: 16
+ }
+ spacing: 10
+
+ Repeater {
+ model: AvatarPackagerCore.recentProjects
+ AvatarProjectCard {
+ title: modelData.name
+ path: modelData.projectPath
+ onOpen: avatarPackager.openProject(modelData.path)
+ }
+ }
+ }
+ }
+ }
+ }
+ AvatarPackagerFooter {
+ id: avatarPackagerFooter
+ }
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml
new file mode 100644
index 0000000000..31e05672d2
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml
@@ -0,0 +1,41 @@
+import QtQuick 2.6
+
+import "../../controlsUit" 1.0 as HifiControls
+import "../../stylesUit" 1.0
+
+Rectangle {
+ id: avatarPackagerFooter
+
+ color: "#404040"
+ height: content === defaultContent ? 0 : 74
+ visible: content !== defaultContent
+ width: parent.width
+
+ property var content: Item { id: defaultContent }
+
+ children: [background, content]
+
+ property var background: Rectangle {
+ anchors.fill: parent
+ color: "#404040"
+
+ Rectangle {
+ id: topBorder1
+
+ anchors.top: parent.top
+
+ color: "#252525"
+ height: 1
+ width: parent.width
+ }
+ Rectangle {
+ id: topBorder2
+
+ anchors.top: topBorder1.bottom
+
+ color: "#575757"
+ height: 1
+ width: parent.width
+ }
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml
new file mode 100644
index 0000000000..25201bf81e
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml
@@ -0,0 +1,144 @@
+import QtQuick 2.6
+
+import "../../controlsUit" 1.0 as HifiControls
+import "../../stylesUit" 1.0
+import "../avatarapp" 1.0
+
+ShadowRectangle {
+ id: root
+
+ width: parent.width
+ height: 74
+ color: "#252525"
+
+ property string title: qsTr("Avatar Packager")
+ property alias docsEnabled: docs.visible
+ property bool backButtonVisible: true // If false, is not visible and does not take up space
+ property bool backButtonEnabled: true // If false, is not visible but does not affect space
+ property bool canRename: false
+ property int colorScheme
+
+ property color textColor: "white"
+ property color hoverTextColor: "gray"
+ property color pressedTextColor: "#6A6A6A"
+
+ signal backButtonClicked
+ signal docsButtonClicked
+
+ RalewayButton {
+ id: back
+
+ visible: backButtonEnabled && backButtonVisible
+
+ size: 28
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.leftMargin: 16
+
+ text: "◀"
+
+ onClicked: root.backButtonClicked()
+ }
+ Item {
+ id: titleArea
+
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.left: root.backButtonVisible ? back.right : parent.left
+ anchors.leftMargin: root.backButtonVisible ? 11 : 21
+ anchors.right: docs.left
+ states: [
+ State {
+ name: "renaming"
+ PropertyChanges { target: title; visible: false }
+ PropertyChanges { target: titleInputArea; visible: true }
+ }
+ ]
+
+ Item {
+ id: title
+ anchors.fill: parent
+
+ RalewaySemiBold {
+ id: titleNotRenameable
+
+ visible: !root.canRename
+
+ size: 28
+ anchors.fill: parent
+ text: root.title
+ color: "white"
+ }
+
+ RalewayButton {
+ id: titleRenameable
+
+ visible: root.canRename
+ enabled: root.canRename
+
+ size: 28
+ anchors.fill: parent
+ text: root.title
+
+ onClicked: {
+ if (!root.canRename || AvatarPackagerCore.currentAvatarProject === null) {
+ return;
+ }
+
+ titleArea.state = "renaming";
+ titleInput.text = AvatarPackagerCore.currentAvatarProject.name;
+ titleInput.selectAll();
+ titleInput.forceActiveFocus(Qt.MouseFocusReason);
+ }
+ }
+ }
+ Item {
+ id: titleInputArea
+ visible: false
+ anchors.fill: parent
+
+ HifiControls.TextField {
+ id: titleInput
+ anchors.fill: parent
+ text: ""
+ colorScheme: root.colorScheme
+ font.family: "Fira Sans"
+ font.pixelSize: 28
+ z: 200
+ onFocusChanged: {
+ if (titleArea.state === "renaming" && !focus) {
+ accepted();
+ }
+ }
+ Keys.onPressed: {
+ if (event.key === Qt.Key_Escape) {
+ titleArea.state = "";
+ }
+ }
+ onAccepted: {
+ if (acceptableInput) {
+ AvatarPackagerCore.currentAvatarProject.name = text;
+ }
+ titleArea.state = "";
+ }
+ }
+ }
+ }
+
+ RalewayButton {
+ id: docs
+ visible: false
+ size: 28
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ anchors.rightMargin: 16
+
+ text: qsTr("Docs")
+
+ onClicked: {
+ docsButtonClicked();
+ }
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml
new file mode 100644
index 0000000000..c81173a080
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml
@@ -0,0 +1,10 @@
+pragma Singleton
+import QtQuick 2.6
+
+Item {
+ id: singleton
+ readonly property string main: "main"
+ readonly property string project: "project"
+ readonly property string createProject: "createProject"
+ readonly property string projectUpload: "projectUpload"
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml
new file mode 100644
index 0000000000..85ef821a4a
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml
@@ -0,0 +1,336 @@
+import QtQuick 2.6
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+import QtGraphicalEffects 1.0
+
+import QtQuick.Controls 2.2 as Original
+
+import "../../controlsUit" 1.0 as HifiControls
+import "../../stylesUit" 1.0
+
+
+Item {
+ id: root
+
+ HifiConstants { id: hifi }
+
+ Style { id: style }
+
+ property int colorScheme
+ property var uploader: null
+
+ property bool hasSuccessfullyUploaded: true
+
+ visible: false
+ anchors.fill: parent
+ anchors.margins: 10
+
+ function reset() {
+ hasSuccessfullyUploaded = false;
+ uploader = null;
+ }
+
+ property var footer: Item {
+ anchors.fill: parent
+
+ Item {
+ id: uploadFooter
+
+ visible: !root.uploader || root.finished || root.uploader.state !== 4
+
+ anchors.fill: parent
+ anchors.rightMargin: 17
+
+ HifiControls.Button {
+ id: uploadButton
+
+ visible: AvatarPackagerCore.currentAvatarProject && !AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded
+ enabled: Account.loggedIn
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: parent.right
+
+ text: qsTr("Upload")
+ color: hifi.buttons.blue
+ colorScheme: root.colorScheme
+ width: 133
+ height: 40
+ onClicked: {
+ uploadNew();
+ }
+ }
+ HifiControls.Button {
+ id: updateButton
+
+ visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded
+ enabled: Account.loggedIn
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: parent.right
+
+ text: qsTr("Update")
+ color: hifi.buttons.blue
+ colorScheme: root.colorScheme
+ width: 134
+ height: 40
+ onClicked: {
+ showConfirmUploadPopup(uploadNew, uploadUpdate);
+ }
+ }
+ Item {
+ anchors.fill: parent
+ visible: root.hasSuccessfullyUploaded
+
+ HifiControls.Button {
+ enabled: Account.loggedIn
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: viewInInventoryButton.left
+ anchors.rightMargin: 16
+
+ text: qsTr("Update")
+ color: hifi.buttons.white
+ colorScheme: root.colorScheme
+ width: 134
+ height: 40
+ onClicked: {
+ showConfirmUploadPopup(uploadNew, uploadUpdate);
+ }
+ }
+ HifiControls.Button {
+ id: viewInInventoryButton
+
+ enabled: Account.loggedIn
+
+ width: 168
+ height: 40
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: parent.right
+
+ text: qsTr("View in Inventory")
+ color: hifi.buttons.blue
+ colorScheme: root.colorScheme
+
+ onClicked: AvatarPackagerCore.currentAvatarProject.openInInventory()
+ }
+ }
+ }
+
+ Rectangle {
+ id: uploadingItemFooter
+
+ anchors.fill: parent
+ anchors.topMargin: 1
+ visible: !!root.uploader && !root.finished && root.uploader.state === 4
+
+ color: "#00B4EF"
+
+ LoadingCircle {
+ id: runningImage
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.leftMargin: 16
+
+ width: 28
+ height: 28
+
+ white: true
+ }
+ RalewayRegular {
+ id: stepText
+
+ size: 20
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: runningImage.right
+ anchors.leftMargin: 16
+
+ text: "Adding item to Inventory"
+ color: "white"
+ }
+ }
+ }
+
+ function uploadNew() {
+ upload(false);
+ }
+ function uploadUpdate() {
+ upload(true);
+ }
+
+ Connections {
+ target: root.uploader
+ onStateChanged: {
+ root.hasSuccessfullyUploaded = newState >= 4;
+ }
+ }
+
+ function upload(updateExisting) {
+ root.uploader = AvatarPackagerCore.currentAvatarProject.upload(updateExisting);
+ console.log("uploader: "+ root.uploader);
+ root.uploader.send();
+ avatarPackager.state = AvatarPackagerState.projectUpload;
+ }
+
+ function showConfirmUploadPopup() {
+ popup.titleText = 'Overwrite Avatar';
+ popup.bodyText = 'You have previously uploaded the avatar file from this project.' +
+ ' This will overwrite that avatar and you won’t be able to access the older version.';
+
+ popup.button1text = 'CREATE NEW';
+ popup.button2text = 'OVERWRITE';
+
+ popup.onButton2Clicked = function() {
+ popup.close();
+ uploadUpdate();
+ };
+ popup.onButton1Clicked = function() {
+ popup.close();
+ showConfirmCreateNewPopup();
+ };
+
+ popup.open();
+ }
+
+ function showConfirmCreateNewPopup(confirmCallback) {
+ popup.titleText = 'Create New';
+ popup.bodyText = 'This will upload your current files with the same avatar name.' +
+ ' You will lose the ability to update the previously uploaded avatar. Are you sure you want to continue?';
+
+ popup.button1text = 'CANCEL';
+ popup.button2text = 'CONFIRM';
+
+ popup.onButton1Clicked = function() {
+ popup.close()
+ };
+ popup.onButton2Clicked = function() {
+ popup.close();
+ uploadNew();
+ };
+
+ popup.open();
+ }
+
+ RalewayRegular {
+ id: infoMessage
+
+ states: [
+ State {
+ when: root.hasSuccessfullyUploaded
+ name: "upload-success"
+ PropertyChanges {
+ target: infoMessage
+ text: "Your avatar has been successfully uploaded to our servers. Make changes to your avatar by editing and uploading the project files."
+ }
+ },
+ State {
+ name: "has-previous-success"
+ when: !!AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID
+ PropertyChanges {
+ target: infoMessage
+ text: "Click \"Update\" to overwrite the hosted files and update the avatar in your inventory. You will have to “Wear” the avatar again to see changes."
+ }
+ }
+ ]
+
+ color: 'white'
+ size: 20
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+
+ anchors.bottomMargin: 24
+
+ wrapMode: Text.Wrap
+
+ text: "You can upload your files to our servers to always access them, and to make your avatar visible to other users."
+ }
+
+ HifiControls.Button {
+ id: openFolderButton
+
+ visible: false
+ width: parent.width
+ anchors.top: infoMessage.bottom
+ anchors.topMargin: 10
+ text: qsTr("Open Project Folder")
+ colorScheme: root.colorScheme
+ height: 30
+ onClicked: {
+ fileDialogHelper.openDirectory(fileDialogHelper.pathToUrl(AvatarPackagerCore.currentAvatarProject.projectFolderPath));
+ }
+ }
+
+ RalewayRegular {
+ id: showFilesText
+
+ color: 'white'
+ linkColor: style.colors.blueHighlight
+
+ visible: AvatarPackagerCore.currentAvatarProject !== null
+
+ anchors.bottom: loginRequiredMessage.top
+ anchors.bottomMargin: 10
+
+ size: 20
+
+ text: AvatarPackagerCore.currentAvatarProject ? AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. See list" : ""
+
+ onLinkActivated: fileListPopup.open()
+ }
+
+ Rectangle {
+ id: loginRequiredMessage
+
+ visible: !Account.loggedIn
+ height: !Account.loggedIn ? loginRequiredTextRow.height + 20 : 0
+
+ anchors {
+ bottom: parent.bottom
+ left: parent.left
+ right: parent.right
+ }
+
+ color: "#FFD6AD"
+
+ border.color: "#F39622"
+ border.width: 2
+ radius: 2
+
+ Item {
+ id: loginRequiredTextRow
+
+ height: Math.max(loginWarningGlyph.implicitHeight, loginWarningText.implicitHeight)
+ anchors.fill: parent
+ anchors.margins: 10
+
+ HiFiGlyphs {
+ id: loginWarningGlyph
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+
+ width: implicitWidth
+
+ size: 48
+ text: "+"
+ color: "black"
+ }
+ RalewayRegular {
+ id: loginWarningText
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: 16
+ anchors.left: loginWarningGlyph.right
+ anchors.right: parent.right
+
+ text: "Please login to upload your avatar to High Fidelity hosting."
+ size: 18
+ wrapMode: Text.Wrap
+ }
+ }
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml
new file mode 100644
index 0000000000..25222c814c
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml
@@ -0,0 +1,102 @@
+import QtQuick 2.0
+import QtGraphicalEffects 1.0
+
+import "../../controlsUit" 1.0 as HifiControls
+import "../../stylesUit" 1.0
+
+
+Item {
+ id: projectCard
+ height: 80
+ width: parent.width
+
+ property alias title: title.text
+ property alias path: path.text
+
+ property color textColor: "#E3E3E3"
+ property color hoverTextColor: "#121212"
+ property color pressedTextColor: "#121212"
+
+ property color backgroundColor: "#121212"
+ property color hoverBackgroundColor: "#E3E3E3"
+ property color pressedBackgroundColor: "#6A6A6A"
+
+ signal open
+
+ state: mouseArea.pressed ? "pressed" : (mouseArea.containsMouse ? "hover" : "normal")
+ states: [
+ State {
+ name: "normal"
+ PropertyChanges { target: background; color: backgroundColor }
+ PropertyChanges { target: title; color: textColor }
+ PropertyChanges { target: path; color: textColor }
+ },
+ State {
+ name: "hover"
+ PropertyChanges { target: background; color: hoverBackgroundColor }
+ PropertyChanges { target: title; color: hoverTextColor }
+ PropertyChanges { target: path; color: hoverTextColor }
+ },
+ State {
+ name: "pressed"
+ PropertyChanges { target: background; color: pressedBackgroundColor }
+ PropertyChanges { target: title; color: pressedTextColor }
+ PropertyChanges { target: path; color: pressedTextColor }
+ }
+ ]
+
+ Rectangle {
+ id: background
+ width: parent.width
+ height: parent.height
+ color: "#121212"
+ radius: 4
+
+ RalewayBold {
+ id: title
+ elide: "ElideRight"
+ anchors {
+ top: parent.top
+ topMargin: 13
+ left: parent.left
+ leftMargin: 16
+ right: parent.right
+ rightMargin: 16
+ }
+ text: "
"
+ size: 24
+ }
+
+ RalewayRegular {
+ id: path
+ anchors {
+ top: title.bottom
+ left: parent.left
+ leftMargin: 32
+ right: background.right
+ rightMargin: 16
+ }
+ elide: "ElideLeft"
+ horizontalAlignment: Text.AlignRight
+ text: ""
+ size: 20
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: open()
+ }
+ }
+
+ DropShadow {
+ id: shadow
+ anchors.fill: background
+ radius: 4
+ horizontalOffset: 0
+ verticalOffset: 4
+ color: Qt.rgba(0, 0, 0, 0.25)
+ source: background
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml
new file mode 100644
index 0000000000..68f465f514
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml
@@ -0,0 +1,202 @@
+import QtQuick 2.6
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+import QtGraphicalEffects 1.0
+
+import QtQuick.Controls 2.2 as Original
+
+import "../../controlsUit" 1.0 as HifiControls
+import "../../stylesUit" 1.0
+
+Item {
+ id: uploadingScreen
+
+ property var root: undefined
+ visible: false
+ anchors.fill: parent
+
+ Timer {
+ id: backToProjectTimer
+ interval: 2000
+ running: false
+ repeat: false
+ onTriggered: {
+ if (avatarPackager.state === AvatarPackagerState.projectUpload) {
+ avatarPackager.state = AvatarPackagerState.project;
+ }
+ }
+ }
+
+ function stateChangedCallback(newState) {
+ if (newState >= 4) {
+ root.uploader.stateChanged.disconnect(stateChangedCallback);
+ backToProjectTimer.start();
+ }
+ }
+
+ onVisibleChanged: {
+ if (visible) {
+ root.uploader.stateChanged.connect(stateChangedCallback);
+ root.uploader.finishedChanged.connect(function() {
+ if (root.uploader.error === 0) {
+ backToProjectTimer.start();
+ }
+ });
+ }
+ }
+
+ Item {
+ id: uploadStatus
+
+ anchors.fill: parent
+
+ Item {
+ id: statusItem
+
+ width: parent.width
+ height: 256
+
+ states: [
+ State {
+ name: "success"
+ when: root.uploader.state >= 4 && root.uploader.error === 0
+ PropertyChanges { target: uploadSpinner; visible: false }
+ PropertyChanges { target: errorIcon; visible: false }
+ PropertyChanges { target: successIcon; visible: true }
+ },
+ State {
+ name: "error"
+ when: root.uploader.finished && root.uploader.error !== 0
+ PropertyChanges { target: uploadSpinner; visible: false }
+ PropertyChanges { target: errorIcon; visible: true }
+ PropertyChanges { target: successIcon; visible: false }
+ PropertyChanges { target: errorFooter; visible: true }
+ PropertyChanges { target: errorMessage; visible: true }
+ }
+ ]
+
+ AnimatedImage {
+ id: uploadSpinner
+
+ visible: true
+
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ verticalCenter: parent.verticalCenter
+ }
+
+ width: 164
+ height: 164
+
+ source: "../../../icons/loader-snake-256.gif"
+ playing: true
+ }
+
+ HiFiGlyphs {
+ id: errorIcon
+
+ visible: false
+
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ verticalCenter: parent.verticalCenter
+ }
+
+ size: 315
+ text: "+"
+ color: "#EA4C5F"
+ }
+
+ Image {
+ id: successIcon
+
+ visible: false
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ width: 148
+ height: 148
+
+ source: "../../../icons/checkmark-stroke.svg"
+ }
+ }
+
+ Item {
+ id: statusRows
+
+ anchors.top: statusItem.bottom
+ anchors.left: parent.left
+ anchors.leftMargin: 12
+
+ AvatarUploadStatusItem {
+ id: statusCategories
+ uploader: root.uploader
+ text: "Retrieving categories"
+
+ uploaderState: 1
+ }
+ AvatarUploadStatusItem {
+ id: statusUploading
+ uploader: root.uploader
+ anchors.top: statusCategories.bottom
+ text: "Uploading data"
+
+ uploaderState: 2
+ }
+ AvatarUploadStatusItem {
+ id: statusResponse
+ uploader: root.uploader
+ anchors.top: statusUploading.bottom
+ text: "Waiting for response"
+
+ uploaderState: 3
+ }
+ }
+
+ RalewayRegular {
+ id: errorMessage
+
+ visible: false
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: errorFooter.top
+ anchors.leftMargin: 16
+ anchors.rightMargin: 16
+ anchors.bottomMargin: 32
+
+ size: 28
+ wrapMode: Text.Wrap
+ color: "white"
+ text: "We couldn't upload your avatar at this time. Please try again later."
+ }
+
+ AvatarPackagerFooter {
+ id: errorFooter
+
+ anchors.bottom: parent.bottom
+ visible: false
+
+ content: Item {
+ anchors.fill: parent
+ anchors.rightMargin: 17
+ HifiControls.Button {
+ id: backButton
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: parent.right
+
+ text: qsTr("Back")
+ color: hifi.buttons.blue
+ colorScheme: root.colorScheme
+ width: 133
+ height: 40
+ onClicked: {
+ avatarPackager.state = AvatarPackagerState.project;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml
new file mode 100644
index 0000000000..70a0ea0672
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml
@@ -0,0 +1,96 @@
+import QtQuick 2.6
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+
+import "../../controlsUit" 1.0 as HifiControls
+import "../../stylesUit" 1.0
+
+Item {
+ id: root
+
+ height: 48
+
+ property string text: "NO STEP TEXT"
+ property int uploaderState;
+ property var uploader;
+
+ states: [
+ State {
+ name: ""
+ when: root.uploader === null
+ },
+ State {
+ name: "success"
+ when: root.uploader.state > uploaderState
+ PropertyChanges { target: stepText; color: "white" }
+ PropertyChanges { target: successGlyph; visible: true }
+ },
+ State {
+ name: "fail"
+ when: root.uploader.error !== 0
+ PropertyChanges { target: stepText; color: "#EA4C5F" }
+ PropertyChanges { target: failGlyph; visible: true }
+ },
+ State {
+ name: "running"
+ when: root.uploader.state === uploaderState
+ PropertyChanges { target: stepText; color: "white" }
+ PropertyChanges { target: runningImage; visible: true; playing: true }
+ }
+ ]
+
+ Item {
+ id: statusItem
+
+ width: 48
+ height: parent.height
+
+ LoadingCircle {
+ id: runningImage
+
+ visible: false
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ width: 32
+ height: 32
+ }
+ Image {
+ id: successGlyph
+
+ visible: false
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ width: 30
+ height: 30
+
+ source: "../../../icons/checkmark-stroke.svg"
+ }
+ HiFiGlyphs {
+ id: failGlyph
+
+ visible: false
+
+ width: implicitWidth
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ size: 48
+ text: "+"
+ color: "#EA4C5F"
+ }
+ }
+ RalewayRegular {
+ id: stepText
+
+ anchors.left: statusItem.right
+ anchors.verticalCenter: parent.verticalCenter
+
+ text: root.text
+ size: 28
+ color: "#777777"
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml b/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml
new file mode 100644
index 0000000000..0f7b201f72
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml
@@ -0,0 +1,63 @@
+import QtQuick 2.6
+
+import "../../controlsUit" 1.0 as HifiControls
+import "../../stylesUit" 1.0
+
+import TabletScriptingInterface 1.0
+
+Item {
+ id: root
+
+ readonly property bool pressed: mouseArea.state == "pressed"
+ readonly property bool hovered: mouseArea.state == "hovering"
+
+ signal clicked()
+
+ MouseArea {
+ id: mouseArea
+
+ anchors.fill: parent
+
+ hoverEnabled: true
+
+ onClicked: {
+ root.focus = true
+ Tablet.playSound(TabletEnums.ButtonClick);
+ root.clicked();
+ }
+
+ property string lastState: ""
+
+ states: [
+ State {
+ name: ""
+ StateChangeScript {
+ script: {
+ mouseArea.lastState = mouseArea.state;
+ }
+ }
+ },
+ State {
+ name: "pressed"
+ when: mouseArea.containsMouse && mouseArea.pressed
+ StateChangeScript {
+ script: {
+ mouseArea.lastState = mouseArea.state;
+ }
+ }
+ },
+ State {
+ name: "hovering"
+ when: mouseArea.containsMouse
+ StateChangeScript {
+ script: {
+ if (mouseArea.lastState == "") {
+ Tablet.playSound(TabletEnums.ButtonHover);
+ }
+ mouseArea.lastState = mouseArea.state;
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml
new file mode 100644
index 0000000000..c299417c27
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml
@@ -0,0 +1,135 @@
+import QtQuick 2.6
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+
+import Hifi.AvatarPackager.AvatarProjectStatus 1.0
+
+import "../../controlsUit" 1.0 as HifiControls
+import "../../stylesUit" 1.0
+
+Item {
+ id: root
+
+ HifiConstants { id: hifi }
+
+ property int colorScheme
+
+ property var footer: Item {
+ anchors.fill: parent
+ anchors.rightMargin: 17
+ HifiControls.Button {
+ id: createButton
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: parent.right
+ height: 30
+ width: 133
+ text: qsTr("Create")
+ enabled: false
+ onClicked: {
+ let status = AvatarPackagerCore.createAvatarProject(projectLocation.text, name.text, avatarModel.text, textureFolder.text);
+ if (status !== AvatarProjectStatus.SUCCESS) {
+ avatarPackager.displayErrorMessage(status);
+ return;
+ }
+ avatarProject.reset();
+ avatarPackager.state = AvatarPackagerState.project;
+ }
+ }
+ }
+
+ visible: false
+ anchors.fill: parent
+ height: parent.height
+ width: parent.width
+
+ function clearInputs() {
+ name.text = projectLocation.text = avatarModel.text = textureFolder.text = "";
+ }
+
+ function checkErrors() {
+ let newErrorMessageText = "";
+
+ let projectName = name.text;
+ let projectFolder = projectLocation.text;
+
+ let hasProjectNameError = projectName !== "" && projectFolder !== "" && !AvatarPackagerCore.isValidNewProjectName(projectFolder, projectName);
+
+ if (hasProjectNameError) {
+ newErrorMessageText = "A folder with that name already exists at that location. Please choose a different project name or location.";
+ }
+
+ name.error = projectLocation.error = hasProjectNameError;
+ errorMessage.text = newErrorMessageText;
+ createButton.enabled = newErrorMessageText === "" && requiredFieldsFilledIn();
+ }
+
+ function requiredFieldsFilledIn() {
+ return name.text !== "" && projectLocation.text !== "" && avatarModel.text !== "";
+ }
+
+ RalewayRegular {
+ id: errorMessage
+ visible: text !== ""
+ text: ""
+ color: "#EA4C5F"
+ wrapMode: Text.WordWrap
+ size: 20
+ anchors {
+ left: parent.left
+ right: parent.right
+ }
+ }
+
+ Column {
+ id: createAvatarColumns
+ anchors.top: errorMessage.visible ? errorMessage.bottom : parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: 10
+
+ spacing: 17
+
+ property string defaultFileBrowserPath: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH)
+
+ ProjectInputControl {
+ id: name
+ label: "Name"
+ colorScheme: root.colorScheme
+ onTextChanged: checkErrors()
+ }
+
+ ProjectInputControl {
+ id: projectLocation
+ label: "Specify Project Location"
+ colorScheme: root.colorScheme
+ browseEnabled: true
+ browseFolder: true
+ browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
+ browseTitle: "Project Location"
+ onTextChanged: checkErrors()
+ }
+
+ ProjectInputControl {
+ id: avatarModel
+ label: "Specify Avatar Model (.fbx)"
+ colorScheme: root.colorScheme
+ browseEnabled: true
+ browseFolder: false
+ browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
+ browseFilter: "Avatar Model File (*.fbx)"
+ browseTitle: "Open Avatar Model (.fbx)"
+ onTextChanged: checkErrors()
+ }
+
+ ProjectInputControl {
+ id: textureFolder
+ label: "Specify Texture Folder - Optional"
+ colorScheme: root.colorScheme
+ browseEnabled: true
+ browseFolder: true
+ browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
+ browseTitle: "Texture Folder"
+ onTextChanged: checkErrors()
+ }
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/InfoBox.qml b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml
new file mode 100644
index 0000000000..e33e427af0
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml
@@ -0,0 +1,120 @@
+import Hifi 1.0 as Hifi
+import QtQuick 2.5
+import stylesUit 1.0
+import controlsUit 1.0 as HifiControlsUit
+import "../../controls" as HifiControls
+
+Rectangle {
+ id: root
+ visible: false
+ color: Qt.rgba(.34, .34, .34, 0.6)
+ z: 999;
+
+ anchors.fill: parent
+
+ property alias title: titleText.text
+ property alias content: loader.sourceComponent
+
+ property bool closeOnClickOutside: false;
+
+ property alias boxWidth: mainContainer.width
+ property alias boxHeight: mainContainer.height
+
+ onVisibleChanged: {
+ if (visible) {
+ focus = true;
+ }
+ }
+
+ function open() {
+ visible = true;
+ }
+
+ function close() {
+ visible = false;
+ }
+
+ HifiConstants {
+ id: hifi
+ }
+
+ // This object is always used in a popup.
+ // This MouseArea is used to prevent a user from being
+ // able to click on a button/mouseArea underneath the popup.
+ MouseArea {
+ anchors.fill: parent;
+ propagateComposedEvents: false;
+ hoverEnabled: true;
+ onClicked: {
+ if (closeOnClickOutside) {
+ root.close()
+ }
+ }
+ }
+
+ Rectangle {
+ id: mainContainer
+
+ width: Math.max(parent.width * 0.8, 400)
+ height: parent.height * 0.6
+
+ MouseArea {
+ anchors.fill: parent
+ propagateComposedEvents: false
+ hoverEnabled: true
+ onClicked: function(ev) {
+ ev.accepted = true;
+ }
+ }
+
+ anchors.centerIn: parent
+
+ color: "#252525"
+
+ // TextStyle1
+ RalewaySemiBold {
+ id: titleText
+ size: 24
+ color: "white"
+
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: 30
+
+ text: "Title not defined"
+ }
+
+ Item {
+ anchors.topMargin: 10
+ anchors.top: titleText.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: button.top
+
+ Loader {
+ id: loader
+ anchors.fill: parent
+ }
+ }
+
+ Item {
+ id: button
+
+ height: 40
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 12
+
+ HifiControlsUit.Button {
+ anchors.centerIn: parent
+
+ text: "CLOSE"
+ onClicked: close()
+
+ color: hifi.buttons.noneBorderlessWhite;
+ colorScheme: hifi.colorSchemes.dark;
+ }
+ }
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml
new file mode 100644
index 0000000000..a1fac72ae4
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml
@@ -0,0 +1,16 @@
+import QtQuick 2.6
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+import QtGraphicalEffects 1.0
+
+AnimatedImage {
+ id: root
+
+ width: 128
+ height: 128
+
+ property bool white: false
+
+ source: white ? "../../../icons/loader-snake-256-wf.gif" : "../../../icons/loader-snake-256.gif"
+ playing: true
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml
new file mode 100644
index 0000000000..f0a3aac8a7
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml
@@ -0,0 +1,78 @@
+import QtQuick 2.6
+
+import "../../controlsUit" 1.0 as HifiControls
+import "../../stylesUit" 1.0
+
+Column {
+ id: control
+
+ anchors.left: parent.left
+ anchors.leftMargin: 21
+ anchors.right: parent.right
+ anchors.rightMargin: 16
+
+ height: 75
+
+ spacing: 4
+
+ property alias label: label.text
+ property alias browseEnabled: browseButton.visible
+ property bool browseFolder: false
+ property string browseFilter: "All Files (*.*)"
+ property string browseTitle: "Open file"
+ property string browseDir: ""
+ property alias text: input.text
+ property alias error: input.error
+
+ property int colorScheme
+
+ Row {
+ RalewaySemiBold {
+ id: label
+ size: 20
+ font.weight: Font.Medium
+ text: ""
+ color: "white"
+ }
+ }
+ Row {
+ width: control.width
+ spacing: 16
+ height: 40
+ HifiControls.TextField {
+ id: input
+ colorScheme: control.colorScheme
+ font.family: "Fira Sans"
+ font.pixelSize: 18
+ height: parent.height
+ width: browseButton.visible ? parent.width - browseButton.width - parent.spacing : parent.width
+ }
+
+ HifiControls.Button {
+ id: browseButton
+ visible: false
+ height: parent.height
+ width: 133
+ text: qsTr("Browse")
+ colorScheme: root.colorScheme
+ onClicked: {
+ avatarPackager.showModalOverlay = true;
+ let browser = avatarPackager.desktopObject.fileDialog({
+ selectDirectory: browseFolder,
+ dir: browseDir,
+ filter: browseFilter,
+ title: browseTitle,
+ });
+
+ browser.canceled.connect(function() {
+ avatarPackager.showModalOverlay = false;
+ });
+
+ browser.selectedFile.connect(function(fileUrl) {
+ input.text = fileDialogHelper.urlToPath(fileUrl);
+ avatarPackager.showModalOverlay = false;
+ });
+ }
+ }
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml
new file mode 100644
index 0000000000..86742ddccd
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml
@@ -0,0 +1,26 @@
+import QtQuick 2.6
+
+import "../../controlsUit" 1.0 as HifiControls
+import "../../stylesUit" 1.0
+
+import TabletScriptingInterface 1.0
+
+RalewaySemiBold {
+ id: root
+
+ property color idleColor: "white"
+ property color hoverColor: "#AFAFAF"
+ property color pressedColor: "#575757"
+
+ color: clickable.hovered ? root.hoverColor : (clickable.pressed ? root.pressedColor : root.idleColor)
+
+ signal clicked()
+
+ ClickableArea {
+ id: clickable
+
+ anchors.fill: root
+
+ onClicked: root.clicked()
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/Style.qml b/interface/resources/qml/hifi/avatarPackager/Style.qml
new file mode 100644
index 0000000000..a1dcc8f0c1
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/Style.qml
@@ -0,0 +1,20 @@
+import QtQuick 2.5
+import QtQuick.Window 2.2
+
+import "../../stylesUit" 1.0
+
+QtObject {
+ readonly property QtObject colors: QtObject {
+ readonly property color lightGrayBackground: "#f2f2f2"
+ readonly property color black: "#000000"
+ readonly property color white: "#ffffff"
+ readonly property color blueHighlight: "#00b4ef"
+ readonly property color inputFieldBackground: "#d4d4d4"
+ readonly property color yellowishOrange: "#ffb017"
+ readonly property color blueAccent: "#0093c5"
+ readonly property color greenHighlight: "#1fc6a6"
+ readonly property color lightGray: "#afafaf"
+ readonly property color redHighlight: "#ea4c5f"
+ readonly property color orangeAccent: "#ff6309"
+ }
+}
diff --git a/interface/resources/qml/hifi/avatarPackager/qmldir b/interface/resources/qml/hifi/avatarPackager/qmldir
new file mode 100644
index 0000000000..4204b6d89f
--- /dev/null
+++ b/interface/resources/qml/hifi/avatarPackager/qmldir
@@ -0,0 +1,2 @@
+module AvatarPackager
+singleton AvatarPackagerState 1.0 AvatarPackagerState.qml
diff --git a/interface/resources/qml/hifi/avatarapp/MessageBox.qml b/interface/resources/qml/hifi/avatarapp/MessageBox.qml
index 1834364fe4..88f7f888cb 100644
--- a/interface/resources/qml/hifi/avatarapp/MessageBox.qml
+++ b/interface/resources/qml/hifi/avatarapp/MessageBox.qml
@@ -23,6 +23,8 @@ Rectangle {
property string button2color: hifi.buttons.blue;
property string button2text: ''
+ property bool closeOnClickOutside: false;
+
property var onButton2Clicked;
property var onButton1Clicked;
property var onLinkClicked;
@@ -56,6 +58,11 @@ Rectangle {
anchors.fill: parent;
propagateComposedEvents: false;
hoverEnabled: true;
+ onClicked: {
+ if (closeOnClickOutside) {
+ root.close()
+ }
+ }
}
Rectangle {
@@ -68,6 +75,15 @@ Rectangle {
console.debug('mainContainer: height = ', height)
}
+ MouseArea {
+ anchors.fill: parent;
+ propagateComposedEvents: false;
+ hoverEnabled: true;
+ onClicked: function(ev) {
+ ev.accepted = true;
+ }
+ }
+
anchors.centerIn: parent
color: "white"
diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml
index cd892c17b1..d212186c5e 100644
--- a/interface/resources/qml/hifi/avatarapp/Settings.qml
+++ b/interface/resources/qml/hifi/avatarapp/Settings.qml
@@ -35,7 +35,8 @@ Rectangle {
property real scaleValue: scaleSlider.value / 10
property alias dominantHandIsLeft: leftHandRadioButton.checked
- property alias avatarCollisionsOn: collisionsEnabledRadiobutton.checked
+ property alias otherAvatarsCollisionsOn: otherAvatarsCollisionsEnabledCheckBox.checked
+ property alias environmentCollisionsOn: environmentCollisionsEnabledCheckBox.checked
property alias avatarAnimationOverrideJSON: avatarAnimationUrlInputText.text
property alias avatarAnimationJSON: avatarAnimationUrlInputText.placeholderText
property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text
@@ -54,11 +55,11 @@ Rectangle {
} else {
rightHandRadioButton.checked = true;
}
-
+ if (settings.otherAvatarsCollisionsEnabled) {
+ otherAvatarsCollisionsEnabledCheckBox.checked = true;
+ }
if (settings.collisionsEnabled) {
- collisionsEnabledRadiobutton.checked = true;
- } else {
- collisionsDisabledRadioButton.checked = true;
+ environmentCollisionsEnabledCheckBox.checked = true;
}
avatarAnimationJSON = settings.animGraphUrl;
@@ -255,55 +256,43 @@ Rectangle {
text: "Right"
boxSize: 20
}
+
+ HifiConstants {
+ id: hifi
+ }
// TextStyle9
RalewaySemiBold {
size: 17;
Layout.row: 1
Layout.column: 0
-
- text: "Avatar Collisions"
+ text: "Avatar collides with other avatars"
}
- ButtonGroup {
- id: onOff
- }
-
- HifiControlsUit.RadioButton {
- id: collisionsEnabledRadiobutton
-
- Layout.row: 1
- Layout.column: 1
- Layout.leftMargin: -40
- ButtonGroup.group: onOff
-
- colorScheme: hifi.colorSchemes.light
- fontSize: 17
- letterSpacing: 1.4
- checked: true
-
- text: "ON"
- boxSize: 20
- }
-
- HifiConstants {
- id: hifi
- }
-
- HifiControlsUit.RadioButton {
- id: collisionsDisabledRadioButton
-
+ HifiControlsUit.CheckBox {
+ id: otherAvatarsCollisionsEnabledCheckBox;
+ boxSize: 20;
Layout.row: 1
Layout.column: 2
- Layout.rightMargin: 20
-
- ButtonGroup.group: onOff
+ Layout.leftMargin: 60
colorScheme: hifi.colorSchemes.light
- fontSize: 17
- letterSpacing: 1.4
+ }
- text: "OFF"
- boxSize: 20
+ // TextStyle9
+ RalewaySemiBold {
+ size: 17;
+ Layout.row: 2
+ Layout.column: 0
+ text: "Avatar collides with environment"
+ }
+
+ HifiControlsUit.CheckBox {
+ id: environmentCollisionsEnabledCheckBox;
+ boxSize: 20;
+ Layout.row: 2
+ Layout.column: 2
+ Layout.leftMargin: 60
+ colorScheme: hifi.colorSchemes.light
}
}
diff --git a/interface/resources/qml/hifi/tablet/AvatarPackager.qml b/interface/resources/qml/hifi/tablet/AvatarPackager.qml
new file mode 100644
index 0000000000..c1c234dd73
--- /dev/null
+++ b/interface/resources/qml/hifi/tablet/AvatarPackager.qml
@@ -0,0 +1,15 @@
+import QtQuick 2.0
+import "../avatarPackager" 1.0
+
+Item {
+ id: root
+ width: 480
+ height: 706
+
+ AvatarPackagerApp {
+ width: parent.width
+ height: parent.height
+
+ desktopObject: tabletRoot
+ }
+}
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index b6b4e8e2a1..d306c77cce 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -158,6 +158,7 @@
#include "audio/AudioScope.h"
#include "avatar/AvatarManager.h"
#include "avatar/MyHead.h"
+#include "avatar/AvatarPackager.h"
#include "CrashRecoveryHandler.h"
#include "CrashHandler.h"
#include "devices/DdeFaceTracker.h"
@@ -922,6 +923,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
DependencyManager::set();
DependencyManager::set();
DependencyManager::set();
+ DependencyManager::set();
return previousSessionCrashed;
}
@@ -2284,7 +2286,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
// Setup the mouse ray pick and related operators
{
- auto mouseRayPick = std::make_shared(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_INCLUDE_NONCOLLIDABLE()), 0.0f, true);
+ auto mouseRayPick = std::make_shared(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES()), 0.0f, true);
mouseRayPick->parentTransform = std::make_shared();
mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE);
auto mouseRayPickID = DependencyManager::get()->addPick(PickQuery::Ray, mouseRayPick);
@@ -2617,6 +2619,7 @@ void Application::cleanupBeforeQuit() {
DependencyManager::destroy();
DependencyManager::destroy();
DependencyManager::destroy();
+ DependencyManager::destroy();
qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete";
}
diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp
index 140d2a7ccc..810e21daf5 100644
--- a/interface/src/Menu.cpp
+++ b/interface/src/Menu.cpp
@@ -35,6 +35,7 @@
#include "assets/ATPAssetMigrator.h"
#include "audio/AudioScope.h"
#include "avatar/AvatarManager.h"
+#include "avatar/AvatarPackager.h"
#include "AvatarBookmarks.h"
#include "devices/DdeFaceTracker.h"
#include "MainWindow.h"
@@ -144,9 +145,13 @@ Menu::Menu() {
assetServerAction->setEnabled(nodeList->getThisNodeCanWriteAssets());
}
- // Edit > Package Avatar as .fst...
- addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0,
- qApp, SLOT(packageModel()));
+ // Edit > Avatar Packager
+#ifndef Q_OS_ANDROID
+ action = addActionToQMenuAndActionHash(editMenu, MenuOption::AvatarPackager);
+ connect(action, &QAction::triggered, [] {
+ DependencyManager::get()->open();
+ });
+#endif
// Edit > Reload All Content
addActionToQMenuAndActionHash(editMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches()));
@@ -645,6 +650,8 @@ Menu::Menu() {
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowTrackedObjects, 0, false, qApp, SLOT(setShowTrackedObjects(bool)));
+ addActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::PackageModel, 0, qApp, SLOT(packageModel()));
+
// Developer > Hands >>>
MenuWrapper* handOptionsMenu = developerMenu->addMenu("Hands");
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false,
diff --git a/interface/src/Menu.h b/interface/src/Menu.h
index 7168b7294e..3611faaf8f 100644
--- a/interface/src/Menu.h
+++ b/interface/src/Menu.h
@@ -46,6 +46,7 @@ namespace MenuOption {
const QString AutoMuteAudio = "Auto Mute Microphone";
const QString AvatarReceiveStats = "Show Receive Stats";
const QString AvatarBookmarks = "Avatar Bookmarks";
+ const QString AvatarPackager = "Avatar Packager";
const QString Back = "Back";
const QString BinaryEyelidControl = "Binary Eyelid Control";
const QString BookmarkAvatar = "Bookmark Avatar";
diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp
index 8a8771c5b1..53c16c8a61 100644
--- a/interface/src/avatar/AvatarManager.cpp
+++ b/interface/src/avatar/AvatarManager.cpp
@@ -267,6 +267,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
if (avatar->getSkeletonModel()->isLoaded()) {
// remove the orb if it is there
avatar->removeOrb();
+ avatar->updateCollisionGroup(_myAvatar->getOtherAvatarsCollisionsEnabled());
if (avatar->needsPhysicsUpdate()) {
_avatarsToChangeInPhysics.insert(avatar);
}
diff --git a/interface/src/avatar/AvatarMotionState.cpp b/interface/src/avatar/AvatarMotionState.cpp
index ca67f634c8..3fa59ea967 100644
--- a/interface/src/avatar/AvatarMotionState.cpp
+++ b/interface/src/avatar/AvatarMotionState.cpp
@@ -19,6 +19,7 @@
AvatarMotionState::AvatarMotionState(OtherAvatarPointer avatar, const btCollisionShape* shape) : ObjectMotionState(shape), _avatar(avatar) {
assert(_avatar);
_type = MOTIONSTATE_TYPE_AVATAR;
+ _collisionGroup = BULLET_COLLISION_GROUP_OTHER_AVATAR;
cacheShapeDiameter();
}
@@ -170,8 +171,8 @@ QUuid AvatarMotionState::getSimulatorID() const {
// virtual
void AvatarMotionState::computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const {
- group = BULLET_COLLISION_GROUP_OTHER_AVATAR;
- mask = Physics::getDefaultCollisionMask(group);
+ group = _collisionGroup;
+ mask = _collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS ? 0 : Physics::getDefaultCollisionMask(group);
}
// virtual
diff --git a/interface/src/avatar/AvatarMotionState.h b/interface/src/avatar/AvatarMotionState.h
index 2533c11d56..3103341622 100644
--- a/interface/src/avatar/AvatarMotionState.h
+++ b/interface/src/avatar/AvatarMotionState.h
@@ -66,6 +66,9 @@ public:
void addDirtyFlags(uint32_t flags) { _dirtyFlags |= flags; }
+ void setCollisionGroup(int32_t group) { _collisionGroup = group; }
+ int32_t getCollisionGroup() { return _collisionGroup; }
+
virtual void computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const override;
virtual float getMass() const override;
@@ -87,7 +90,7 @@ protected:
OtherAvatarPointer _avatar;
float _diameter { 0.0f };
-
+ int32_t _collisionGroup;
uint32_t _dirtyFlags;
};
diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp
new file mode 100644
index 0000000000..fa70eee374
--- /dev/null
+++ b/interface/src/avatar/AvatarPackager.cpp
@@ -0,0 +1,149 @@
+//
+// AvatarPackager.cpp
+//
+//
+// Created by Thijs Wenker on 12/6/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
+//
+
+#include "AvatarPackager.h"
+
+#include "Application.h"
+
+#include
+#include
+#include
+
+#include
+#include "ModelSelector.h"
+#include
+
+#include
+#include "ui/TabletScriptingInterface.h"
+
+std::once_flag setupQMLTypesFlag;
+AvatarPackager::AvatarPackager() {
+ std::call_once(setupQMLTypesFlag, []() {
+ qmlRegisterType();
+ qmlRegisterType();
+ qRegisterMetaType();
+ qRegisterMetaType();
+ qRegisterMetaType();
+ qmlRegisterUncreatableMetaObject(
+ AvatarProjectStatus::staticMetaObject,
+ "Hifi.AvatarPackager.AvatarProjectStatus",
+ 1, 0,
+ "AvatarProjectStatus",
+ "Error: only enums"
+ );
+ });
+
+ recentProjectsFromVariantList(_recentProjectsSetting.get());
+
+ QDir defaultProjectsDir(AvatarProject::getDefaultProjectsPath());
+ defaultProjectsDir.mkpath(".");
+}
+
+bool AvatarPackager::open() {
+ const auto packageModelDialogCreated = [=](QQmlContext* context, QObject* newObject) {
+ context->setContextProperty("AvatarPackagerCore", this);
+ };
+
+ static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system";
+ auto tablet = dynamic_cast(DependencyManager::get()->getTablet(SYSTEM_TABLET));
+
+ if (tablet->getToolbarMode()) {
+ static const QUrl url{ "hifi/AvatarPackagerWindow.qml" };
+ DependencyManager::get()->show(url, "AvatarPackager", packageModelDialogCreated);
+ return true;
+ }
+
+ static const QUrl url{ "hifi/tablet/AvatarPackager.qml" };
+ if (!tablet->isPathLoaded(url)) {
+ tablet->getTabletSurface()->getSurfaceContext()->setContextProperty("AvatarPackagerCore", this);
+ tablet->pushOntoStack(url);
+ return true;
+ }
+
+ return false;
+}
+
+void AvatarPackager::addCurrentProjectToRecentProjects() {
+ const int MAX_RECENT_PROJECTS = 5;
+ const QString& fstPath = _currentAvatarProject->getFSTPath();
+ auto removeProjects = QVector();
+ for (const auto& project : _recentProjects) {
+ if (project.getProjectFSTPath() == fstPath) {
+ removeProjects.append(project);
+ }
+ }
+ for (const auto& removeProject : removeProjects) {
+ _recentProjects.removeOne(removeProject);
+ }
+
+ const auto newRecentProject = RecentAvatarProject(_currentAvatarProject->getProjectName(), fstPath);
+ _recentProjects.prepend(newRecentProject);
+
+ while (_recentProjects.size() > MAX_RECENT_PROJECTS) {
+ _recentProjects.pop_back();
+ }
+
+ _recentProjectsSetting.set(recentProjectsToVariantList(false));
+ emit recentProjectsChanged();
+}
+
+QVariantList AvatarPackager::recentProjectsToVariantList(bool includeProjectPaths) const {
+ QVariantList result;
+ for (const auto& project : _recentProjects) {
+ QVariantMap projectVariant;
+ projectVariant.insert("name", project.getProjectName());
+ projectVariant.insert("path", project.getProjectFSTPath());
+ if (includeProjectPaths) {
+ projectVariant.insert("projectPath", project.getProjectPath());
+ }
+ result.append(projectVariant);
+ }
+
+ return result;
+}
+void AvatarPackager::recentProjectsFromVariantList(QVariantList projectsVariant) {
+ _recentProjects.clear();
+ for (const auto& projectVariant : projectsVariant) {
+ auto map = projectVariant.toMap();
+ _recentProjects.append(RecentAvatarProject(map.value("name").toString(), map.value("path").toString()));
+ }
+}
+
+AvatarProjectStatus::AvatarProjectStatus AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) {
+ AvatarProjectStatus::AvatarProjectStatus status;
+ setAvatarProject(AvatarProject::openAvatarProject(avatarProjectFSTPath, status));
+ return status;
+}
+
+AvatarProjectStatus::AvatarProjectStatus AvatarPackager::createAvatarProject(const QString& projectsFolder,
+ const QString& avatarProjectName,
+ const QString& avatarModelPath,
+ const QString& textureFolder) {
+ AvatarProjectStatus::AvatarProjectStatus status;
+ setAvatarProject(AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder, status));
+ return status;
+}
+
+void AvatarPackager::setAvatarProject(AvatarProject* avatarProject) {
+ if (avatarProject == _currentAvatarProject) {
+ return;
+ }
+ if (_currentAvatarProject) {
+ _currentAvatarProject->deleteLater();
+ }
+ _currentAvatarProject = avatarProject;
+ if (_currentAvatarProject) {
+ addCurrentProjectToRecentProjects();
+ connect(_currentAvatarProject, &AvatarProject::nameChanged, this, &AvatarPackager::addCurrentProjectToRecentProjects);
+ QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership);
+ }
+ emit avatarProjectChanged();
+}
diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h
new file mode 100644
index 0000000000..ec954a60d7
--- /dev/null
+++ b/interface/src/avatar/AvatarPackager.h
@@ -0,0 +1,100 @@
+//
+// AvatarPackager.h
+//
+//
+// Created by Thijs Wenker on 12/6/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
+//
+
+#pragma once
+#ifndef hifi_AvatarPackager_h
+#define hifi_AvatarPackager_h
+
+#include
+#include
+
+#include "FileDialogHelper.h"
+
+#include "avatar/AvatarProject.h"
+#include "SettingHandle.h"
+
+class RecentAvatarProject {
+public:
+ RecentAvatarProject() = default;
+
+
+ RecentAvatarProject(QString projectName, QString projectFSTPath) {
+ _projectName = projectName;
+ _projectFSTPath = projectFSTPath;
+ }
+ RecentAvatarProject(const RecentAvatarProject& other) {
+ _projectName = other._projectName;
+ _projectFSTPath = other._projectFSTPath;
+ }
+
+ QString getProjectName() const { return _projectName; }
+
+ QString getProjectFSTPath() const { return _projectFSTPath; }
+
+ QString getProjectPath() const {
+ return QFileInfo(_projectFSTPath).absoluteDir().absolutePath();
+ }
+
+ bool operator==(const RecentAvatarProject& other) const {
+ return _projectName == other._projectName && _projectFSTPath == other._projectFSTPath;
+ }
+
+private:
+ QString _projectName;
+ QString _projectFSTPath;
+
+};
+
+class AvatarPackager : public QObject, public Dependency {
+ Q_OBJECT
+ SINGLETON_DEPENDENCY
+ Q_PROPERTY(AvatarProject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged)
+ Q_PROPERTY(QString AVATAR_PROJECTS_PATH READ getAvatarProjectsPath CONSTANT)
+ Q_PROPERTY(QVariantList recentProjects READ getRecentProjects NOTIFY recentProjectsChanged)
+public:
+ AvatarPackager();
+ bool open();
+
+ Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus createAvatarProject(const QString& projectsFolder,
+ const QString& avatarProjectName,
+ const QString& avatarModelPath,
+ const QString& textureFolder);
+
+ Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus openAvatarProject(const QString& avatarProjectFSTPath);
+ Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) const {
+ return AvatarProject::isValidNewProjectName(projectPath, projectName);
+ }
+
+signals:
+ void avatarProjectChanged();
+ void recentProjectsChanged();
+
+private:
+ Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; };
+ Q_INVOKABLE QString getAvatarProjectsPath() const { return AvatarProject::getDefaultProjectsPath(); }
+ Q_INVOKABLE QVariantList getRecentProjects() const { return recentProjectsToVariantList(true); }
+
+ void setAvatarProject(AvatarProject* avatarProject);
+
+ void addCurrentProjectToRecentProjects();
+
+ AvatarProject* _currentAvatarProject { nullptr };
+ QVector _recentProjects;
+
+ QVariantList recentProjectsToVariantList(bool includeProjectPaths) const;
+
+ void recentProjectsFromVariantList(QVariantList projectsVariant);
+
+
+ Setting::Handle _recentProjectsSetting { "io.highfidelity.avatarPackager.recentProjects", QVariantList() };
+};
+
+#endif // hifi_AvatarPackager_h
diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp
new file mode 100644
index 0000000000..728917e673
--- /dev/null
+++ b/interface/src/avatar/AvatarProject.cpp
@@ -0,0 +1,260 @@
+//
+// AvatarProject.cpp
+//
+//
+// Created by Thijs Wenker on 12/7/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
+//
+
+#include "AvatarProject.h"
+
+#include
+
+#include
+#include
+#include
+#include
+
+#include "FBXSerializer.h"
+#include
+#include "scripting/HMDScriptingInterface.h"
+
+AvatarProject* AvatarProject::openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status) {
+ status = AvatarProjectStatus::NONE;
+
+ if (!path.toLower().endsWith(".fst")) {
+ status = AvatarProjectStatus::ERROR_OPEN_INVALID_FILE_TYPE;
+ return nullptr;
+ }
+
+ QFileInfo fstFileInfo{ path };
+ if (!fstFileInfo.absoluteDir().exists()) {
+ status = AvatarProjectStatus::ERROR_OPEN_PROJECT_FOLDER;
+ return nullptr;
+ }
+
+ if (!fstFileInfo.exists()) {
+ status = AvatarProjectStatus::ERROR_OPEN_FIND_FST;
+ return nullptr;
+ }
+
+ QFile file{ fstFileInfo.filePath() };
+ if (!file.open(QIODevice::ReadOnly)) {
+ status = AvatarProjectStatus::ERROR_OPEN_OPEN_FST;
+ return nullptr;
+ }
+
+ const auto project = new AvatarProject(path, file.readAll());
+
+ QFileInfo fbxFileInfo{ project->getFBXPath() };
+ if (!fbxFileInfo.exists()) {
+ project->deleteLater();
+ status = AvatarProjectStatus::ERROR_OPEN_FIND_MODEL;
+ return nullptr;
+ }
+
+ QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership);
+ status = AvatarProjectStatus::SUCCESS;
+ return project;
+}
+
+AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName,
+ const QString& avatarModelPath, const QString& textureFolder,
+ AvatarProjectStatus::AvatarProjectStatus& status) {
+ status = AvatarProjectStatus::NONE;
+
+ if (!isValidNewProjectName(projectsFolder, avatarProjectName)) {
+ status = AvatarProjectStatus::ERROR_CREATE_PROJECT_NAME;
+ return nullptr;
+ }
+
+ QDir projectDir(projectsFolder + "/" + avatarProjectName);
+ if (!projectDir.mkpath(".")) {
+ status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
+ return nullptr;
+ }
+
+ QDir projectTexturesDir(projectDir.path() + "/textures");
+ if (!projectTexturesDir.mkpath(".")) {
+ status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
+ return nullptr;
+ }
+
+ QDir projectScriptsDir(projectDir.path() + "/scripts");
+ if (!projectScriptsDir.mkpath(".")) {
+ status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
+ return nullptr;
+ }
+
+ const auto fileName = QFileInfo(avatarModelPath).fileName();
+ const auto newModelPath = projectDir.absoluteFilePath(fileName);
+ const auto newFSTPath = projectDir.absoluteFilePath("avatar.fst");
+ QFile::copy(avatarModelPath, newModelPath);
+
+ QFileInfo fbxInfo{ newModelPath };
+ if (!fbxInfo.exists() || !fbxInfo.isFile()) {
+ status = AvatarProjectStatus::ERROR_CREATE_FIND_MODEL;
+ return nullptr;
+ }
+
+ QFile fbx{ fbxInfo.filePath() };
+ if (!fbx.open(QIODevice::ReadOnly)) {
+ status = AvatarProjectStatus::ERROR_CREATE_OPEN_MODEL;
+ return nullptr;
+ }
+
+ std::shared_ptr hfmModel;
+
+ try {
+ const QByteArray fbxContents = fbx.readAll();
+ hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath());
+ } catch (const QString& error) {
+ Q_UNUSED(error)
+ status = AvatarProjectStatus::ERROR_CREATE_READ_MODEL;
+ return nullptr;
+ }
+ QStringList textures{};
+
+ auto addTextureToList = [&textures](hfm::Texture texture) mutable {
+ if (!texture.filename.isEmpty() && texture.content.isEmpty() && !textures.contains(texture.filename)) {
+ textures << texture.filename;
+ }
+ };
+
+ foreach(const HFMMaterial material, hfmModel->materials) {
+ addTextureToList(material.normalTexture);
+ addTextureToList(material.albedoTexture);
+ addTextureToList(material.opacityTexture);
+ addTextureToList(material.glossTexture);
+ addTextureToList(material.roughnessTexture);
+ addTextureToList(material.specularTexture);
+ addTextureToList(material.metallicTexture);
+ addTextureToList(material.emissiveTexture);
+ addTextureToList(material.occlusionTexture);
+ addTextureToList(material.scatteringTexture);
+ addTextureToList(material.lightmapTexture);
+ }
+
+ QDir textureDir(textureFolder.isEmpty() ? fbxInfo.absoluteDir() : textureFolder);
+
+ for (const auto& texture : textures) {
+ QString sourcePath = textureDir.path() + "/" + texture;
+ QString targetPath = projectTexturesDir.path() + "/" + texture;
+
+ QFileInfo sourceTexturePath(sourcePath);
+ if (sourceTexturePath.exists()) {
+ QFile::copy(sourcePath, targetPath);
+ }
+ }
+
+ auto fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel);
+ fst->setName(avatarProjectName);
+
+ if (!fst->write()) {
+ status = AvatarProjectStatus::ERROR_CREATE_WRITE_FST;
+ return nullptr;
+ }
+
+ status = AvatarProjectStatus::SUCCESS;
+ return new AvatarProject(fst);
+}
+
+QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) const {
+ QStringList result{};
+ constexpr auto flags = QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden;
+ if (!scriptsDir.exists()) {
+ return result;
+ }
+
+ for (const auto& script : scriptsDir.entryInfoList({}, flags)) {
+ if (script.fileName().toLower().endsWith(".js")) {
+ result.push_back("scripts/" + script.fileName());
+ }
+ }
+
+ return result;
+}
+
+bool AvatarProject::isValidNewProjectName(const QString& projectPath, const QString& projectName) {
+ if (projectPath.trimmed().isEmpty() || projectName.trimmed().isEmpty()) {
+ return false;
+ }
+ QDir dir(projectPath + "/" + projectName);
+ return !dir.exists();
+}
+
+AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) :
+ AvatarProject::AvatarProject(new FST(fstPath, FSTReader::readMapping(data))) {
+}
+AvatarProject::AvatarProject(FST* fst) {
+ _fst = fst;
+ auto fileInfo = QFileInfo(getFSTPath());
+ _directory = fileInfo.absoluteDir();
+
+ _fst->setScriptPaths(getScriptPaths(QDir(_directory.path() + "/scripts")));
+ _fst->write();
+
+ refreshProjectFiles();
+
+ _projectPath = fileInfo.absoluteDir().absolutePath();
+}
+
+void AvatarProject::appendDirectory(const QString& prefix, const QDir& dir) {
+ constexpr auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden;
+ for (auto& entry : dir.entryInfoList({}, flags)) {
+ if (entry.isFile()) {
+ _projectFiles.append({ entry.absoluteFilePath(), prefix + entry.fileName() });
+ } else if (entry.isDir()) {
+ appendDirectory(prefix + entry.fileName() + "/", entry.absoluteFilePath());
+ }
+ }
+}
+
+void AvatarProject::refreshProjectFiles() {
+ _projectFiles.clear();
+ appendDirectory("", _directory);
+}
+
+QStringList AvatarProject::getProjectFiles() const {
+ QStringList paths;
+ for (auto& path : _projectFiles) {
+ paths.append(path.relativePath);
+ }
+ return paths;
+}
+
+MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) {
+ QUuid itemID;
+ if (updateExisting) {
+ itemID = _fst->getMarketplaceID();
+ }
+ auto uploader = new MarketplaceItemUploader(getProjectName(), "", QFileInfo(getFSTPath()).fileName(),
+ itemID, _projectFiles);
+ connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() {
+ if (uploader->getError() == MarketplaceItemUploader::Error::None) {
+ _fst->setMarketplaceID(uploader->getMarketplaceID());
+ _fst->write();
+ }
+ });
+
+ return uploader;
+}
+
+void AvatarProject::openInInventory() const {
+ constexpr int TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS { 1000 };
+
+ auto tablet = dynamic_cast(
+ DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"));
+ tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml");
+ DependencyManager::get()->openTablet();
+ tablet->getTabletRoot()->forceActiveFocus();
+ auto name = getProjectName();
+
+ // I'm not a fan of this, but it's the only current option.
+ QTimer::singleShot(TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS, [name, tablet]() {
+ tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } }));
+ });
+}
diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h
new file mode 100644
index 0000000000..1710282a3e
--- /dev/null
+++ b/interface/src/avatar/AvatarProject.h
@@ -0,0 +1,115 @@
+//
+// AvatarProject.h
+//
+//
+// Created by Thijs Wenker on 12/7/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
+//
+
+#pragma once
+#ifndef hifi_AvatarProject_h
+#define hifi_AvatarProject_h
+
+#include "MarketplaceItemUploader.h"
+#include "ProjectFile.h"
+#include "FST.h"
+
+#include
+#include
+#include
+#include
+
+namespace AvatarProjectStatus {
+ Q_NAMESPACE
+ enum AvatarProjectStatus {
+ NONE,
+ SUCCESS,
+ ERROR_CREATE_PROJECT_NAME,
+ ERROR_CREATE_CREATING_DIRECTORIES,
+ ERROR_CREATE_FIND_MODEL,
+ ERROR_CREATE_OPEN_MODEL,
+ ERROR_CREATE_READ_MODEL,
+ ERROR_CREATE_WRITE_FST,
+ ERROR_OPEN_INVALID_FILE_TYPE,
+ ERROR_OPEN_PROJECT_FOLDER,
+ ERROR_OPEN_FIND_FST,
+ ERROR_OPEN_OPEN_FST,
+ ERROR_OPEN_FIND_MODEL
+ };
+ Q_ENUM_NS(AvatarProjectStatus)
+}
+
+
+class AvatarProject : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(FST* fst READ getFST CONSTANT)
+
+ Q_PROPERTY(QStringList projectFiles READ getProjectFiles NOTIFY projectFilesChanged)
+
+ Q_PROPERTY(QString projectFolderPath READ getProjectPath CONSTANT)
+ Q_PROPERTY(QString projectFSTPath READ getFSTPath CONSTANT)
+ Q_PROPERTY(QString projectFBXPath READ getFBXPath CONSTANT)
+ Q_PROPERTY(QString name READ getProjectName WRITE setProjectName NOTIFY nameChanged)
+
+public:
+ Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting);
+ Q_INVOKABLE void openInInventory() const;
+ Q_INVOKABLE QStringList getProjectFiles() const;
+
+ Q_INVOKABLE QString getProjectName() const { return _fst->getName(); }
+ Q_INVOKABLE void setProjectName(const QString& newProjectName) {
+ if (newProjectName.trimmed().length() > 0) {
+ _fst->setName(newProjectName);
+ _fst->write();
+ emit nameChanged();
+ }
+ }
+ Q_INVOKABLE QString getProjectPath() const { return _projectPath; }
+ Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); }
+ Q_INVOKABLE QString getFBXPath() const {
+ return QDir::cleanPath(QDir(_projectPath).absoluteFilePath(_fst->getModelPath()));
+ }
+
+ /**
+ * returns the AvatarProject or a nullptr on failure.
+ */
+ static AvatarProject* openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status);
+ static AvatarProject* createAvatarProject(const QString& projectsFolder,
+ const QString& avatarProjectName,
+ const QString& avatarModelPath,
+ const QString& textureFolder,
+ AvatarProjectStatus::AvatarProjectStatus& status);
+
+ static bool isValidNewProjectName(const QString& projectPath, const QString& projectName);
+
+ static QString getDefaultProjectsPath() {
+ return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/High Fidelity Projects";
+ }
+
+signals:
+ void nameChanged();
+ void projectFilesChanged();
+
+private:
+ AvatarProject(const QString& fstPath, const QByteArray& data);
+ AvatarProject(FST* fst);
+
+ ~AvatarProject() { _fst->deleteLater(); }
+
+ FST* getFST() { return _fst; }
+
+ void refreshProjectFiles();
+ void appendDirectory(const QString& prefix, const QDir& dir);
+ QStringList getScriptPaths(const QDir& scriptsDir) const;
+
+ FST* _fst;
+
+ QDir _directory;
+ QList _projectFiles{};
+ QString _projectPath;
+};
+
+#endif // hifi_AvatarProject_h
diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp
new file mode 100644
index 0000000000..53b37eba4f
--- /dev/null
+++ b/interface/src/avatar/MarketplaceItemUploader.cpp
@@ -0,0 +1,321 @@
+//
+// MarketplaceItemUploader.cpp
+//
+//
+// Created by Ryan Huffman on 12/10/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
+//
+
+#include "MarketplaceItemUploader.h"
+
+#include
+#include
+
+#ifndef Q_OS_ANDROID
+#include
+#include
+#endif
+
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+MarketplaceItemUploader::MarketplaceItemUploader(QString title,
+ QString description,
+ QString rootFilename,
+ QUuid marketplaceID,
+ QList filePaths) :
+ _title(title),
+ _description(description),
+ _rootFilename(rootFilename),
+ _marketplaceID(marketplaceID),
+ _filePaths(filePaths) {
+}
+
+void MarketplaceItemUploader::setState(State newState) {
+ Q_ASSERT(_state != State::Complete);
+ Q_ASSERT(_error == Error::None);
+ Q_ASSERT(newState != _state);
+
+ _state = newState;
+ emit stateChanged(newState);
+ if (newState == State::Complete) {
+ emit completed();
+ emit finishedChanged();
+ }
+}
+
+void MarketplaceItemUploader::setError(Error error) {
+ Q_ASSERT(_state != State::Complete);
+ Q_ASSERT(_error == Error::None);
+
+ _error = error;
+ emit errorChanged(error);
+ emit finishedChanged();
+}
+
+void MarketplaceItemUploader::send() {
+ doGetCategories();
+}
+
+void MarketplaceItemUploader::doGetCategories() {
+ setState(State::GettingCategories);
+
+ static const QString path = "/api/v1/marketplace/categories";
+
+ auto accountManager = DependencyManager::get();
+ auto request = accountManager->createRequest(path, AccountManagerAuth::None);
+
+ qWarning() << "Request url is: " << request.url();
+
+ QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
+
+ QNetworkReply* reply = networkAccessManager.get(request);
+
+ connect(reply, &QNetworkReply::finished, this, [this, reply]() {
+ auto error = reply->error();
+ if (error == QNetworkReply::NoError) {
+ auto doc = QJsonDocument::fromJson(reply->readAll());
+ auto extractCategoryID = [&doc]() -> std::pair {
+ auto items = doc.object()["data"].toObject()["items"];
+ if (!items.isArray()) {
+ qWarning() << "Categories parse error: data.items is not an array";
+ return { false, 0 };
+ }
+
+ auto itemsArray = items.toArray();
+ for (const auto item : itemsArray) {
+ if (!item.isObject()) {
+ qWarning() << "Categories parse error: item is not an object";
+ return { false, 0 };
+ }
+
+ auto itemObject = item.toObject();
+ if (itemObject["name"].toString() == "Avatars") {
+ auto idValue = itemObject["id"];
+ if (!idValue.isDouble()) {
+ qWarning() << "Categories parse error: id is not a number";
+ return { false, 0 };
+ }
+ return { true, (int)idValue.toDouble() };
+ }
+ }
+
+ qWarning() << "Categories parse error: could not find a category for 'Avatar'";
+ return { false, 0 };
+ };
+
+ bool success;
+ std::tie(success, _categoryID) = extractCategoryID();
+ if (!success) {
+ qWarning() << "Failed to find marketplace category id";
+ setError(Error::Unknown);
+ } else {
+ qDebug() << "Marketplace Avatar category ID is" << _categoryID;
+ doUploadAvatar();
+ }
+ } else {
+ setError(Error::Unknown);
+ }
+ });
+}
+
+void MarketplaceItemUploader::doUploadAvatar() {
+#ifdef Q_OS_ANDROID
+ qWarning() << "Marketplace uploading is not supported on Android";
+ setError(Error::Unknown);
+ return;
+#else
+ QBuffer buffer{ &_fileData };
+ QuaZip zip{ &buffer };
+ if (!zip.open(QuaZip::Mode::mdAdd)) {
+ qWarning() << "Failed to open zip";
+ setError(Error::Unknown);
+ return;
+ }
+
+ for (auto& filePath : _filePaths) {
+ qWarning() << "Zipping: " << filePath.absolutePath << filePath.relativePath;
+ QFileInfo fileInfo{ filePath.absolutePath };
+
+ QuaZipFile zipFile{ &zip };
+ if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(filePath.relativePath))) {
+ qWarning() << "Could not open zip file:" << zipFile.getZipError();
+ setError(Error::Unknown);
+ return;
+ }
+ QFile file{ filePath.absolutePath };
+ if (file.open(QIODevice::ReadOnly)) {
+ zipFile.write(file.readAll());
+ } else {
+ qWarning() << "Failed to open: " << filePath.absolutePath;
+ }
+ file.close();
+ zipFile.close();
+ if (zipFile.getZipError() != UNZ_OK) {
+ qWarning() << "Could not close zip file: " << zipFile.getZipError();
+ setState(State::Complete);
+ return;
+ }
+ }
+
+ zip.close();
+
+ qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB";
+
+ QString path = "/api/v1/marketplace/items";
+ bool creating = true;
+ if (!_marketplaceID.isNull()) {
+ creating = false;
+ auto idWithBraces = _marketplaceID.toString();
+ auto idWithoutBraces = idWithBraces.mid(1, idWithBraces.length() - 2);
+ path += "/" + idWithoutBraces;
+ }
+ auto accountManager = DependencyManager::get();
+ auto request = accountManager->createRequest(path, AccountManagerAuth::Required);
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+
+ // TODO(huffman) add JSON escaping
+ auto escapeJson = [](QString str) -> QString { return str; };
+
+ QString jsonString = "{\"marketplace_item\":{";
+ jsonString += "\"title\":\"" + escapeJson(_title) + "\"";
+
+ // Items cannot have their description updated after they have been submitted.
+ if (creating) {
+ jsonString += ",\"description\":\"" + escapeJson(_description) + "\"";
+ }
+
+ jsonString += ",\"root_file_key\":\"" + escapeJson(_rootFilename) + "\"";
+ jsonString += ",\"category_ids\":[" + QStringLiteral("%1").arg(_categoryID) + "]";
+ jsonString += ",\"license\":0";
+ jsonString += ",\"files\":\"" + QString::fromLatin1(_fileData.toBase64()) + "\"}}";
+
+ QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
+
+ QNetworkReply* reply{ nullptr };
+ if (creating) {
+ reply = networkAccessManager.post(request, jsonString.toUtf8());
+ } else {
+ reply = networkAccessManager.put(request, jsonString.toUtf8());
+ }
+
+ connect(reply, &QNetworkReply::uploadProgress, this, [this](float bytesSent, float bytesTotal) {
+ if (_state == State::UploadingAvatar) {
+ emit uploadProgress(bytesSent, bytesTotal);
+ if (bytesSent >= bytesTotal) {
+ setState(State::WaitingForUploadResponse);
+ }
+ }
+ });
+
+ connect(reply, &QNetworkReply::finished, this, [this, reply]() {
+ _responseData = reply->readAll();
+
+ auto error = reply->error();
+ if (error == QNetworkReply::NoError) {
+ auto doc = QJsonDocument::fromJson(_responseData.toLatin1());
+ auto status = doc.object()["status"].toString();
+ if (status == "success") {
+ _marketplaceID = QUuid::fromString(doc["data"].toObject()["marketplace_id"].toString());
+ _itemVersion = doc["data"].toObject()["version"].toDouble();
+ setState(State::WaitingForInventory);
+ doWaitForInventory();
+ } else {
+ qWarning() << "Got error response while uploading avatar: " << _responseData;
+ setError(Error::Unknown);
+ }
+ } else {
+ qWarning() << "Got error while uploading avatar: " << reply->error() << reply->errorString() << _responseData;
+ setError(Error::Unknown);
+ }
+ });
+
+ setState(State::UploadingAvatar);
+#endif
+}
+
+void MarketplaceItemUploader::doWaitForInventory() {
+ static const QString path = "/api/v1/commerce/inventory";
+
+ auto accountManager = DependencyManager::get();
+ auto request = accountManager->createRequest(path, AccountManagerAuth::Required);
+
+ QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
+
+ QNetworkReply* reply = networkAccessManager.post(request, "");
+
+ _numRequestsForInventory++;
+
+ connect(reply, &QNetworkReply::finished, this, [this, reply]() {
+ auto data = reply->readAll();
+
+ bool success = false;
+
+ auto error = reply->error();
+ if (error == QNetworkReply::NoError) {
+ // Parse response data
+ auto doc = QJsonDocument::fromJson(data);
+ auto isAssetAvailable = [this, &doc]() -> bool {
+ if (!doc.isObject()) {
+ return false;
+ }
+ auto root = doc.object();
+ auto status = root["status"].toString();
+ if (status != "success") {
+ return false;
+ }
+ auto data = root["data"];
+ if (!data.isObject()) {
+ return false;
+ }
+ auto assets = data.toObject()["assets"];
+ if (!assets.isArray()) {
+ return false;
+ }
+ for (auto asset : assets.toArray()) {
+ auto assetObject = asset.toObject();
+ auto id = QUuid::fromString(assetObject["id"].toString());
+ if (id.isNull()) {
+ continue;
+ }
+ if (id == _marketplaceID) {
+ auto version = assetObject["version"];
+ auto valid = assetObject["valid"];
+ if (version.isDouble() && valid.isBool()) {
+ if ((int)version.toDouble() >= _itemVersion && valid.toBool()) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ };
+
+ success = isAssetAvailable();
+ }
+ if (success) {
+ qDebug() << "Found item in inventory";
+ setState(State::Complete);
+ } else {
+ constexpr int MAX_INVENTORY_REQUESTS { 8 };
+ constexpr int TIME_BETWEEN_INVENTORY_REQUESTS_MS { 5000 };
+ if (_numRequestsForInventory > MAX_INVENTORY_REQUESTS) {
+ qDebug() << "Failed to find item in inventory";
+ setError(Error::Unknown);
+ } else {
+ QTimer::singleShot(TIME_BETWEEN_INVENTORY_REQUESTS_MS, [this]() { doWaitForInventory(); });
+ }
+ }
+ });
+}
diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h
new file mode 100644
index 0000000000..998413da88
--- /dev/null
+++ b/interface/src/avatar/MarketplaceItemUploader.h
@@ -0,0 +1,105 @@
+//
+// MarketplaceItemUploader.h
+//
+//
+// Created by Ryan Huffman on 12/10/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
+//
+
+#pragma once
+#ifndef hifi_MarketplaceItemUploader_h
+#define hifi_MarketplaceItemUploader_h
+
+#include "ProjectFile.h"
+
+#include
+#include
+
+class QNetworkReply;
+
+class MarketplaceItemUploader : public QObject {
+ Q_OBJECT
+
+ Q_PROPERTY(bool finished READ getFinished NOTIFY finishedChanged)
+
+ Q_PROPERTY(bool complete READ getComplete NOTIFY stateChanged)
+ Q_PROPERTY(State state READ getState NOTIFY stateChanged)
+ Q_PROPERTY(Error error READ getError NOTIFY errorChanged)
+ Q_PROPERTY(QString responseData READ getResponseData)
+public:
+ enum class Error {
+ None,
+ Unknown,
+ };
+ Q_ENUM(Error);
+
+ enum class State {
+ Idle,
+ GettingCategories,
+ UploadingAvatar,
+ WaitingForUploadResponse,
+ WaitingForInventory,
+ Complete,
+ };
+ Q_ENUM(State);
+
+ MarketplaceItemUploader(QString title,
+ QString description,
+ QString rootFilename,
+ QUuid marketplaceID,
+ QList filePaths);
+
+ Q_INVOKABLE void send();
+
+ void setError(Error error);
+
+ QString getResponseData() const { return _responseData; }
+ void setState(State newState);
+ State getState() const { return _state; }
+ bool getComplete() const { return _state == State::Complete; }
+
+ QUuid getMarketplaceID() const { return _marketplaceID; }
+
+ Error getError() const { return _error; }
+ bool getFinished() const { return _state == State::Complete || _error != Error::None; }
+
+signals:
+ void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
+ void completed();
+
+ void stateChanged(State newState);
+ void errorChanged(Error error);
+
+ // Triggered when the upload has finished, either succesfully completing, or stopping with an error
+ void finishedChanged();
+
+private:
+ void doGetCategories();
+ void doUploadAvatar();
+ void doWaitForInventory();
+
+ QNetworkReply* _reply;
+
+ State _state { State::Idle };
+ Error _error { Error::None };
+
+ QString _title;
+ QString _description;
+ QString _rootFilename;
+ QUuid _marketplaceID;
+ int _categoryID;
+ int _itemVersion;
+
+ QString _responseData;
+
+ int _numRequestsForInventory { 0 };
+
+ QString _rootFilePath;
+ QList _filePaths;
+ QByteArray _fileData;
+};
+
+#endif // hifi_MarketplaceItemUploader_h
diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp
index aecfb687e4..e40fc7f9dd 100755
--- a/interface/src/avatar/MyAvatar.cpp
+++ b/interface/src/avatar/MyAvatar.cpp
@@ -3374,7 +3374,6 @@ void MyAvatar::setCollisionsEnabled(bool enabled) {
QMetaObject::invokeMethod(this, "setCollisionsEnabled", Q_ARG(bool, enabled));
return;
}
-
_characterController.setCollisionless(!enabled);
emit collisionsEnabledChanged(enabled);
}
@@ -3385,6 +3384,20 @@ bool MyAvatar::getCollisionsEnabled() {
return _characterController.computeCollisionGroup() != BULLET_COLLISION_GROUP_COLLISIONLESS;
}
+void MyAvatar::setOtherAvatarsCollisionsEnabled(bool enabled) {
+
+ if (QThread::currentThread() != thread()) {
+ QMetaObject::invokeMethod(this, "setOtherAvatarsCollisionsEnabled", Q_ARG(bool, enabled));
+ return;
+ }
+ _collideWithOtherAvatars = enabled;
+ emit otherAvatarsCollisionsEnabledChanged(enabled);
+}
+
+bool MyAvatar::getOtherAvatarsCollisionsEnabled() {
+ return _collideWithOtherAvatars;
+}
+
void MyAvatar::updateCollisionCapsuleCache() {
glm::vec3 start, end;
float radius;
diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h
index 461cd23cbd..17b71153ea 100644
--- a/interface/src/avatar/MyAvatar.h
+++ b/interface/src/avatar/MyAvatar.h
@@ -225,6 +225,7 @@ class MyAvatar : public Avatar {
Q_PROPERTY(bool centerOfGravityModelEnabled READ getCenterOfGravityModelEnabled WRITE setCenterOfGravityModelEnabled)
Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled)
Q_PROPERTY(bool collisionsEnabled READ getCollisionsEnabled WRITE setCollisionsEnabled)
+ Q_PROPERTY(bool otherAvatarsCollisionsEnabled READ getOtherAvatarsCollisionsEnabled WRITE setOtherAvatarsCollisionsEnabled)
Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled)
Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls)
Q_PROPERTY(bool showPlayArea READ getShowPlayArea WRITE setShowPlayArea)
@@ -1064,6 +1065,18 @@ public:
*/
Q_INVOKABLE bool getCollisionsEnabled();
+ /**jsdoc
+ * @function MyAvatar.setOtherAvatarsCollisionsEnabled
+ * @param {boolean} enabled
+ */
+ Q_INVOKABLE void setOtherAvatarsCollisionsEnabled(bool enabled);
+
+ /**jsdoc
+ * @function MyAvatar.getOtherAvatarsCollisionsEnabled
+ * @returns {boolean}
+ */
+ Q_INVOKABLE bool getOtherAvatarsCollisionsEnabled();
+
/**jsdoc
* @function MyAvatar.getCollisionCapsule
* @returns {object}
@@ -1491,6 +1504,14 @@ signals:
*/
void collisionsEnabledChanged(bool enabled);
+ /**jsdoc
+ * Triggered when collisions with other avatars enabled or disabled
+ * @function MyAvatar.otherAvatarsCollisionsEnabledChanged
+ * @param {boolean} enabled
+ * @returns {Signal}
+ */
+ void otherAvatarsCollisionsEnabledChanged(bool enabled);
+
/**jsdoc
* Triggered when avatar's animation url changes
* @function MyAvatar.animGraphUrlChanged
diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp
index c2687fd525..a71d2478ad 100644
--- a/interface/src/avatar/OtherAvatar.cpp
+++ b/interface/src/avatar/OtherAvatar.cpp
@@ -120,7 +120,7 @@ bool OtherAvatar::shouldBeInPhysicsSimulation() const {
}
bool OtherAvatar::needsPhysicsUpdate() const {
- constexpr uint32_t FLAGS_OF_INTEREST = Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS | Simulation::DIRTY_POSITION;
+ constexpr uint32_t FLAGS_OF_INTEREST = Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS | Simulation::DIRTY_POSITION | Simulation::DIRTY_COLLISION_GROUP;
return (_motionState && (bool)(_motionState->getIncomingDirtyFlags() & FLAGS_OF_INTEREST));
}
@@ -129,3 +129,17 @@ void OtherAvatar::rebuildCollisionShape() {
_motionState->addDirtyFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS);
}
}
+
+void OtherAvatar::updateCollisionGroup(bool myAvatarCollide) {
+ if (_motionState) {
+ bool collides = _motionState->getCollisionGroup() == BULLET_COLLISION_GROUP_OTHER_AVATAR && myAvatarCollide;
+ if (_collideWithOtherAvatars != collides) {
+ if (!myAvatarCollide) {
+ _collideWithOtherAvatars = false;
+ }
+ auto newCollisionGroup = _collideWithOtherAvatars ? BULLET_COLLISION_GROUP_OTHER_AVATAR : BULLET_COLLISION_GROUP_COLLISIONLESS;
+ _motionState->setCollisionGroup(newCollisionGroup);
+ _motionState->addDirtyFlags(Simulation::DIRTY_COLLISION_GROUP);
+ }
+ }
+}
\ No newline at end of file
diff --git a/interface/src/avatar/OtherAvatar.h b/interface/src/avatar/OtherAvatar.h
index 5b72815757..48402fe55c 100644
--- a/interface/src/avatar/OtherAvatar.h
+++ b/interface/src/avatar/OtherAvatar.h
@@ -45,6 +45,8 @@ public:
bool shouldBeInPhysicsSimulation() const;
bool needsPhysicsUpdate() const;
+ void updateCollisionGroup(bool myAvatarCollide);
+
friend AvatarManager;
protected:
diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
index daf70ada22..fddd52a6dd 100644
--- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
+++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
@@ -549,6 +549,7 @@ protected:
glm::vec3 getBodyRightDirection() const { return getWorldOrientation() * IDENTITY_RIGHT; }
glm::vec3 getBodyUpDirection() const { return getWorldOrientation() * IDENTITY_UP; }
void measureMotionDerivatives(float deltaTime);
+ bool getCollideWithOtherAvatars() const { return _collideWithOtherAvatars; }
float getSkeletonHeight() const;
float getHeadHeight() const;
diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp
index ae72725e2e..e72fa3a6eb 100644
--- a/libraries/avatars/src/AvatarData.cpp
+++ b/libraries/avatars/src/AvatarData.cpp
@@ -540,6 +540,10 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
if (_headData->getHasProceduralBlinkFaceMovement()) {
setAtBit16(flags, PROCEDURAL_BLINK_FACE_MOVEMENT);
}
+ // avatar collisions enabled
+ if (_collideWithOtherAvatars) {
+ setAtBit16(flags, COLLIDE_WITH_OTHER_AVATARS);
+ }
data->flags = flags;
destinationBuffer += sizeof(AvatarDataPacket::AdditionalFlags);
@@ -1116,7 +1120,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
auto newHasAudioEnabledFaceMovement = oneAtBit16(bitItems, AUDIO_ENABLED_FACE_MOVEMENT);
auto newHasProceduralEyeFaceMovement = oneAtBit16(bitItems, PROCEDURAL_EYE_FACE_MOVEMENT);
auto newHasProceduralBlinkFaceMovement = oneAtBit16(bitItems, PROCEDURAL_BLINK_FACE_MOVEMENT);
-
+ auto newCollideWithOtherAvatars = oneAtBit16(bitItems, COLLIDE_WITH_OTHER_AVATARS);
bool keyStateChanged = (_keyState != newKeyState);
bool handStateChanged = (_handState != newHandState);
@@ -1125,7 +1129,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
bool audioEnableFaceMovementChanged = (_headData->getHasAudioEnabledFaceMovement() != newHasAudioEnabledFaceMovement);
bool proceduralEyeFaceMovementChanged = (_headData->getHasProceduralEyeFaceMovement() != newHasProceduralEyeFaceMovement);
bool proceduralBlinkFaceMovementChanged = (_headData->getHasProceduralBlinkFaceMovement() != newHasProceduralBlinkFaceMovement);
- bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged || proceduralEyeFaceMovementChanged || proceduralBlinkFaceMovementChanged;
+ bool collideWithOtherAvatarsChanged = (_collideWithOtherAvatars != newCollideWithOtherAvatars);
+ bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged ||
+ proceduralEyeFaceMovementChanged || proceduralBlinkFaceMovementChanged || collideWithOtherAvatarsChanged;
_keyState = newKeyState;
_handState = newHandState;
@@ -1134,6 +1140,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
_headData->setHasAudioEnabledFaceMovement(newHasAudioEnabledFaceMovement);
_headData->setHasProceduralEyeFaceMovement(newHasProceduralEyeFaceMovement);
_headData->setHasProceduralBlinkFaceMovement(newHasProceduralBlinkFaceMovement);
+ _collideWithOtherAvatars = newCollideWithOtherAvatars;
sourceBuffer += sizeof(AvatarDataPacket::AdditionalFlags);
diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h
index 64b8814149..b42c387f61 100644
--- a/libraries/avatars/src/AvatarData.h
+++ b/libraries/avatars/src/AvatarData.h
@@ -110,6 +110,7 @@ const int HAND_STATE_FINGER_POINTING_BIT = 7; // 8th bit
const int AUDIO_ENABLED_FACE_MOVEMENT = 8; // 9th bit
const int PROCEDURAL_EYE_FACE_MOVEMENT = 9; // 10th bit
const int PROCEDURAL_BLINK_FACE_MOVEMENT = 10; // 11th bit
+const int COLLIDE_WITH_OTHER_AVATARS = 11; // 12th bit
const char HAND_STATE_NULL = 0;
@@ -1495,6 +1496,7 @@ protected:
int _replicaIndex { 0 };
bool _isNewAvatar { true };
bool _isClientAvatar { false };
+ bool _collideWithOtherAvatars { true };
// null unless MyAvatar or ScriptableAvatar sending traits data to mixer
std::unique_ptr _clientTraitsHandler;
diff --git a/libraries/avatars/src/ProjectFile.h b/libraries/avatars/src/ProjectFile.h
new file mode 100644
index 0000000000..4040eb1ce5
--- /dev/null
+++ b/libraries/avatars/src/ProjectFile.h
@@ -0,0 +1,13 @@
+#ifndef hifi_AvatarProjectFile_h
+#define hifi_AvatarProjectFile_h
+
+#include
+
+class ProjectFilePath {
+ Q_GADGET;
+public:
+ QString absolutePath;
+ QString relativePath;
+};
+
+#endif // hifi_AvatarProjectFile_h
diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp
new file mode 100644
index 0000000000..7828037c74
--- /dev/null
+++ b/libraries/fbx/src/FST.cpp
@@ -0,0 +1,190 @@
+//
+// FST.cpp
+//
+// Created by Ryan Huffman on 12/11/15.
+// 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 "FST.h"
+
+#include
+#include
+#include
+
+constexpr float DEFAULT_SCALE { 1.0f };
+
+FST::FST(QString fstPath, QVariantHash data) : _fstPath(std::move(fstPath)) {
+
+ auto setValueFromFSTData = [&data] (const QString& propertyID, auto &targetProperty) mutable {
+ if (data.contains(propertyID)) {
+ targetProperty = data[propertyID].toString();
+ data.remove(propertyID);
+ }
+ };
+ setValueFromFSTData(NAME_FIELD, _name);
+ setValueFromFSTData(FILENAME_FIELD, _modelPath);
+ setValueFromFSTData(MARKETPLACE_ID_FIELD, _marketplaceID);
+
+ if (data.contains(SCRIPT_FIELD)) {
+ QVariantList scripts = data.values(SCRIPT_FIELD);
+ for (const auto& script : scripts) {
+ _scriptPaths.push_back(script.toString());
+ }
+ data.remove(SCRIPT_FIELD);
+ }
+
+ _other = data;
+}
+
+FST* FST::createFSTFromModel(const QString& fstPath, const QString& modelFilePath, const hfm::Model& hfmModel) {
+ QVariantHash mapping;
+
+ // mixamo files - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will
+ // be rewritten, so we detect the existence of several different blendshapes which indicate we're likely a mixamo file
+ bool likelyMixamoFile = hfmModel.applicationName == "mixamo.com" ||
+ (hfmModel.blendshapeChannelNames.contains("BrowsDown_Right") &&
+ hfmModel.blendshapeChannelNames.contains("MouthOpen") &&
+ hfmModel.blendshapeChannelNames.contains("Blink_Left") &&
+ hfmModel.blendshapeChannelNames.contains("Blink_Right") &&
+ hfmModel.blendshapeChannelNames.contains("Squint_Right"));
+
+ mapping.insert(NAME_FIELD, QFileInfo(fstPath).baseName());
+ mapping.insert(FILENAME_FIELD, QFileInfo(modelFilePath).fileName());
+ mapping.insert(TEXDIR_FIELD, "textures");
+
+ // mixamo/autodesk defaults
+ mapping.insert(SCALE_FIELD, DEFAULT_SCALE);
+ QVariantHash joints = mapping.value(JOINT_FIELD).toHash();
+ joints.insert("jointEyeLeft", hfmModel.jointIndices.contains("jointEyeLeft") ? "jointEyeLeft" :
+ (hfmModel.jointIndices.contains("EyeLeft") ? "EyeLeft" : "LeftEye"));
+
+ joints.insert("jointEyeRight", hfmModel.jointIndices.contains("jointEyeRight") ? "jointEyeRight" :
+ hfmModel.jointIndices.contains("EyeRight") ? "EyeRight" : "RightEye");
+
+ joints.insert("jointNeck", hfmModel.jointIndices.contains("jointNeck") ? "jointNeck" : "Neck");
+ joints.insert("jointRoot", "Hips");
+ joints.insert("jointLean", "Spine");
+ joints.insert("jointLeftHand", "LeftHand");
+ joints.insert("jointRightHand", "RightHand");
+
+ const char* topName = likelyMixamoFile ? "HeadTop_End" : "HeadEnd";
+ joints.insert("jointHead", hfmModel.jointIndices.contains(topName) ? topName : "Head");
+
+ mapping.insert(JOINT_FIELD, joints);
+
+ QVariantHash jointIndices;
+ for (int i = 0; i < hfmModel.joints.size(); i++) {
+ jointIndices.insert(hfmModel.joints.at(i).name, QString::number(i));
+ }
+ mapping.insert(JOINT_INDEX_FIELD, jointIndices);
+
+ mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm");
+ mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm");
+ mapping.insertMulti(FREE_JOINT_FIELD, "RightArm");
+ mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm");
+
+
+ // If there are no blendshape mappings, and we detect that this is likely a mixamo file,
+ // then we can add the default mixamo to "faceshift" mappings
+ if (likelyMixamoFile) {
+ QVariantHash blendshapes;
+ blendshapes.insertMulti("BrowsD_L", QVariantList() << "BrowsDown_Left" << 1.0);
+ blendshapes.insertMulti("BrowsD_R", QVariantList() << "BrowsDown_Right" << 1.0);
+ blendshapes.insertMulti("BrowsU_C", QVariantList() << "BrowsUp_Left" << 1.0);
+ blendshapes.insertMulti("BrowsU_C", QVariantList() << "BrowsUp_Right" << 1.0);
+ blendshapes.insertMulti("BrowsU_L", QVariantList() << "BrowsUp_Left" << 1.0);
+ blendshapes.insertMulti("BrowsU_R", QVariantList() << "BrowsUp_Right" << 1.0);
+ blendshapes.insertMulti("ChinLowerRaise", QVariantList() << "Jaw_Up" << 1.0);
+ blendshapes.insertMulti("ChinUpperRaise", QVariantList() << "UpperLipUp_Left" << 0.5);
+ blendshapes.insertMulti("ChinUpperRaise", QVariantList() << "UpperLipUp_Right" << 0.5);
+ blendshapes.insertMulti("EyeBlink_L", QVariantList() << "Blink_Left" << 1.0);
+ blendshapes.insertMulti("EyeBlink_R", QVariantList() << "Blink_Right" << 1.0);
+ blendshapes.insertMulti("EyeOpen_L", QVariantList() << "EyesWide_Left" << 1.0);
+ blendshapes.insertMulti("EyeOpen_R", QVariantList() << "EyesWide_Right" << 1.0);
+ blendshapes.insertMulti("EyeSquint_L", QVariantList() << "Squint_Left" << 1.0);
+ blendshapes.insertMulti("EyeSquint_R", QVariantList() << "Squint_Right" << 1.0);
+ blendshapes.insertMulti("JawFwd", QVariantList() << "JawForeward" << 1.0);
+ blendshapes.insertMulti("JawLeft", QVariantList() << "JawRotateY_Left" << 0.5);
+ blendshapes.insertMulti("JawOpen", QVariantList() << "MouthOpen" << 0.7);
+ blendshapes.insertMulti("JawRight", QVariantList() << "Jaw_Right" << 1.0);
+ blendshapes.insertMulti("LipsFunnel", QVariantList() << "JawForeward" << 0.39);
+ blendshapes.insertMulti("LipsFunnel", QVariantList() << "Jaw_Down" << 0.36);
+ blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthNarrow_Left" << 1.0);
+ blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthNarrow_Right" << 1.0);
+ blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthWhistle_NarrowAdjust_Left" << 0.5);
+ blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthWhistle_NarrowAdjust_Right" << 0.5);
+ blendshapes.insertMulti("LipsFunnel", QVariantList() << "TongueUp" << 1.0);
+ blendshapes.insertMulti("LipsLowerClose", QVariantList() << "LowerLipIn" << 1.0);
+ blendshapes.insertMulti("LipsLowerDown", QVariantList() << "LowerLipDown_Left" << 0.7);
+ blendshapes.insertMulti("LipsLowerDown", QVariantList() << "LowerLipDown_Right" << 0.7);
+ blendshapes.insertMulti("LipsLowerOpen", QVariantList() << "LowerLipOut" << 1.0);
+ blendshapes.insertMulti("LipsPucker", QVariantList() << "MouthNarrow_Left" << 1.0);
+ blendshapes.insertMulti("LipsPucker", QVariantList() << "MouthNarrow_Right" << 1.0);
+ blendshapes.insertMulti("LipsUpperClose", QVariantList() << "UpperLipIn" << 1.0);
+ blendshapes.insertMulti("LipsUpperOpen", QVariantList() << "UpperLipOut" << 1.0);
+ blendshapes.insertMulti("LipsUpperUp", QVariantList() << "UpperLipUp_Left" << 0.7);
+ blendshapes.insertMulti("LipsUpperUp", QVariantList() << "UpperLipUp_Right" << 0.7);
+ blendshapes.insertMulti("MouthDimple_L", QVariantList() << "Smile_Left" << 0.25);
+ blendshapes.insertMulti("MouthDimple_R", QVariantList() << "Smile_Right" << 0.25);
+ blendshapes.insertMulti("MouthFrown_L", QVariantList() << "Frown_Left" << 1.0);
+ blendshapes.insertMulti("MouthFrown_R", QVariantList() << "Frown_Right" << 1.0);
+ blendshapes.insertMulti("MouthLeft", QVariantList() << "Midmouth_Left" << 1.0);
+ blendshapes.insertMulti("MouthRight", QVariantList() << "Midmouth_Right" << 1.0);
+ blendshapes.insertMulti("MouthSmile_L", QVariantList() << "Smile_Left" << 1.0);
+ blendshapes.insertMulti("MouthSmile_R", QVariantList() << "Smile_Right" << 1.0);
+ blendshapes.insertMulti("Puff", QVariantList() << "CheekPuff_Left" << 1.0);
+ blendshapes.insertMulti("Puff", QVariantList() << "CheekPuff_Right" << 1.0);
+ blendshapes.insertMulti("Sneer", QVariantList() << "NoseScrunch_Left" << 0.75);
+ blendshapes.insertMulti("Sneer", QVariantList() << "NoseScrunch_Right" << 0.75);
+ blendshapes.insertMulti("Sneer", QVariantList() << "Squint_Left" << 0.5);
+ blendshapes.insertMulti("Sneer", QVariantList() << "Squint_Right" << 0.5);
+ mapping.insert(BLENDSHAPE_FIELD, blendshapes);
+ }
+ return new FST(fstPath, mapping);
+}
+
+QString FST::absoluteModelPath() const {
+ QFileInfo fileInfo{ _fstPath };
+ QDir dir{ fileInfo.absoluteDir() };
+ return dir.absoluteFilePath(_modelPath);
+}
+
+void FST::setName(const QString& name) {
+ _name = name;
+ emit nameChanged(name);
+}
+
+void FST::setModelPath(const QString& modelPath) {
+ _modelPath = modelPath;
+ emit modelPathChanged(modelPath);
+}
+
+QVariantHash FST::getMapping() const {
+ QVariantHash mapping;
+ mapping.unite(_other);
+ mapping.insert(NAME_FIELD, _name);
+ mapping.insert(FILENAME_FIELD, _modelPath);
+ mapping.insert(MARKETPLACE_ID_FIELD, _marketplaceID);
+ for (const auto& scriptPath : _scriptPaths) {
+ mapping.insertMulti(SCRIPT_FIELD, scriptPath);
+ }
+ return mapping;
+}
+
+bool FST::write() {
+ QFile fst(_fstPath);
+ if (!fst.open(QIODevice::WriteOnly)) {
+ return false;
+ }
+ fst.write(FSTReader::writeMapping(getMapping()));
+ return true;
+}
+
+void FST::setMarketplaceID(QUuid marketplaceID) {
+ _marketplaceID = marketplaceID;
+ emit marketplaceIDChanged();
+}
diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h
new file mode 100644
index 0000000000..0f4c1ecd3a
--- /dev/null
+++ b/libraries/fbx/src/FST.h
@@ -0,0 +1,71 @@
+//
+// FST.h
+//
+// Created by Ryan Huffman on 12/11/15.
+// 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
+//
+
+#ifndef hifi_FST_h
+#define hifi_FST_h
+
+#include
+#include
+#include "FSTReader.h"
+
+namespace hfm {
+ class Model;
+};
+
+class FST : public QObject {
+ Q_OBJECT
+ Q_PROPERTY(QString name READ getName WRITE setName NOTIFY nameChanged)
+ Q_PROPERTY(QString modelPath READ getModelPath WRITE setModelPath NOTIFY modelPathChanged)
+ Q_PROPERTY(QUuid marketplaceID READ getMarketplaceID)
+ Q_PROPERTY(bool hasMarketplaceID READ getHasMarketplaceID NOTIFY marketplaceIDChanged)
+public:
+ FST(QString fstPath, QVariantHash data);
+
+ static FST* createFSTFromModel(const QString& fstPath, const QString& modelFilePath, const hfm::Model& hfmModel);
+
+ QString absoluteModelPath() const;
+
+ QString getName() const { return _name; }
+ void setName(const QString& name);
+
+ QString getModelPath() const { return _modelPath; }
+ void setModelPath(const QString& modelPath);
+
+ Q_INVOKABLE bool getHasMarketplaceID() const { return !_marketplaceID.isNull(); }
+ QUuid getMarketplaceID() const { return _marketplaceID; }
+ void setMarketplaceID(QUuid marketplaceID);
+
+ QStringList getScriptPaths() const { return _scriptPaths; }
+ void setScriptPaths(QStringList scriptPaths) { _scriptPaths = scriptPaths; }
+
+ QString getPath() const { return _fstPath; }
+
+ QVariantHash getMapping() const;
+
+ bool write();
+
+signals:
+ void nameChanged(const QString& name);
+ void modelPathChanged(const QString& modelPath);
+ void marketplaceIDChanged();
+
+private:
+ QString _fstPath;
+
+ QString _name{};
+ QString _modelPath{};
+ QUuid _marketplaceID{};
+
+ QStringList _scriptPaths{};
+
+ QVariantHash _other{};
+};
+
+#endif // hifi_FST_h
diff --git a/libraries/fbx/src/FSTReader.cpp b/libraries/fbx/src/FSTReader.cpp
index 75596862d2..43806560dc 100644
--- a/libraries/fbx/src/FSTReader.cpp
+++ b/libraries/fbx/src/FSTReader.cpp
@@ -84,7 +84,7 @@ void FSTReader::writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it)
QByteArray FSTReader::writeMapping(const QVariantHash& mapping) {
static const QStringList PREFERED_ORDER = QStringList() << NAME_FIELD << TYPE_FIELD << SCALE_FIELD << FILENAME_FIELD
- << TEXDIR_FIELD << SCRIPT_FIELD << JOINT_FIELD << FREE_JOINT_FIELD
+ << MARKETPLACE_ID_FIELD << TEXDIR_FIELD << SCRIPT_FIELD << JOINT_FIELD << FREE_JOINT_FIELD
<< BLENDSHAPE_FIELD << JOINT_INDEX_FIELD;
QBuffer buffer;
buffer.open(QIODevice::WriteOnly);
diff --git a/libraries/fbx/src/FSTReader.h b/libraries/fbx/src/FSTReader.h
index 00244877b3..993d7c3148 100644
--- a/libraries/fbx/src/FSTReader.h
+++ b/libraries/fbx/src/FSTReader.h
@@ -18,6 +18,7 @@
static const QString NAME_FIELD = "name";
static const QString TYPE_FIELD = "type";
static const QString FILENAME_FIELD = "filename";
+static const QString MARKETPLACE_ID_FIELD = "marketplaceID";
static const QString TEXDIR_FIELD = "texdir";
static const QString LOD_FIELD = "lod";
static const QString JOINT_INDEX_FIELD = "jointIndex";
diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp
index 5721ac9334..989661cb81 100644
--- a/libraries/networking/src/AccountManager.cpp
+++ b/libraries/networking/src/AccountManager.cpp
@@ -208,6 +208,44 @@ void AccountManager::setSessionID(const QUuid& sessionID) {
}
}
+QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth::Type authType) {
+ QNetworkRequest networkRequest;
+ networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+ networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter());
+
+ networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER,
+ uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit());
+
+ QUrl requestURL = _authURL;
+
+ if (requestURL.isEmpty()) { // Assignment client doesn't set _authURL.
+ requestURL = getMetaverseServerURL();
+ }
+
+ if (path.startsWith("/")) {
+ requestURL.setPath(path);
+ } else {
+ requestURL.setPath("/" + path);
+ }
+
+ if (authType != AccountManagerAuth::None ) {
+ if (hasValidAccessToken()) {
+ networkRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER,
+ _accountInfo.getAccessToken().authorizationHeaderValue());
+ } else {
+ if (authType == AccountManagerAuth::Required) {
+ qCDebug(networking) << "No valid access token present. Bailing on invoked request to"
+ << path << "that requires authentication";
+ return QNetworkRequest();
+ }
+ }
+ }
+
+ networkRequest.setUrl(requestURL);
+
+ return networkRequest;
+}
+
void AccountManager::sendRequest(const QString& path,
AccountManagerAuth::Type authType,
QNetworkAccessManager::Operation operation,
@@ -231,46 +269,10 @@ void AccountManager::sendRequest(const QString& path,
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
- QNetworkRequest networkRequest;
- networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
- networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter());
-
- networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER,
- uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit());
-
- QUrl requestURL = _authURL;
-
- if (requestURL.isEmpty()) { // Assignment client doesn't set _authURL.
- requestURL = getMetaverseServerURL();
- }
-
- if (path.startsWith("/")) {
- requestURL.setPath(path);
- } else {
- requestURL.setPath("/" + path);
- }
-
- if (!query.isEmpty()) {
- requestURL.setQuery(query);
- }
-
- if (authType != AccountManagerAuth::None ) {
- if (hasValidAccessToken()) {
- networkRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER,
- _accountInfo.getAccessToken().authorizationHeaderValue());
- } else {
- if (authType == AccountManagerAuth::Required) {
- qCDebug(networking) << "No valid access token present. Bailing on invoked request to"
- << path << "that requires authentication";
- return;
- }
- }
- }
-
- networkRequest.setUrl(requestURL);
+ QNetworkRequest networkRequest = createRequest(path, authType);
if (VERBOSE_HTTP_REQUEST_DEBUGGING) {
- qCDebug(networking) << "Making a request to" << qPrintable(requestURL.toString());
+ qCDebug(networking) << "Making a request to" << qPrintable(networkRequest.url().toString());
if (!dataByteArray.isEmpty()) {
qCDebug(networking) << "The POST/PUT body -" << QString(dataByteArray);
diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h
index d5406707e7..ca2b826c98 100644
--- a/libraries/networking/src/AccountManager.h
+++ b/libraries/networking/src/AccountManager.h
@@ -28,7 +28,8 @@
class JSONCallbackParameters {
public:
- JSONCallbackParameters(QObject* callbackReceiver = nullptr, const QString& jsonCallbackMethod = QString(),
+ JSONCallbackParameters(QObject* callbackReceiver = nullptr,
+ const QString& jsonCallbackMethod = QString(),
const QString& errorCallbackMethod = QString());
bool isEmpty() const { return !callbackReceiver; }
@@ -39,11 +40,11 @@ public:
};
namespace AccountManagerAuth {
- enum Type {
- None,
- Required,
- Optional
- };
+enum Type {
+ None,
+ Required,
+ Optional,
+};
}
Q_DECLARE_METATYPE(AccountManagerAuth::Type);
@@ -60,6 +61,7 @@ class AccountManager : public QObject, public Dependency {
public:
AccountManager(UserAgentGetter userAgentGetter = DEFAULT_USER_AGENT_GETTER);
+ QNetworkRequest createRequest(QString path, AccountManagerAuth::Type authType);
Q_INVOKABLE void sendRequest(const QString& path,
AccountManagerAuth::Type authType,
QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation,
@@ -84,7 +86,7 @@ public:
void requestProfile();
DataServerAccountInfo& getAccountInfo() { return _accountInfo; }
- void setAccountInfo(const DataServerAccountInfo &newAccountInfo);
+ void setAccountInfo(const DataServerAccountInfo& newAccountInfo);
static QJsonObject dataObjectFromResponse(QNetworkReply* requestReply);
@@ -104,7 +106,10 @@ public:
public slots:
void requestAccessToken(const QString& login, const QString& password);
void requestAccessTokenWithSteam(QByteArray authSessionTicket);
- void requestAccessTokenWithAuthCode(const QString& authCode, const QString& clientId, const QString& clientSecret, const QString& redirectUri);
+ void requestAccessTokenWithAuthCode(const QString& authCode,
+ const QString& clientId,
+ const QString& clientSecret,
+ const QString& redirectUri);
void refreshAccessToken();
void requestAccessTokenFinished();
@@ -159,4 +164,4 @@ private:
bool _limitedCommerce { false };
};
-#endif // hifi_AccountManager_h
+#endif // hifi_AccountManager_h
diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp
index 063885e782..8b9e37569c 100644
--- a/libraries/networking/src/LimitedNodeList.cpp
+++ b/libraries/networking/src/LimitedNodeList.cpp
@@ -113,8 +113,6 @@ LimitedNodeList::LimitedNodeList(int socketListenPort, int dtlsListenPort) :
// handle when a socket connection has its receiver side reset - might need to emit clientConnectionToNodeReset
connect(&_nodeSocket, &udt::Socket::clientHandshakeRequestComplete, this, &LimitedNodeList::clientConnectionToSockAddrReset);
- _packetStatTimer.start();
-
if (_stunSockAddr.getAddress().isNull()) {
// we don't know the stun server socket yet, add it to unfiltered once known
connect(&_stunSockAddr, &HifiSockAddr::lookupCompleted, this, &LimitedNodeList::addSTUNHandlerToUnfiltered);
@@ -378,12 +376,6 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe
return false;
}
-void LimitedNodeList::collectPacketStats(const NLPacket& packet) {
- // stat collection for packets
- ++_numCollectedPackets;
- _numCollectedBytes += packet.getDataSize();
-}
-
void LimitedNodeList::fillPacketHeader(const NLPacket& packet, HMACAuth* hmacAuth) {
if (!PacketTypeEnum::getNonSourcedPackets().contains(packet.getType())) {
packet.writeSourceID(getSessionLocalID());
@@ -414,7 +406,6 @@ qint64 LimitedNodeList::sendUnreliablePacket(const NLPacket& packet, const HifiS
Q_ASSERT_X(!packet.isReliable(), "LimitedNodeList::sendUnreliablePacket",
"Trying to send a reliable packet unreliably.");
- collectPacketStats(packet);
fillPacketHeader(packet, hmacAuth);
return _nodeSocket.writePacket(packet, sockAddr);
@@ -436,7 +427,6 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const HifiS
HMACAuth* hmacAuth) {
Q_ASSERT(!packet->isPartOfMessage());
if (packet->isReliable()) {
- collectPacketStats(*packet);
fillPacketHeader(*packet, hmacAuth);
auto size = packet->getDataSize();
@@ -490,7 +480,6 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr packetList,
for (std::unique_ptr& packet : packetList->_packets) {
NLPacket* nlPacket = static_cast(packet.get());
- collectPacketStats(*nlPacket);
fillPacketHeader(*nlPacket);
}
@@ -505,7 +494,6 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr packetList,
for (std::unique_ptr& packet : packetList->_packets) {
NLPacket* nlPacket = static_cast(packet.get());
- collectPacketStats(*nlPacket);
fillPacketHeader(*nlPacket, destinationNode.getAuthenticateHash());
}
@@ -832,23 +820,6 @@ SharedNodePointer LimitedNodeList::soloNodeOfType(NodeType_t nodeType) {
});
}
-void LimitedNodeList::getPacketStats(float& packetsInPerSecond, float& bytesInPerSecond, float& packetsOutPerSecond, float& bytesOutPerSecond) {
- packetsInPerSecond = (float) getPacketReceiver().getInPacketCount() / ((float) _packetStatTimer.elapsed() / 1000.0f);
- bytesInPerSecond = (float) getPacketReceiver().getInByteCount() / ((float) _packetStatTimer.elapsed() / 1000.0f);
-
- packetsOutPerSecond = (float) _numCollectedPackets / ((float) _packetStatTimer.elapsed() / 1000.0f);
- bytesOutPerSecond = (float) _numCollectedBytes / ((float) _packetStatTimer.elapsed() / 1000.0f);
-}
-
-void LimitedNodeList::resetPacketStats() {
- getPacketReceiver().resetCounters();
-
- _numCollectedPackets = 0;
- _numCollectedBytes = 0;
-
- _packetStatTimer.restart();
-}
-
void LimitedNodeList::removeSilentNodes() {
QSet killedNodes;
diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h
index 78d4d5810f..450fad96a9 100644
--- a/libraries/networking/src/LimitedNodeList.h
+++ b/libraries/networking/src/LimitedNodeList.h
@@ -183,9 +183,6 @@ public:
unsigned int broadcastToNodes(std::unique_ptr packet, const NodeSet& destinationNodeTypes);
SharedNodePointer soloNodeOfType(NodeType_t nodeType);
- void getPacketStats(float& packetsInPerSecond, float& bytesInPerSecond, float& packetsOutPerSecond, float& bytesOutPerSecond);
- void resetPacketStats();
-
std::unique_ptr constructPingPacket(const QUuid& nodeId, PingType_t pingType = PingType::Agnostic);
std::unique_ptr constructPingReplyPacket(ReceivedMessage& message);
@@ -377,7 +374,6 @@ protected:
qint64 sendPacket(std::unique_ptr packet, const Node& destinationNode,
const HifiSockAddr& overridenSockAddr);
- void collectPacketStats(const NLPacket& packet);
void fillPacketHeader(const NLPacket& packet, HMACAuth* hmacAuth = nullptr);
void setLocalSocket(const HifiSockAddr& sockAddr);
@@ -406,10 +402,6 @@ protected:
PacketReceiver* _packetReceiver;
- std::atomic _numCollectedPackets { 0 };
- std::atomic _numCollectedBytes { 0 };
-
- QElapsedTimer _packetStatTimer;
NodePermissions _permissions;
QPointer _initialSTUNTimer;
diff --git a/libraries/networking/src/PacketReceiver.cpp b/libraries/networking/src/PacketReceiver.cpp
index 83be481914..962ceab00f 100644
--- a/libraries/networking/src/PacketReceiver.cpp
+++ b/libraries/networking/src/PacketReceiver.cpp
@@ -212,18 +212,12 @@ void PacketReceiver::handleVerifiedPacket(std::unique_ptr packet) {
auto nlPacket = NLPacket::fromBase(std::move(packet));
auto receivedMessage = QSharedPointer::create(*nlPacket);
- _inPacketCount += 1;
- _inByteCount += nlPacket->size();
-
handleVerifiedMessage(receivedMessage, true);
}
void PacketReceiver::handleVerifiedMessagePacket(std::unique_ptr packet) {
auto nlPacket = NLPacket::fromBase(std::move(packet));
- _inPacketCount += 1;
- _inByteCount += nlPacket->size();
-
auto key = std::pair(nlPacket->getSenderSockAddr(), nlPacket->getMessageNumber());
auto it = _pendingMessages.find(key);
QSharedPointer message;
diff --git a/libraries/networking/src/PacketReceiver.h b/libraries/networking/src/PacketReceiver.h
index 4b4d260409..e29a0d6e5a 100644
--- a/libraries/networking/src/PacketReceiver.h
+++ b/libraries/networking/src/PacketReceiver.h
@@ -49,13 +49,8 @@ public:
PacketReceiver(const PacketReceiver&) = delete;
PacketReceiver& operator=(const PacketReceiver&) = delete;
-
- int getInPacketCount() const { return _inPacketCount; }
- int getInByteCount() const { return _inByteCount; }
void setShouldDropPackets(bool shouldDropPackets) { _shouldDropPackets = shouldDropPackets; }
-
- void resetCounters() { _inPacketCount = 0; _inByteCount = 0; }
// If deliverPending is false, ReceivedMessage will only be delivered once all packets for the message have
// been received. If deliverPending is true, ReceivedMessage will be delivered as soon as the first packet
@@ -87,8 +82,7 @@ private:
QMutex _packetListenerLock;
QHash _messageListenerMap;
- int _inPacketCount = 0;
- int _inByteCount = 0;
+
bool _shouldDropPackets = false;
QMutex _directConnectSetMutex;
QSet _directlyConnectedObjects;
diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp
index 13d4e0bf8b..bdba47f0ed 100644
--- a/libraries/networking/src/ThreadedAssignment.cpp
+++ b/libraries/networking/src/ThreadedAssignment.cpp
@@ -94,15 +94,11 @@ void ThreadedAssignment::commonInit(const QString& targetName, NodeType_t nodeTy
void ThreadedAssignment::addPacketStatsAndSendStatsPacket(QJsonObject statsObject) {
auto nodeList = DependencyManager::get();
- float packetsInPerSecond, bytesInPerSecond, packetsOutPerSecond, bytesOutPerSecond;
- nodeList->getPacketStats(packetsInPerSecond, bytesInPerSecond, packetsOutPerSecond, bytesOutPerSecond);
- nodeList->resetPacketStats();
-
QJsonObject ioStats;
- ioStats["inbound_bytes_per_s"] = bytesInPerSecond;
- ioStats["inbound_packets_per_s"] = packetsInPerSecond;
- ioStats["outbound_bytes_per_s"] = bytesOutPerSecond;
- ioStats["outbound_packets_per_s"] = packetsOutPerSecond;
+ ioStats["inbound_kbps"] = nodeList->getInboundKbps();
+ ioStats["inbound_pps"] = nodeList->getInboundPPS();
+ ioStats["outbound_kbps"] = nodeList->getOutboundKbps();
+ ioStats["outbound_pps"] = nodeList->getOutboundPPS();
statsObject["io_stats"] = ioStats;
diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp
index 6273c61762..6d717419da 100644
--- a/libraries/networking/src/udt/PacketHeaders.cpp
+++ b/libraries/networking/src/udt/PacketHeaders.cpp
@@ -38,6 +38,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
return static_cast(EntityQueryPacketVersion::ConicalFrustums);
case PacketType::AvatarIdentity:
case PacketType::AvatarData:
+ return static_cast(AvatarMixerPacketVersion::CollisionFlag);
case PacketType::BulkAvatarData:
case PacketType::KillAvatar:
return static_cast(AvatarMixerPacketVersion::GrabTraits);
diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h
index c1d91681df..50d5756f19 100644
--- a/libraries/networking/src/udt/PacketHeaders.h
+++ b/libraries/networking/src/udt/PacketHeaders.h
@@ -306,7 +306,8 @@ enum class AvatarMixerPacketVersion : PacketVersion {
MigrateAvatarEntitiesToTraits,
FarGrabJointsRedux,
JointTransScaled,
- GrabTraits
+ GrabTraits,
+ CollisionFlag
};
enum class DomainConnectRequestVersion : PacketVersion {
diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js
index 65abf791a5..2b9a738202 100644
--- a/scripts/system/avatarapp.js
+++ b/scripts/system/avatarapp.js
@@ -62,7 +62,8 @@ function getMyAvatar() {
function getMyAvatarSettings() {
return {
dominantHand: MyAvatar.getDominantHand(),
- collisionsEnabled : MyAvatar.getCollisionsEnabled(),
+ collisionsEnabled: MyAvatar.getCollisionsEnabled(),
+ otherAvatarsCollisionsEnabled: MyAvatar.getOtherAvatarsCollisionsEnabled(),
collisionSoundUrl : MyAvatar.collisionSoundURL,
animGraphUrl: MyAvatar.getAnimGraphUrl(),
animGraphOverrideUrl : MyAvatar.getAnimGraphOverrideUrl(),
@@ -135,6 +136,13 @@ function onCollisionsEnabledChanged(enabled) {
}
}
+function onOtherAvatarsCollisionsEnabledChanged(enabled) {
+ if (currentAvatarSettings.otherAvatarsCollisionsEnabled !== enabled) {
+ currentAvatarSettings.otherAvatarsCollisionsEnabled = enabled;
+ sendToQml({ 'method': 'settingChanged', 'name': 'otherAvatarsCollisionsEnabled', 'value': enabled })
+ }
+}
+
function onNewCollisionSoundUrl(url) {
if(currentAvatarSettings.collisionSoundUrl !== url) {
currentAvatarSettings.collisionSoundUrl = url;
@@ -323,6 +331,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
currentAvatar.avatarScale = message.avatarScale;
MyAvatar.setDominantHand(message.settings.dominantHand);
+ MyAvatar.setOtherAvatarsCollisionsEnabled(message.settings.otherAvatarsCollisionsEnabled);
MyAvatar.setCollisionsEnabled(message.settings.collisionsEnabled);
MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl;
MyAvatar.setAnimGraphOverrideUrl(message.settings.animGraphOverrideUrl);
@@ -513,6 +522,7 @@ function off() {
MyAvatar.skeletonModelURLChanged.disconnect(onSkeletonModelURLChanged);
MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged);
MyAvatar.collisionsEnabledChanged.disconnect(onCollisionsEnabledChanged);
+ MyAvatar.otherAvatarsCollisionsEnabledChanged.disconnect(onOtherAvatarsCollisionsEnabledChanged);
MyAvatar.newCollisionSoundURL.disconnect(onNewCollisionSoundUrl);
MyAvatar.animGraphUrlChanged.disconnect(onAnimGraphUrlChanged);
MyAvatar.targetScaleChanged.disconnect(onTargetScaleChanged);
@@ -533,6 +543,7 @@ function on() {
MyAvatar.skeletonModelURLChanged.connect(onSkeletonModelURLChanged);
MyAvatar.dominantHandChanged.connect(onDominantHandChanged);
MyAvatar.collisionsEnabledChanged.connect(onCollisionsEnabledChanged);
+ MyAvatar.otherAvatarsCollisionsEnabledChanged.connect(onOtherAvatarsCollisionsEnabledChanged);
MyAvatar.newCollisionSoundURL.connect(onNewCollisionSoundUrl);
MyAvatar.animGraphUrlChanged.connect(onAnimGraphUrlChanged);
MyAvatar.targetScaleChanged.connect(onTargetScaleChanged);
diff --git a/tools/nitpick/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp
index 0b93ce44e5..59be26c383 100644
--- a/tools/nitpick/src/AWSInterface.cpp
+++ b/tools/nitpick/src/AWSInterface.cpp
@@ -476,24 +476,33 @@ void AWSInterface::updateAWS() {
QStringList parts = nextDirectory.split('/');
QString filename = parts[parts.length() - 3] + "/" + parts[parts.length() - 2] + "/" + parts[parts.length() - 1];
- stream << "data = open('" << _workingDirectory << "/" << filename << "/"
- << "Actual Image.png"
- << "', 'rb')\n";
-
- stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n";
-
- stream << "data = open('" << _workingDirectory << "/" << filename << "/"
- << "Expected Image.png"
- << "', 'rb')\n";
-
- stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n";
-
- if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) {
+ // The directory may contain either 'Result.txt', or 3 images (and a text file named 'TestResults.txt' that is not used)
+ if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/Result.txt")) {
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
- << "Difference Image.png"
- << "', 'rb')\n";
+ << "Result.txt"
+ << "', 'rb')\n";
- stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n";
+ stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Result.txt" << "', Body=data)\n\n";
+ } else {
+ stream << "data = open('" << _workingDirectory << "/" << filename << "/"
+ << "Actual Image.png"
+ << "', 'rb')\n";
+
+ stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n";
+
+ stream << "data = open('" << _workingDirectory << "/" << filename << "/"
+ << "Expected Image.png"
+ << "', 'rb')\n";
+
+ stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n";
+
+ if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) {
+ stream << "data = open('" << _workingDirectory << "/" << filename << "/"
+ << "Difference Image.png"
+ << "', 'rb')\n";
+
+ stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n";
+ }
}
}
@@ -510,31 +519,39 @@ void AWSInterface::updateAWS() {
// We need to concatenate the last 3 components, to get `TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ]/successes/engine.render.effect.bloom.00000`
QStringList parts = nextDirectory.split('/');
QString filename = parts[parts.length() - 3] + "/" + parts[parts.length() - 2] + "/" + parts[parts.length() - 1];
-
- stream << "data = open('" << _workingDirectory << "/" << filename << "/"
- << "Actual Image.png"
- << "', 'rb')\n";
-
- stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n";
-
- stream << "data = open('" << _workingDirectory << "/" << filename << "/"
- << "Expected Image.png"
- << "', 'rb')\n";
-
- stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n";
-
- if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) {
+ // The directory may contain either 'Result.txt', or 3 images (and a text file named 'TestResults.txt' that is not used)
+ if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/Result.txt")) {
stream << "data = open('" << _workingDirectory << "/" << filename << "/"
- << "Difference Image.png"
- << "', 'rb')\n";
+ << "Result.txt"
+ << "', 'rb')\n";
- stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n";
+ stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Result.txt" << "', Body=data)\n\n";
+ } else {
+ stream << "data = open('" << _workingDirectory << "/" << filename << "/"
+ << "Actual Image.png"
+ << "', 'rb')\n";
+
+ stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n";
+
+ stream << "data = open('" << _workingDirectory << "/" << filename << "/"
+ << "Expected Image.png"
+ << "', 'rb')\n";
+
+ stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n";
+
+ if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) {
+ stream << "data = open('" << _workingDirectory << "/" << filename << "/"
+ << "Difference Image.png"
+ << "', 'rb')\n";
+
+ stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n";
+ }
}
}
stream << "data = open('" << _workingDirectory << "/" << _resultsFolder << "/" << HTML_FILENAME << "', 'rb')\n";
stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << _resultsFolder << "/"
- << HTML_FILENAME << "', Body=data, ContentType='text/html')\n";
+ << HTML_FILENAME << "', Body=data, ContentType='text/html')\n";
file.close();
@@ -548,7 +565,7 @@ void AWSInterface::updateAWS() {
connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); });
connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater()));
connect(process, static_cast(&QProcess::finished), this,
- [=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); });
+ [=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); });
#ifdef Q_OS_WIN
QStringList parameters = QStringList() << filename;
diff --git a/tools/nitpick/src/ui/Nitpick.cpp b/tools/nitpick/src/ui/Nitpick.cpp
index 38124e1f39..0bd397715b 100644
--- a/tools/nitpick/src/ui/Nitpick.cpp
+++ b/tools/nitpick/src/ui/Nitpick.cpp
@@ -38,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) {
_ui.statusLabel->setText("");
_ui.plainTextEdit->setReadOnly(true);
- setWindowTitle("Nitpick - v1.3");
+ setWindowTitle("Nitpick - v1.3.2");
}
Nitpick::~Nitpick() {