From 3943fe2861511ff26014f66fa0f15c918b4ea844 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Mon, 10 Dec 2018 21:29:03 +0100 Subject: [PATCH 01/72] avatar packager initial --- .../resources/qml/hifi/AvatarPackager.qml | 54 +++++++++++++++++++ interface/src/Application.cpp | 3 ++ interface/src/Menu.cpp | 7 +++ interface/src/Menu.h | 1 + interface/src/avatar/AvatarPackager.cpp | 44 +++++++++++++++ interface/src/avatar/AvatarPackager.h | 35 ++++++++++++ interface/src/avatar/AvatarProject.cpp | 27 ++++++++++ interface/src/avatar/AvatarProject.h | 45 ++++++++++++++++ 8 files changed, 216 insertions(+) create mode 100644 interface/resources/qml/hifi/AvatarPackager.qml create mode 100644 interface/src/avatar/AvatarPackager.cpp create mode 100644 interface/src/avatar/AvatarPackager.h create mode 100644 interface/src/avatar/AvatarProject.cpp create mode 100644 interface/src/avatar/AvatarProject.h diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml new file mode 100644 index 0000000000..787c8d6b69 --- /dev/null +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -0,0 +1,54 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtQml.Models 2.1 +import QtGraphicalEffects 1.0 +import "../controlsUit" 1.0 as HifiControls +import "../stylesUit" 1.0 +import "../windows" as Windows +import "../dialogs" + +Windows.ScrollingWindow { + id: root + objectName: "AvatarPackager" + width: 480 + height: 706 + title: "Avatar Packager" + resizable: true + opacity: parent.opacity + destroyOnHidden: true + implicitWidth: 384; implicitHeight: 640 + minSize: Qt.vector2d(200, 300) + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + RalewaySemiBold { + id: displayNameLabel + size: 24; + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 25 + text: 'Avatar Projects' + } + + HifiControls.Button { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: displayNameLabel.bottom + text: qsTr("Open Avatar Project") + // color: hifi.buttons.blue + colorScheme: root.colorScheme + height: 30 + onClicked: function() { + let avatarProject = AvatarPackagerCore.openAvatarProject(); + if (avatarProject) { + console.log("LOAD COMPLETE"); + } else { + console.log("LOAD FAILED"); + } + } + } + } +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 0b53e24a8e..7c4975e7a9 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" @@ -912,6 +913,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -2639,6 +2641,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 e9a44b1e87..4056458dcc 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" @@ -145,6 +146,12 @@ Menu::Menu() { addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0, qApp, SLOT(packageModel())); + // Edit > Avatar Packager + action = addActionToQMenuAndActionHash(editMenu, MenuOption::AvatarPackager); + connect(action, &QAction::triggered, [] { + DependencyManager::get()->open(); + }); + // Edit > Reload All Content addActionToQMenuAndActionHash(editMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); 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/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp new file mode 100644 index 0000000000..ef127d4459 --- /dev/null +++ b/interface/src/avatar/AvatarPackager.cpp @@ -0,0 +1,44 @@ +// +// 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 +#include +#include + +#include +#include "ModelSelector.h" + +bool AvatarPackager::open() { + static const QUrl url{ "hifi/AvatarPackager.qml" }; + + const auto packageModelDialogCreated = [=](QQmlContext* context, QObject* newObject) { + context->setContextProperty("AvatarPackagerCore", this); + }; + DependencyManager::get()->show(url, "AvatarPackager", packageModelDialogCreated); + return true; +} + +QObject* AvatarPackager::openAvatarProject() { + // TODO: use QML file browser here, could handle this on QML side instead + static Setting::Handle lastModelBrowseLocation("LastModelBrowseLocation", + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); + const QString filename = QFileDialog::getOpenFileName(nullptr, "Select your model file ...", + lastModelBrowseLocation.get(), + "Model files (*.fst *.fbx)"); + QFileInfo fileInfo(filename); + + if (fileInfo.isFile() && fileInfo.completeSuffix().contains(QRegExp("fst|fbx|FST|FBX"))) { + return AvatarProject::openAvatarProject(fileInfo.absoluteFilePath()); + } + return nullptr; +} diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h new file mode 100644 index 0000000000..8f29f98c0b --- /dev/null +++ b/interface/src/avatar/AvatarPackager.h @@ -0,0 +1,35 @@ +// +// 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 "avatar/AvatarProject.h" + +class AvatarPackager : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + bool open(); + + Q_INVOKABLE void openAvatarProjectWithoutReturnType() { + openAvatarProject(); + } + + Q_INVOKABLE QObject* openAvatarProject(); +}; + +#endif // hifi_AvatarPackager_h diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp new file mode 100644 index 0000000000..876bd1725b --- /dev/null +++ b/interface/src/avatar/AvatarProject.cpp @@ -0,0 +1,27 @@ +// +// 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 +// + +#include "AvatarProject.h" + +AvatarProject* AvatarProject::openAvatarProject(QString path) { + const auto pathToLower = path.toLower(); + if (pathToLower.endsWith(".fst")) { + // TODO: do we open FSTs from any path? + return new AvatarProject(path); + } + + if (pathToLower.endsWith(".fbx")) { + // TODO: Create FST here: + + } + + return nullptr; +} diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h new file mode 100644 index 0000000000..cc347c3d4b --- /dev/null +++ b/interface/src/avatar/AvatarProject.h @@ -0,0 +1,45 @@ +// +// 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 + +class AvatarProject : public QObject { +public: + Q_INVOKABLE bool write() { + // Write FST here + return false; + } + + Q_INVOKABLE QObject* upload() { + // TODO: create new AvatarProjectUploader here, launch it and return it for status tracking in QML + return nullptr; + } + + /** + * returns the AvatarProject or a nullptr on failure. + */ + static AvatarProject* openAvatarProject(QString path); + +private: + AvatarProject(QString fstPath) { + + } + + ~AvatarProject() { + // TODO: cleanup FST / AvatarProjectUploader etc. + } +}; + +#endif // hifi_AvatarProject_h From c0bd9121fbe1935cb537feccf9d28f470e94becf Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 12 Dec 2018 19:55:14 +0100 Subject: [PATCH 02/72] open avatar projects --- .../resources/qml/hifi/AvatarPackager.qml | 66 +++++++++++++++---- .../qml/hifi/avatarPackager/AvatarProject.qml | 59 +++++++++++++++++ interface/src/avatar/AvatarPackager.cpp | 19 ++---- interface/src/avatar/AvatarPackager.h | 9 +-- interface/src/avatar/AvatarProject.cpp | 2 +- interface/src/avatar/AvatarProject.h | 26 +++++++- 6 files changed, 149 insertions(+), 32 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarProject.qml diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 787c8d6b69..d93a3900a8 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -7,6 +7,7 @@ import "../controlsUit" 1.0 as HifiControls import "../stylesUit" 1.0 import "../windows" as Windows import "../dialogs" +import "avatarPackager" Windows.ScrollingWindow { id: root @@ -19,35 +20,78 @@ Windows.ScrollingWindow { destroyOnHidden: true implicitWidth: 384; implicitHeight: 640 minSize: Qt.vector2d(200, 300) + + //HifiConstants { id: hifi } + + AvatarProject { + id: avatarProject + colorScheme: root.colorScheme + } + Rectangle { + id: avatarPackagerMain anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom RalewaySemiBold { - id: displayNameLabel + id: avatarPackagerLabel size: 24; anchors.left: parent.left anchors.top: parent.top anchors.topMargin: 25 - text: 'Avatar Projects' + anchors.bottomMargin: 25 + text: 'Avatar Packager' } HifiControls.Button { + id: createProjectButton anchors.left: parent.left anchors.right: parent.right - anchors.top: displayNameLabel.bottom - text: qsTr("Open Avatar Project") - // color: hifi.buttons.blue + anchors.top: avatarPackagerLabel.bottom + text: qsTr("Create Project") colorScheme: root.colorScheme height: 30 onClicked: function() { - let avatarProject = AvatarPackagerCore.openAvatarProject(); - if (avatarProject) { - console.log("LOAD COMPLETE"); - } else { - console.log("LOAD FAILED"); - } + + } + } + HifiControls.Button { + id: openProjectButton + anchors.left: parent.left + anchors.right: parent.right + anchors.top: createProjectButton.bottom + text: qsTr("Open Avatar Project") + colorScheme: root.colorScheme + height: 30 + onClicked: function() { + var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects"; + console.log("path = " + avatarProjectsPath); + + // TODO: make the dialog modal + + var browser = desktop.fileDialog({ + selectDirectory: false, + dir: fileDialogHelper.pathToUrl(avatarProjectsPath), + filter: "Avatar Project FST Files (*.fst)", + title: "Open Project (.fst)" + }); + + browser.canceled.connect(function() { + + }); + + browser.selectedFile.connect(function(fileUrl) { + console.log("FOUND PATH " + fileUrl); + let fstFilePath = fileDialogHelper.urlToPath(fileUrl); + let avatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); + if (avatarProject) { + console.log("LOAD COMPLETE"); + console.log("file dir = " + AvatarPackagerCore.currentAvatarProject.projectFolderPath); + avatarProject.visible = true; + avatarPackagerMain.visible = false; + } + }); } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml new file mode 100644 index 0000000000..a34c709567 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -0,0 +1,59 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.0 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Rectangle { + id: avatarProject + + HifiConstants { id: hifi } + + + z: 3 + property int colorScheme; + + visible: false + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + + RalewaySemiBold { + id: avatarProjectLabel + size: 24; + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 25 + anchors.bottomMargin: 25 + text: 'Avatar Project' + } + HifiControls.Button { + id: openFolderButton + anchors.left: parent.left + anchors.right: parent.right + anchors.top: avatarProjectLabel.bottom + text: qsTr("Open Project Folder") + colorScheme: avatarProject.colorScheme + height: 30 + onClicked: function() { + fileDialogHelper.openDirectory(AvatarPackagerCore.currentAvatarProject.projectFolderPath); + } + } + HifiControls.Button { + id: uploadButton + anchors.left: parent.left + anchors.right: parent.right + anchors.top: openFolderButton.bottom + text: qsTr("Upload") + color: hifi.buttons.blue + colorScheme: avatarProject.colorScheme + height: 30 + onClicked: function() { + + } + } +} diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index ef127d4459..c6c1318eab 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -12,11 +12,9 @@ #include "AvatarPackager.h" #include -#include #include #include -#include "ModelSelector.h" bool AvatarPackager::open() { static const QUrl url{ "hifi/AvatarPackager.qml" }; @@ -28,17 +26,10 @@ bool AvatarPackager::open() { return true; } -QObject* AvatarPackager::openAvatarProject() { - // TODO: use QML file browser here, could handle this on QML side instead - static Setting::Handle lastModelBrowseLocation("LastModelBrowseLocation", - QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); - const QString filename = QFileDialog::getOpenFileName(nullptr, "Select your model file ...", - lastModelBrowseLocation.get(), - "Model files (*.fst *.fbx)"); - QFileInfo fileInfo(filename); - - if (fileInfo.isFile() && fileInfo.completeSuffix().contains(QRegExp("fst|fbx|FST|FBX"))) { - return AvatarProject::openAvatarProject(fileInfo.absoluteFilePath()); +QObject* AvatarPackager::openAvatarProject(QString avatarProjectFSTPath) { + if (_currentAvatarProject) { + _currentAvatarProject->deleteLater(); } - return nullptr; + _currentAvatarProject = AvatarProject::openAvatarProject(avatarProjectFSTPath); + return _currentAvatarProject; } diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index 8f29f98c0b..a8ef6c6421 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -21,15 +21,16 @@ class AvatarPackager : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY + Q_PROPERTY(QObject* currentAvatarProject READ getAvatarProject) public: bool open(); - Q_INVOKABLE void openAvatarProjectWithoutReturnType() { - openAvatarProject(); - } + Q_INVOKABLE QObject* openAvatarProject(QString avatarProjectFSTPath); - Q_INVOKABLE QObject* openAvatarProject(); +private: + Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; + AvatarProject* _currentAvatarProject { nullptr }; }; #endif // hifi_AvatarPackager_h diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 876bd1725b..a3b22ab2c2 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -1,5 +1,5 @@ // -// AvatarProject.h +// AvatarProject.cpp // // // Created by Thijs Wenker on 12/7/2018 diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index cc347c3d4b..2114b147dd 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -14,8 +14,15 @@ #define hifi_AvatarProject_h #include +#include +#include class AvatarProject : public QObject { + Q_OBJECT + Q_PROPERTY(QString projectFolderPath READ getProjectPath) + Q_PROPERTY(QString projectFSTPath READ getFSTPath) + Q_PROPERTY(QString projectFBXPath READ getFBXPath) + public: Q_INVOKABLE bool write() { // Write FST here @@ -33,13 +40,28 @@ public: static AvatarProject* openAvatarProject(QString path); private: - AvatarProject(QString fstPath) { - + AvatarProject(QString fstPath) : + _fstPath(fstPath) { + auto fileInfo = QFileInfo(_fstPath); + _projectPath = fileInfo.absoluteDir().absolutePath(); + + _fstPath = _projectPath + "TemporaryFBX.fbx"; + } ~AvatarProject() { // TODO: cleanup FST / AvatarProjectUploader etc. } + + Q_INVOKABLE QString getProjectPath() const { return _projectPath; } + Q_INVOKABLE QString getFSTPath() const { return _fstPath; } + Q_INVOKABLE QString getFBXPath() const { return _fbxPath; } + + QString _projectPath; + QString _fstPath; + // TODO: handle this in the FST Class + QString _fbxPath; + }; #endif // hifi_AvatarProject_h From 96673bb5b9087194ebf610c0a65a416f3ee27a0e Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 12 Dec 2018 21:15:44 +0100 Subject: [PATCH 03/72] show project page --- .../resources/qml/hifi/AvatarPackager.qml | 121 +++++++++--------- .../qml/hifi/avatarPackager/AvatarProject.qml | 10 +- interface/src/avatar/AvatarPackager.cpp | 3 +- 3 files changed, 70 insertions(+), 64 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index d93a3900a8..2044529427 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -22,76 +22,83 @@ Windows.ScrollingWindow { minSize: Qt.vector2d(200, 300) //HifiConstants { id: hifi } - - AvatarProject { - id: avatarProject - colorScheme: root.colorScheme - } - Rectangle { - id: avatarPackagerMain anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom - RalewaySemiBold { - id: avatarPackagerLabel - size: 24; + AvatarProject { + id: avatarProject + colorScheme: root.colorScheme + visible: false + } + + Rectangle { + id: avatarPackagerMain anchors.left: parent.left + anchors.right: parent.right anchors.top: parent.top - anchors.topMargin: 25 - anchors.bottomMargin: 25 - text: 'Avatar Packager' - } - - HifiControls.Button { - id: createProjectButton - anchors.left: parent.left - anchors.right: parent.right - anchors.top: avatarPackagerLabel.bottom - text: qsTr("Create Project") - colorScheme: root.colorScheme - height: 30 - onClicked: function() { - + anchors.bottom: parent.bottom + RalewaySemiBold { + id: avatarPackagerLabel + size: 24; + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 25 + anchors.bottomMargin: 25 + text: 'Avatar Packager' } - } - HifiControls.Button { - id: openProjectButton - anchors.left: parent.left - anchors.right: parent.right - anchors.top: createProjectButton.bottom - text: qsTr("Open Avatar Project") - colorScheme: root.colorScheme - height: 30 - onClicked: function() { - var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects"; - console.log("path = " + avatarProjectsPath); - // TODO: make the dialog modal + HifiControls.Button { + id: createProjectButton + anchors.left: parent.left + anchors.right: parent.right + anchors.top: avatarPackagerLabel.bottom + text: qsTr("Create Project") + colorScheme: root.colorScheme + height: 30 + onClicked: function() { + + } + } + HifiControls.Button { + id: openProjectButton + anchors.left: parent.left + anchors.right: parent.right + anchors.top: createProjectButton.bottom + text: qsTr("Open Avatar Project") + colorScheme: root.colorScheme + height: 30 + onClicked: function() { + var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects"; + console.log("path = " + avatarProjectsPath); - var browser = desktop.fileDialog({ - selectDirectory: false, - dir: fileDialogHelper.pathToUrl(avatarProjectsPath), - filter: "Avatar Project FST Files (*.fst)", - title: "Open Project (.fst)" - }); + // TODO: make the dialog modal - browser.canceled.connect(function() { + var browser = desktop.fileDialog({ + selectDirectory: false, + dir: fileDialogHelper.pathToUrl(avatarProjectsPath), + filter: "Avatar Project FST Files (*.fst)", + title: "Open Project (.fst)" + }); + + browser.canceled.connect(function() { - }); + }); - browser.selectedFile.connect(function(fileUrl) { - console.log("FOUND PATH " + fileUrl); - let fstFilePath = fileDialogHelper.urlToPath(fileUrl); - let avatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); - if (avatarProject) { - console.log("LOAD COMPLETE"); - console.log("file dir = " + AvatarPackagerCore.currentAvatarProject.projectFolderPath); - avatarProject.visible = true; - avatarPackagerMain.visible = false; - } - }); + browser.selectedFile.connect(function(fileUrl) { + console.log("FOUND PATH " + fileUrl); + let fstFilePath = fileDialogHelper.urlToPath(fileUrl); + let currentAvatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); + if (currentAvatarProject) { + console.log("LOAD COMPLETE"); + console.log("file dir = " + AvatarPackagerCore.currentAvatarProject.projectFolderPath); + + avatarPackagerMain.visible = false; + avatarProject.visible = true; + } + }); + } } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index a34c709567..0a6ed6c459 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -7,15 +7,13 @@ import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 Rectangle { - id: avatarProject + id: root HifiConstants { id: hifi } - - z: 3 property int colorScheme; - visible: false + visible: true anchors.left: parent.left anchors.right: parent.right @@ -37,7 +35,7 @@ Rectangle { anchors.right: parent.right anchors.top: avatarProjectLabel.bottom text: qsTr("Open Project Folder") - colorScheme: avatarProject.colorScheme + colorScheme: root.colorScheme height: 30 onClicked: function() { fileDialogHelper.openDirectory(AvatarPackagerCore.currentAvatarProject.projectFolderPath); @@ -50,7 +48,7 @@ Rectangle { anchors.top: openFolderButton.bottom text: qsTr("Upload") color: hifi.buttons.blue - colorScheme: avatarProject.colorScheme + colorScheme: root.colorScheme height: 30 onClicked: function() { diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index c6c1318eab..1088c862d4 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -28,7 +28,8 @@ bool AvatarPackager::open() { QObject* AvatarPackager::openAvatarProject(QString avatarProjectFSTPath) { if (_currentAvatarProject) { - _currentAvatarProject->deleteLater(); + //_currentAvatarProject->deleteLater(); + //_currentAvatarProject = nullptr; } _currentAvatarProject = AvatarProject::openAvatarProject(avatarProjectFSTPath); return _currentAvatarProject; From 78c4c2599e007cfec2765b3fff4d1c8a99c73f93 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 13 Dec 2018 13:22:52 -0800 Subject: [PATCH 04/72] Add start of marketplace uploading and project file list --- .../resources/qml/hifi/AvatarPackager.qml | 7 +- .../qml/hifi/avatarPackager/AvatarProject.qml | 38 ++++++--- interface/src/avatar/AvatarPackager.cpp | 18 +++++ interface/src/avatar/AvatarPackager.h | 13 +++- interface/src/avatar/AvatarProject.cpp | 47 ++++++++++- interface/src/avatar/AvatarProject.h | 36 +++++---- .../src/avatar/MarketplaceItemUploader.cpp | 57 ++++++++++++++ .../src/avatar/MarketplaceItemUploader.h | 55 +++++++++++++ libraries/fbx/src/FST.cpp | 45 +++++++++++ libraries/fbx/src/FST.h | 49 ++++++++++++ libraries/networking/src/AccountManager.cpp | 78 ++++++++++--------- libraries/networking/src/AccountManager.h | 34 ++++---- 12 files changed, 388 insertions(+), 89 deletions(-) create mode 100644 interface/src/avatar/MarketplaceItemUploader.cpp create mode 100644 interface/src/avatar/MarketplaceItemUploader.h create mode 100644 libraries/fbx/src/FST.cpp create mode 100644 libraries/fbx/src/FST.h diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 2044529427..434cd4128f 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -23,14 +23,13 @@ Windows.ScrollingWindow { //HifiConstants { id: hifi } Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom + anchors.fill: parent + AvatarProject { id: avatarProject colorScheme: root.colorScheme visible: false + anchors.fill: parent } Rectangle { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 0a6ed6c459..c365d7436e 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -13,26 +13,23 @@ Rectangle { property int colorScheme; + color: "blue" + visible: true - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom + + anchors.fill: parent RalewaySemiBold { id: avatarProjectLabel size: 24; - anchors.left: parent.left - anchors.top: parent.top + width: parent.width anchors.topMargin: 25 anchors.bottomMargin: 25 text: 'Avatar Project' } HifiControls.Button { id: openFolderButton - anchors.left: parent.left - anchors.right: parent.right + width: parent.width anchors.top: avatarProjectLabel.bottom text: qsTr("Open Project Folder") colorScheme: root.colorScheme @@ -43,15 +40,32 @@ Rectangle { } HifiControls.Button { id: uploadButton - anchors.left: parent.left - anchors.right: parent.right + width: parent.width anchors.top: openFolderButton.bottom text: qsTr("Upload") color: hifi.buttons.blue colorScheme: root.colorScheme height: 30 onClicked: function() { - + } + } + Text { + id: modelText + anchors.top: uploadButton.bottom + height: 30 + text: parent.height + } + Rectangle { + color: 'white' + visible: AvatarPackagerCore.currentAvatarProject !== null + width: parent.width + anchors.top: modelText.bottom + height: 1000 + + ListView { + anchors.fill: parent + model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles + delegate: Text { text: 'File: ' + modelData } } } } diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index 1088c862d4..3fdf193087 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -15,6 +15,18 @@ #include #include +#include "ModelSelector.h" +#include + +#include +#include + +std::once_flag setupQMLTypesFlag; +AvatarPackager::AvatarPackager() { + std::call_once(setupQMLTypesFlag, []() { + qmlRegisterType(); + }); +} bool AvatarPackager::open() { static const QUrl url{ "hifi/AvatarPackager.qml" }; @@ -32,5 +44,11 @@ QObject* AvatarPackager::openAvatarProject(QString avatarProjectFSTPath) { //_currentAvatarProject = nullptr; } _currentAvatarProject = AvatarProject::openAvatarProject(avatarProjectFSTPath); + emit avatarProjectChanged(); return _currentAvatarProject; } + +QObject* AvatarPackager::uploadItem() { + std::vector filePaths; + return new MarketplaceItemUploader(QUuid(), filePaths); +} diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index a8ef6c6421..f002631f17 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -21,16 +21,23 @@ class AvatarPackager : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY - Q_PROPERTY(QObject* currentAvatarProject READ getAvatarProject) + Q_PROPERTY(QObject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged) public: + AvatarPackager(); bool open(); Q_INVOKABLE QObject* openAvatarProject(QString avatarProjectFSTPath); +signals: + void avatarProjectChanged(); + private: Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; - AvatarProject* _currentAvatarProject { nullptr }; + //Q_INVOKABLE QObject* openAvatarProject(); + Q_INVOKABLE QObject* uploadItem(); + + AvatarProject* _currentAvatarProject{ nullptr }; }; -#endif // hifi_AvatarPackager_h +#endif // hifi_AvatarPackager_h diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index a3b22ab2c2..32fe4febcd 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -11,17 +11,56 @@ #include "AvatarProject.h" -AvatarProject* AvatarProject::openAvatarProject(QString path) { +#include + +#include +#include +#include +#include + +AvatarProject* AvatarProject::openAvatarProject(const QString& path) { const auto pathToLower = path.toLower(); if (pathToLower.endsWith(".fst")) { - // TODO: do we open FSTs from any path? - return new AvatarProject(path); + QFile file{ path }; + if (!file.open(QIODevice::ReadOnly)) { + return nullptr; + } + return new AvatarProject(path, file.readAll()); } if (pathToLower.endsWith(".fbx")) { // TODO: Create FST here: - } return nullptr; } + +AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : + _fstPath(fstPath), _fst(fstPath, FSTReader::readMapping(data)) { + + _directory = QFileInfo(_fstPath).absoluteDir(); + + //_projectFiles = _directory.entryList(); + refreshProjectFiles(); + + auto fileInfo = QFileInfo(_fstPath); + _projectPath = fileInfo.absoluteDir().absolutePath(); +} + +void AvatarProject::appendDirectory(QString prefix, QDir dir) { + qDebug() << "Inside of " << prefix << dir.absolutePath(); + auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; + for (auto& entry : dir.entryInfoList({}, flags)) { + if (entry.isFile()) { + _projectFiles.append(prefix + "/" + entry.fileName()); + } else if (entry.isDir()) { + qDebug() << "Found dir " << entry.absoluteFilePath() << " in " << dir.absolutePath(); + appendDirectory(prefix + dir.dirName() + "/", entry.absoluteFilePath()); + } + } +} + +void AvatarProject::refreshProjectFiles() { + _projectFiles.clear(); + appendDirectory("", _directory); +} diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 2114b147dd..6dc64cda6f 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -13,12 +13,21 @@ #ifndef hifi_AvatarProject_h #define hifi_AvatarProject_h +#include "FST.h" + +#include #include #include #include +#include +#include class AvatarProject : public QObject { Q_OBJECT + Q_PROPERTY(FST* fst READ getFST) + + Q_PROPERTY(QStringList projectFiles MEMBER _projectFiles) + Q_PROPERTY(QString projectFolderPath READ getProjectPath) Q_PROPERTY(QString projectFSTPath READ getFSTPath) Q_PROPERTY(QString projectFBXPath READ getFBXPath) @@ -37,17 +46,10 @@ public: /** * returns the AvatarProject or a nullptr on failure. */ - static AvatarProject* openAvatarProject(QString path); + static AvatarProject* openAvatarProject(const QString& path); private: - AvatarProject(QString fstPath) : - _fstPath(fstPath) { - auto fileInfo = QFileInfo(_fstPath); - _projectPath = fileInfo.absoluteDir().absolutePath(); - - _fstPath = _projectPath + "TemporaryFBX.fbx"; - - } + AvatarProject(const QString& fstPath, const QByteArray& data); ~AvatarProject() { // TODO: cleanup FST / AvatarProjectUploader etc. @@ -55,13 +57,19 @@ private: Q_INVOKABLE QString getProjectPath() const { return _projectPath; } Q_INVOKABLE QString getFSTPath() const { return _fstPath; } - Q_INVOKABLE QString getFBXPath() const { return _fbxPath; } + Q_INVOKABLE QString getFBXPath() const { return _fst.getModelPath(); } + FST* getFST() { return &_fst; } + + void refreshProjectFiles(); + void appendDirectory(QString prefix, QDir dir); + + FST _fst; + + QDir _directory; + QStringList _projectFiles{}; QString _projectPath; QString _fstPath; - // TODO: handle this in the FST Class - QString _fbxPath; - }; -#endif // hifi_AvatarProject_h +#endif // hifi_AvatarProject_h diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp new file mode 100644 index 0000000000..c2671aadec --- /dev/null +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -0,0 +1,57 @@ +// +// 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 + +#include + +MarketplaceItemUploader::MarketplaceItemUploader(QUuid marketplaceID, std::vector filePaths) + : _filePaths(filePaths), _marketplaceID(marketplaceID) { + +} + +void MarketplaceItemUploader::send() { + auto accountManager = DependencyManager::get(); + auto request = accountManager->createRequest("/marketplace/item", AccountManagerAuth::Required); + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QByteArray data; + + /* + auto reply = networkAccessManager.post(request, data); + + connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + auto error = reply->error(); + if (error == QNetworkReply::NoError) { + } else { + } + emit complete(); + }); + */ + + QTimer* timer = new QTimer(); + timer->setInterval(1000); + connect(timer, &QTimer::timeout, this, [this, timer]() { + if (progress <= 1.0f) { + progress += 0.1; + emit uploadProgress(progress * 100.0f, 100.0f); + } else { + emit complete(); + timer->stop(); + } + }); + timer->start(); +} diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h new file mode 100644 index 0000000000..2a3071244e --- /dev/null +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -0,0 +1,55 @@ +// +// 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 +#include + +class QNetworkReply; + +class MarketplaceItemUploader : public QObject { + Q_OBJECT +public: + enum class Error + { + None, + ItemNotUpdateable, + ItemDoesNotExist, + RequestTimedOut, + Unknown + }; + enum class State + { + Ready, + Sent + }; + + MarketplaceItemUploader(QUuid markertplaceID, std::vector filePaths); + + float progress{ 0.0f }; + + Q_INVOKABLE void send(); + +signals: + void uploadProgress(qint64 bytesSent, qint64 bytesTotal); + void complete(); + +private: + + QNetworkReply* _reply; + QUuid _marketplaceID; + std::vector _filePaths; +}; + +#endif // hifi_MarketplaceItemUploader_h diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp new file mode 100644 index 0000000000..6574b66e51 --- /dev/null +++ b/libraries/fbx/src/FST.cpp @@ -0,0 +1,45 @@ +// +// 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 + +FST::FST(QString fstPath, QVariantHash data) : _fstPath(fstPath) { + if (data.contains("name")) { + _name = data["name"].toString(); + data.remove("name"); + } + + if (data.contains("filename")) { + _modelPath = data["filename"].toString(); + data.remove("filename"); + } + + _other = data; +} + +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); +} \ No newline at end of file diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h new file mode 100644 index 0000000000..e8c67c6c6b --- /dev/null +++ b/libraries/fbx/src/FST.h @@ -0,0 +1,49 @@ +// +// 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 + +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) +public: + FST(QString fstPath, QVariantHash data); + + QString absoluteModelPath() const; + + QString getName() const { return _name; } + void setName(const QString& name); + + QString getModelPath() const { return _modelPath; } + void setModelPath(const QString& modelPath); + + QUuid getMarketplaceID() const { return _marketplaceID; } + +signals: + void nameChanged(const QString& name); + void modelPathChanged(const QString& modelPath); + +private: + QString _fstPath; + + QString _name{}; + QString _modelPath{}; + QUuid _marketplaceID{}; + + QVariantHash _other{}; +}; + +#endif // hifi_FST_h 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..77f20472fa 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,12 @@ public: }; namespace AccountManagerAuth { - enum Type { - None, - Required, - Optional - }; +enum Type +{ + None, + Required, + Optional +}; } Q_DECLARE_METATYPE(AccountManagerAuth::Type); @@ -60,6 +62,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 +87,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 +107,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(); @@ -148,15 +154,15 @@ private: QUrl _authURL; DataServerAccountInfo _accountInfo; - bool _isWaitingForTokenRefresh { false }; - bool _isAgent { false }; + bool _isWaitingForTokenRefresh{ false }; + bool _isAgent{ false }; - bool _isWaitingForKeypairResponse { false }; + bool _isWaitingForKeypairResponse{ false }; QByteArray _pendingPrivateKey; - QUuid _sessionID { QUuid::createUuid() }; + QUuid _sessionID{ QUuid::createUuid() }; - bool _limitedCommerce { false }; + bool _limitedCommerce{ false }; }; -#endif // hifi_AccountManager_h +#endif // hifi_AccountManager_h From 2269447741c405d150a38340afaab51bd6040df5 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 13 Dec 2018 14:52:36 -0800 Subject: [PATCH 05/72] Fix AvatarPackager QML size --- .../resources/qml/hifi/AvatarPackager.qml | 8 ++-- .../qml/hifi/avatarPackager/AvatarProject.qml | 43 +++++++++---------- interface/src/avatar/AvatarProject.cpp | 5 ++- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 434cd4128f..5a51a3c873 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -21,9 +21,11 @@ Windows.ScrollingWindow { implicitWidth: 384; implicitHeight: 640 minSize: Qt.vector2d(200, 300) + //HifiConstants { id: hifi } - Rectangle { - anchors.fill: parent + Item { + height: pane.height + width: pane.width AvatarProject { id: avatarProject @@ -32,7 +34,7 @@ Windows.ScrollingWindow { anchors.fill: parent } - Rectangle { + Item { id: avatarPackagerMain anchors.left: parent.left anchors.right: parent.right diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index c365d7436e..085f1acdce 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -6,18 +6,17 @@ import QtGraphicalEffects 1.0 import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 -Rectangle { +Item { id: root HifiConstants { id: hifi } property int colorScheme; - color: "blue" - visible: true anchors.fill: parent + anchors.margins: 10 RalewaySemiBold { id: avatarProjectLabel @@ -26,11 +25,13 @@ Rectangle { anchors.topMargin: 25 anchors.bottomMargin: 25 text: 'Avatar Project' + color: "white" } HifiControls.Button { id: openFolderButton width: parent.width anchors.top: avatarProjectLabel.bottom + anchors.topMargin: 10 text: qsTr("Open Project Folder") colorScheme: root.colorScheme height: 30 @@ -38,28 +39,15 @@ Rectangle { fileDialogHelper.openDirectory(AvatarPackagerCore.currentAvatarProject.projectFolderPath); } } - HifiControls.Button { - id: uploadButton - width: parent.width - anchors.top: openFolderButton.bottom - text: qsTr("Upload") - color: hifi.buttons.blue - colorScheme: root.colorScheme - height: 30 - onClicked: function() { - } - } - Text { - id: modelText - anchors.top: uploadButton.bottom - height: 30 - text: parent.height - } Rectangle { color: 'white' visible: AvatarPackagerCore.currentAvatarProject !== null - width: parent.width - anchors.top: modelText.bottom + anchors.top: openFolderButton.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: uploadButton.top + anchors.topMargin: 10 + anchors.bottomMargin: 10 height: 1000 ListView { @@ -68,4 +56,15 @@ Rectangle { delegate: Text { text: 'File: ' + modelData } } } + HifiControls.Button { + id: uploadButton + width: parent.width + anchors.bottom: parent.bottom + text: qsTr("Upload") + color: hifi.buttons.blue + colorScheme: root.colorScheme + height: 30 + onClicked: function() { + } + } } diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 32fe4febcd..c7ea7e52ac 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -17,6 +17,7 @@ #include #include #include +#include AvatarProject* AvatarProject::openAvatarProject(const QString& path) { const auto pathToLower = path.toLower(); @@ -25,7 +26,9 @@ AvatarProject* AvatarProject::openAvatarProject(const QString& path) { if (!file.open(QIODevice::ReadOnly)) { return nullptr; } - return new AvatarProject(path, file.readAll()); + auto project = new AvatarProject(path, file.readAll()); + QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership); + return project; } if (pathToLower.endsWith(".fbx")) { From ad471387f7212559fcbf34d4f37d1cbe0010ed85 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 14 Dec 2018 16:10:42 -0800 Subject: [PATCH 06/72] Integrate marketplace upload API --- .../resources/qml/controlsUit/Button.qml | 7 + .../resources/qml/hifi/AvatarPackager.qml | 3 +- .../qml/hifi/avatarPackager/AvatarProject.qml | 110 ++++++++- interface/src/Application.cpp | 2 +- interface/src/avatar/AvatarManager.cpp | 4 +- interface/src/avatar/AvatarPackager.cpp | 6 +- interface/src/avatar/AvatarPackager.h | 1 - interface/src/avatar/AvatarProject.cpp | 12 +- interface/src/avatar/AvatarProject.h | 7 +- .../src/avatar/MarketplaceItemUploader.cpp | 225 ++++++++++++++++-- .../src/avatar/MarketplaceItemUploader.h | 52 +++- scripts/system/html/js/entityProperties.js | 9 + 12 files changed, 386 insertions(+), 52 deletions(-) diff --git a/interface/resources/qml/controlsUit/Button.qml b/interface/resources/qml/controlsUit/Button.qml index 6ea7ce4b4c..9d92ff5e9a 100644 --- a/interface/resources/qml/controlsUit/Button.qml +++ b/interface/resources/qml/controlsUit/Button.qml @@ -28,6 +28,10 @@ Original.Button { width: hifi.dimensions.buttonWidth height: hifi.dimensions.controlLineHeight + property size implicitPadding: Qt.size(20, 16) + property int implicitWidth: content.implicitWidth + implicitPadding.width + property int implicitHeight: content.implicitHeight + implicitPadding.height + HifiConstants { id: hifi } onHoveredChanged: { @@ -89,6 +93,9 @@ Original.Button { } contentItem: Item { + id: content + implicitWidth: (buttonGlyph.visible ? buttonGlyph.implicitWidth : 0) + buttonText.implicitWidth + implicitHeight: buttonText.implicitHeight HiFiGlyphs { id: buttonGlyph; visible: control.buttonGlyph !== ""; diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 5a51a3c873..d5e21d9653 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -47,7 +47,7 @@ Windows.ScrollingWindow { anchors.top: parent.top anchors.topMargin: 25 anchors.bottomMargin: 25 - text: 'Avatar Packager' + text: 'Avatar Packager ' + parent.width + " " + parent.height } HifiControls.Button { @@ -72,6 +72,7 @@ Windows.ScrollingWindow { height: 30 onClicked: function() { var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects"; + var avatarProjectsPath = "C:/Users/ryanh/Documents/High Fidelity Avatars"; console.log("path = " + avatarProjectsPath); // TODO: make the dialog modal diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 085f1acdce..4f42927676 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -3,6 +3,8 @@ 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 @@ -12,6 +14,7 @@ Item { HifiConstants { id: hifi } property int colorScheme; + property var uploader: undefined; visible: true @@ -58,13 +61,118 @@ Item { } HifiControls.Button { id: uploadButton + width: parent.width + height: 30 anchors.bottom: parent.bottom + text: qsTr("Upload") color: hifi.buttons.blue colorScheme: root.colorScheme - height: 30 onClicked: function() { + console.log("Uploading"); + parent.uploader = AvatarPackagerCore.currentAvatarProject.upload(); + console.log("uploader: "+ parent.uploader); + parent.uploader.uploadProgress.connect(function(uploaded, total) { + console.log("Uploader progress: " + uploaded + " / " + total); + }); + parent.uploader.completed.connect(function() { + try { + var response = JSON.parse(parent.uploader.responseData); + console.log("Uploader complete! " + response); + uploadStatus.text = response.status; + } catch (e) { + console.log("Error parsing JSON: " + parent.uploader.reponseData); + } + }); + parent.uploader.send(); } } + + Rectangle { + id: uploadingScreen + + visible: !!root.uploader + anchors.fill: parent + + color: "black" + + Item { + visible: !!root.uploader && !root.uploader.complete + + anchors.fill: parent + + AnimatedImage { + id: uploadSpinner + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + source: "../../../icons/loader-snake-64-w.gif" + playing: true + z: 10000 + } + } + + Item { + visible: !!root.uploader && root.uploader.complete + + anchors.fill: parent + + HiFiGlyphs { + id: successIcon + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + size: 128 + text: "\ue01a" + color: "#1FC6A6" + } + + Text { + text: "Congratulations!" + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: successIcon.bottom + + color: "white" + } + + HifiControls.Button { + width: implicitWidth + height: implicitHeight + + anchors.bottom: parent.bottom + anchors.right: parent.right + + text: "View in Inventory" + + color: hifi.buttons.blue + colorScheme: root.colorScheme + onClicked: function() { + console.log("Opening in inventory"); + } + } + } + + Column { + Text { + id: uploadStatus + + text: "Uploading" + color: "white" + + } + Text { + text: "State: " + (!!root.uploader ? root.uploader.state : " NONE") + color: "white" + } + } + + } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7c4975e7a9..10f8d66855 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -460,7 +460,7 @@ public: // Don't actually crash in debug builds, in case this apparent deadlock is simply from // the developer actively debugging code #ifdef NDEBUG - deadlockDetectionCrash(); + //deadlockDetectionCrash(); #endif } } diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 7ca18ca258..1a6b510ea1 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -536,6 +536,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents // my avatar. (Other user machines will make a similar analysis and inject sound for their collisions.) if (collision.idA.isNull() || collision.idB.isNull()) { auto myAvatar = getMyAvatar(); + myAvatar->collisionWithEntity(collision); auto collisionSound = myAvatar->getCollisionSound(); if (collisionSound) { const auto characterController = myAvatar->getCharacterController(); @@ -571,9 +572,8 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents auto injector = AudioInjector::playSoundAndDelete(collisionSound, options); _collisionInjectors.emplace_back(injector); } - myAvatar->collisionWithEntity(collision); - return; } + return; } } } diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index 3fdf193087..04ecc87067 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -25,6 +25,7 @@ std::once_flag setupQMLTypesFlag; AvatarPackager::AvatarPackager() { std::call_once(setupQMLTypesFlag, []() { qmlRegisterType(); + qmlRegisterType(); }); } @@ -47,8 +48,3 @@ QObject* AvatarPackager::openAvatarProject(QString avatarProjectFSTPath) { emit avatarProjectChanged(); return _currentAvatarProject; } - -QObject* AvatarPackager::uploadItem() { - std::vector filePaths; - return new MarketplaceItemUploader(QUuid(), filePaths); -} diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index f002631f17..def82b6311 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -35,7 +35,6 @@ signals: private: Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; //Q_INVOKABLE QObject* openAvatarProject(); - Q_INVOKABLE QObject* uploadItem(); AvatarProject* _currentAvatarProject{ nullptr }; }; diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index c7ea7e52ac..cc23027562 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -41,6 +41,9 @@ AvatarProject* AvatarProject::openAvatarProject(const QString& path) { AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : _fstPath(fstPath), _fst(fstPath, FSTReader::readMapping(data)) { + _fstFilename = QFileInfo(_fstPath).fileName(); + qDebug() << "Pointers: " << this << &_fst; + _directory = QFileInfo(_fstPath).absoluteDir(); //_projectFiles = _directory.entryList(); @@ -51,13 +54,12 @@ AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : } void AvatarProject::appendDirectory(QString prefix, QDir dir) { - qDebug() << "Inside of " << prefix << dir.absolutePath(); auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; for (auto& entry : dir.entryInfoList({}, flags)) { if (entry.isFile()) { - _projectFiles.append(prefix + "/" + entry.fileName()); + //_projectFiles.append(prefix + "/" + entry.fileName()); + _projectFiles.append(entry.absoluteFilePath()); } else if (entry.isDir()) { - qDebug() << "Found dir " << entry.absoluteFilePath() << " in " << dir.absolutePath(); appendDirectory(prefix + dir.dirName() + "/", entry.absoluteFilePath()); } } @@ -67,3 +69,7 @@ void AvatarProject::refreshProjectFiles() { _projectFiles.clear(); appendDirectory("", _directory); } + +Q_INVOKABLE MarketplaceItemUploader* AvatarProject::upload() { + return new MarketplaceItemUploader("test_avatar", "blank description", _fstFilename, QUuid(), _projectFiles); +} diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 6dc64cda6f..1a0ed5cc5e 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -13,6 +13,7 @@ #ifndef hifi_AvatarProject_h #define hifi_AvatarProject_h +#include "MarketplaceItemUploader.h" #include "FST.h" #include @@ -38,10 +39,7 @@ public: return false; } - Q_INVOKABLE QObject* upload() { - // TODO: create new AvatarProjectUploader here, launch it and return it for status tracking in QML - return nullptr; - } + Q_INVOKABLE MarketplaceItemUploader* upload(); /** * returns the AvatarProject or a nullptr on failure. @@ -70,6 +68,7 @@ private: QStringList _projectFiles{}; QString _projectPath; QString _fstPath; + QString _fstFilename; }; #endif // hifi_AvatarProject_h diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index c2671aadec..7a5abacce4 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -14,44 +14,221 @@ #include #include -#include - -MarketplaceItemUploader::MarketplaceItemUploader(QUuid marketplaceID, std::vector filePaths) - : _filePaths(filePaths), _marketplaceID(marketplaceID) { +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +MarketplaceItemUploader::MarketplaceItemUploader(QString title, + QString description, + QString rootFilename, + QUuid marketplaceID, + QStringList filePaths) : + _title(title), + _description(description), _rootFilename(rootFilename), _filePaths(filePaths), _marketplaceID(marketplaceID) { + qWarning() << "File paths: " << _filePaths.join(", "); + //_marketplaceID = QUuid::fromString(QLatin1String("{50dbd62f-cb6b-4be4-afb8-1ef8bd2dffa8}")); +} + +void MarketplaceItemUploader::setState(State newState) { + qDebug() << "Setting uploader state to: " << newState; + + _state = newState; + emit stateChanged(newState); + if (newState == State::Complete) { + emit completed(); + } } 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("/marketplace/item", AccountManagerAuth::Required); + auto request = accountManager->createRequest(path, AccountManagerAuth::None); + + qWarning() << "Request url is: " << request.url(); QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QByteArray data; - /* - auto reply = networkAccessManager.post(request, data); - - connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + QNetworkReply* reply = networkAccessManager.get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { + auto doc = QJsonDocument::fromJson(reply->readAll()); + auto error = reply->error(); if (error == QNetworkReply::NoError) { - } else { - } - emit complete(); - }); - */ + 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 }; + } - QTimer* timer = new QTimer(); - timer->setInterval(1000); - connect(timer, &QTimer::timeout, this, [this, timer]() { - if (progress <= 1.0f) { - progress += 0.1; - emit uploadProgress(progress * 100.0f, 100.0f); + 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; + int id; + std::tie(success, id) = extractCategoryID(); + qDebug() << "Done " << success << id; + if (!success) { + qWarning() << "Failed to find marketplace category id"; + _error = Error::Unknown; + setState(State::Complete); + } else { + doUploadAvatar(); + } } else { - emit complete(); - timer->stop(); + _error = Error::Unknown; + setState(State::Complete); } }); - timer->start(); +} + +void MarketplaceItemUploader::doUploadAvatar() { + QBuffer buffer{ &_fileData }; + //buffer.open(QIODevice::WriteOnly); + QuaZip zip{ &buffer }; + if (!zip.open(QuaZip::Mode::mdAdd)) { + qWarning() << "Failed to open zip!!"; + } + + for (auto& filePath : _filePaths) { + qWarning() << "Zipping: " << filePath; + QFileInfo fileInfo{ filePath }; + + QuaZipFile zipFile{ &zip }; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileInfo.fileName()))) { + qWarning() << "Could not open zip file:" << zipFile.getZipError(); + _error = Error::Unknown; + setState(State::Complete); + return; + } + QFile file{ filePath }; + if (file.open(QIODevice::ReadOnly)) { + zipFile.write(file.readAll()); + } else { + qWarning() << "Failed to open: " << filePath; + } + 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); + qWarning() << "Request url is: " << request.url(); + + QJsonObject root{ { "marketplace_item", + QJsonObject{ { "title", _title }, + { "description", _description }, + { "root_file_key", _rootFilename }, + { "category_ids", QJsonArray({ 5 }) }, + //{ "attributions", QJsonArray({ QJsonObject{ { "name", "" }, { "link", "" } } }) }, + { "license", 0 }, + { "files", QString::fromLatin1(_fileData.toBase64()) } } } }; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + QJsonDocument doc{ root }; + + qWarning() << "data: " << doc.toJson(); + + _fileData.toBase64(); + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkReply* reply{ nullptr }; + if (creating) { + reply = networkAccessManager.post(request, doc.toJson()); + } else { + reply = networkAccessManager.put(request, doc.toJson()); + } + + connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + _responseData = reply->readAll(); + qWarning() << "Finished request " << _responseData; + auto error = reply->error(); + if (error == QNetworkReply::NoError) { + doWaitForInventory(); + } else { + _error = Error::Unknown; + setState(State::Complete); + } + }); + + setState(State::UploadingAvatar); +} + +void MarketplaceItemUploader::doWaitForInventory() { + static const QString path = "/api/v1/commerce/inventory"; + + auto accountManager = DependencyManager::get(); + auto request = accountManager->createRequest(path, AccountManagerAuth::Required); + + qWarning() << "Request url is: " << request.url(); + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkReply* reply = networkAccessManager.post(request, ""); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + auto data = reply->readAll(); + qWarning() << "Finished inventory request " << data; + + auto error = reply->error(); + if (error == QNetworkReply::NoError) { + } else { + _error = Error::Unknown; + } + setState(State::Complete); + }); } diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index 2a3071244e..a0ec3f6991 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -20,36 +20,68 @@ class QNetworkReply; class MarketplaceItemUploader : public QObject { Q_OBJECT + + Q_PROPERTY(bool complete READ getComplete NOTIFY stateChanged) + Q_PROPERTY(State state READ getState NOTIFY stateChanged) + Q_PROPERTY(Error error READ getError) + Q_PROPERTY(QString responseData READ getResponseData) public: enum class Error { None, - ItemNotUpdateable, - ItemDoesNotExist, - RequestTimedOut, Unknown }; + Q_ENUM(Error); + enum class State { - Ready, - Sent + Idle, + GettingCategories, + UploadingAvatar, + WaitingForInventory, + Complete }; + Q_ENUM(State); - MarketplaceItemUploader(QUuid markertplaceID, std::vector filePaths); - - float progress{ 0.0f }; + MarketplaceItemUploader(QString title, + QString description, + QString rootFilename, + QUuid marketplaceID, + QStringList filePaths); Q_INVOKABLE void send(); + QString getResponseData() const { return _responseData; } + void setState(State newState); + State getState() const { return _state; } + bool getComplete() const { return _state == State::Complete; } + + Error getError() const { return _error; } + signals: void uploadProgress(qint64 bytesSent, qint64 bytesTotal); - void complete(); + void completed(); + void stateChanged(State newState); 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; - std::vector _filePaths; + + QString _responseData; + + QStringList _filePaths; + QByteArray _fileData; }; #endif // hifi_MarketplaceItemUploader_h diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index dc304c6803..23c7346df8 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -112,6 +112,15 @@ const GROUPS = [ type: "color", propertyID: "color", }, + { + label: "Alpha", + type: "", + type: "number", + min: 0, + max: 1, + step: 0.001, + propertyID: "alpha", + }, ] }, { From cb74313de8210710e6666836b838e1456786f487 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 19 Dec 2018 19:23:24 +0100 Subject: [PATCH 07/72] create projects / style changes --- .../resources/qml/hifi/AvatarPackager.qml | 196 ++++++++++++------ .../avatarPackager/AvatarPackagerFooter.qml | 24 +++ .../avatarPackager/AvatarPackagerHeader.qml | 71 +++++++ .../qml/hifi/avatarPackager/AvatarProject.qml | 55 +++-- .../avatarPackager/CreateAvatarProject.qml | 108 ++++++++++ .../avatarPackager/ProjectInputControl.qml | 76 +++++++ interface/src/avatar/AvatarPackager.cpp | 21 +- interface/src/avatar/AvatarPackager.h | 13 +- interface/src/avatar/AvatarProject.cpp | 83 ++++++-- interface/src/avatar/AvatarProject.h | 25 ++- libraries/fbx/src/FST.cpp | 137 +++++++++++- libraries/fbx/src/FST.h | 15 +- 12 files changed, 693 insertions(+), 131 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 5a51a3c873..8eb765716e 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -15,92 +15,160 @@ Windows.ScrollingWindow { width: 480 height: 706 title: "Avatar Packager" - resizable: true + resizable: false opacity: parent.opacity destroyOnHidden: true implicitWidth: 384; implicitHeight: 640 - minSize: Qt.vector2d(200, 300) + minSize: Qt.vector2d(480, 706) + HifiConstants { id: hifi } - //HifiConstants { id: hifi } Item { + id: windowContent height: pane.height width: pane.width + anchors.fill: parent - AvatarProject { - id: avatarProject - colorScheme: root.colorScheme - visible: false + // FIXME: modal overlay does not show + Rectangle { + id: modalOverlay anchors.fill: parent + z: 20000 + color: "#aa031b33" + clip: true + visible: true } - Item { - id: avatarPackagerMain - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - RalewaySemiBold { - id: avatarPackagerLabel - size: 24; - anchors.left: parent.left - anchors.top: parent.top - anchors.topMargin: 25 - anchors.bottomMargin: 25 - text: 'Avatar Packager' - } + Column { + id: avatarPackager + anchors.fill: parent + state: "main" + states: [ + State { + name: "main" + PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); faqEnabled: true; backButtonEnabled: false } + PropertyChanges { target: avatarPackagerMain; visible: true } + PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer } + }, + State { + name: "createProject" + PropertyChanges { target: avatarPackagerHeader; title: qsTr("Create Project") } + PropertyChanges { target: createAvatarProject; visible: true } + PropertyChanges { target: avatarPackagerFooter; content: createAvatarProject.footer } + }, + State { + name: "project" + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } + PropertyChanges { target: avatarProject; visible: true } + PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer } + } + ] - HifiControls.Button { - id: createProjectButton - anchors.left: parent.left - anchors.right: parent.right - anchors.top: avatarPackagerLabel.bottom - text: qsTr("Create Project") - colorScheme: root.colorScheme - height: 30 - onClicked: function() { - + AvatarPackagerHeader { + id: avatarPackagerHeader + onBackButtonClicked: { + avatarPackager.state = "main" } } - HifiControls.Button { - id: openProjectButton - anchors.left: parent.left - anchors.right: parent.right - anchors.top: createProjectButton.bottom - text: qsTr("Open Avatar Project") - colorScheme: root.colorScheme - height: 30 - onClicked: function() { - var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects"; - console.log("path = " + avatarProjectsPath); - // TODO: make the dialog modal + Item { + height: pane.height - avatarPackagerHeader.height - avatarPackagerFooter.height + width: pane.width - var browser = desktop.fileDialog({ - selectDirectory: false, - dir: fileDialogHelper.pathToUrl(avatarProjectsPath), - filter: "Avatar Project FST Files (*.fst)", - title: "Open Project (.fst)" - }); + Rectangle { + anchors.fill: parent + color: "#404040" + } - browser.canceled.connect(function() { - - }); + AvatarProject { + id: avatarProject + colorScheme: root.colorScheme + anchors.fill: parent + } - browser.selectedFile.connect(function(fileUrl) { - console.log("FOUND PATH " + fileUrl); - let fstFilePath = fileDialogHelper.urlToPath(fileUrl); - let currentAvatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); - if (currentAvatarProject) { - console.log("LOAD COMPLETE"); - console.log("file dir = " + AvatarPackagerCore.currentAvatarProject.projectFolderPath); - - avatarPackagerMain.visible = false; - avatarProject.visible = true; + 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: { + avatarPackager.state = "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: { + // TODO: make the dialog modal + let browser = desktop.fileDialog({ + selectDirectory: false, + dir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH), + filter: "Avatar Project FST Files (*.fst)", + title: "Open Project (.fst)", + }); + + browser.canceled.connect(function() { + + }); + + browser.selectedFile.connect(function(fileUrl) { + let fstFilePath = fileDialogHelper.urlToPath(fileUrl); + let currentAvatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); + if (currentAvatarProject) { + avatarPackager.state = "project"; + } + }); + } + } + } + Flow { + anchors { + fill: parent + topMargin: 18 + leftMargin: 16 + rightMargin: 16 + } + RalewayRegular { + size: 20 + color: "white" + text: qsTr("Use a custom avatar to express your identity") + } + RalewayRegular { + size: 20 + color: "white" + text: qsTr("To learn more about using this tool, visit our docs") + } + } } } + 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..526a2047e3 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml @@ -0,0 +1,24 @@ +import QtQuick 2.6 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Rectangle { + id: avatarPackagerFooter + + color: "#404040" + height: 74 + width: parent.width + + property var content: Item { } + + children: [background, content] + + property var background: Rectangle { + anchors.fill: parent + color: "#404040" + // TODO Use a shadow instead / border is just here for element debug purposes + border.width: 2; + } + +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml new file mode 100644 index 0000000000..6dcb1267d4 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -0,0 +1,71 @@ +import QtQuick 2.6 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Rectangle { + id: avatarPackagerHeader + + width: parent.width + height: 74 + color: "#252525" + + property alias title: title.text + property alias faqEnabled: faq.visible + property alias backButtonEnabled: back.visible + signal backButtonClicked + + RalewaySemiBold { + id: back + visible: true + size: 28 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: 16 + anchors.verticalCenter: back.verticalCenter + text: "◀" + color: "white" + MouseArea { + anchors.fill: parent + onClicked: avatarPackagerHeader.backButtonClicked() + hoverEnabled: true + onEntered: { state = "hovering" } + onExited: { state = "" } + states: [ + State { + name: "hovering" + PropertyChanges { + target: back + color: "gray" + } + } + ] + } + } + + RalewaySemiBold { + id: title + size: 28 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: back.visible ? back.right : parent.left + anchors.leftMargin: back.visible ? 11 : 21 + anchors.verticalCenter: title.verticalCenter + text: qsTr("Avatar Packager") + color: "white" + } + + RalewaySemiBold { + id: faq + visible: false + size: 28 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.rightMargin: 16 + anchors.verticalCenter: faq.verticalCenter + text: qsTr("FAQ") + color: "white" + } +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 085f1acdce..1e1d256024 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -11,36 +11,56 @@ Item { HifiConstants { id: hifi } - property int colorScheme; - - visible: true + property int colorScheme + visible: false anchors.fill: parent anchors.margins: 10 + property var footer: Item { + anchors.fill: parent + anchors.rightMargin: 17 + HifiControls.Button { + id: uploadButton + //width: parent.width + //anchors.bottom: parent.bottom + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + text: qsTr("Upload") + color: hifi.buttons.blue + colorScheme: root.colorScheme + width: 133 + height: 40 + onClicked: { + } + } + } + RalewaySemiBold { - id: avatarProjectLabel - size: 24; - width: parent.width + id: avatarFBXNameLabel + size: 14 + anchors.left: parent.left + anchors.top: parent.top anchors.topMargin: 25 anchors.bottomMargin: 25 - text: 'Avatar Project' - color: "white" + text: qsTr("FBX file here") } + HifiControls.Button { id: openFolderButton width: parent.width - anchors.top: avatarProjectLabel.bottom + anchors.top: avatarFBXNameLabel.bottom anchors.topMargin: 10 text: qsTr("Open Project Folder") colorScheme: root.colorScheme height: 30 - onClicked: function() { - fileDialogHelper.openDirectory(AvatarPackagerCore.currentAvatarProject.projectFolderPath); + onClicked: { + fileDialogHelper.openDirectory(fileDialogHelper.pathToUrl(AvatarPackagerCore.currentAvatarProject.projectFolderPath)); } } + Rectangle { - color: 'white' + color: "white" visible: AvatarPackagerCore.currentAvatarProject !== null anchors.top: openFolderButton.bottom anchors.left: parent.left @@ -56,15 +76,4 @@ Item { delegate: Text { text: 'File: ' + modelData } } } - HifiControls.Button { - id: uploadButton - width: parent.width - anchors.bottom: parent.bottom - text: qsTr("Upload") - color: hifi.buttons.blue - colorScheme: root.colorScheme - height: 30 - onClicked: function() { - } - } } diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml new file mode 100644 index 0000000000..4d1f745fa5 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -0,0 +1,108 @@ +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 + + 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") + onClicked: { + if (!AvatarPackagerCore.createAvatarProject(projectLocation.text, name.text, avatarModel.text, textureFolder.text)) { + Window.alert('Failed to create project') + return; + } + avatarPackager.state = "project"; + } + } + } + + visible: false + anchors.fill: parent + height: parent.height + width: parent.width + + Column { + id: createAvatarColumns + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 10 + + spacing: 17 + + ProjectInputControl { + id: name + label: "Name" + colorScheme: root.colorScheme + } + + ProjectInputControl { + id: projectLocation + label: "Specify Project Location" + colorScheme: root.colorScheme + browseEnabled: true + browseFolder: true + browseDir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + browseTitle: "Project Location" + text: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + onTextChanged: { + //TODO: valid folder? Does project with name exist here already? + } + } + + ProjectInputControl { + id: avatarModel + label: "Specify Avatar Model (.fbx)" + colorScheme: root.colorScheme + browseEnabled: true + browseFolder: false + browseDir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + browseFilter: "Avatar Model File (*.fbx)" + browseTitle: "Open Avatar Model (.fbx)" + onTextChanged: { + //TODO: try to get texture folder from fbx if none is set? + } + } + + ProjectInputControl { + id: textureFolder + label: "Specify Texture Folder" + colorScheme: root.colorScheme + browseEnabled: true + browseFolder: true + browseDir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + browseTitle: "Texture Folder" + onTextChanged: { + //TODO: valid folder? + + } + } + } + RalewayRegular { + text: "A folder with that name already exists at that location. Please choose a different project name or location." + color: "#EA4C5F"; + wrapMode: Text.WordWrap + size: 20 + anchors { + top: createAvatarColumns.bottom + bottom: parent.bottom + left: parent.left + right: parent.right + } + } +} diff --git a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml new file mode 100644 index 0000000000..472db47c2f --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml @@ -0,0 +1,76 @@ +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 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: { + // TODO: make the dialog modal + let browser = desktop.fileDialog({ + selectDirectory: browseFolder, + dir: browseDir, + filter: browseFilter, + title: browseTitle, + }); + + browser.canceled.connect(function() { + + }); + + browser.selectedFile.connect(function(fileUrl) { + input.text = fileDialogHelper.urlToPath(fileUrl); + }); + } + } + } +} diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index 3fdf193087..8075bc5bdc 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -12,6 +12,7 @@ #include "AvatarPackager.h" #include +#include #include #include @@ -25,6 +26,8 @@ std::once_flag setupQMLTypesFlag; AvatarPackager::AvatarPackager() { std::call_once(setupQMLTypesFlag, []() { qmlRegisterType(); + qRegisterMetaType(); + qRegisterMetaType(); }); } @@ -38,12 +41,24 @@ bool AvatarPackager::open() { return true; } -QObject* AvatarPackager::openAvatarProject(QString avatarProjectFSTPath) { +AvatarProject* AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) { if (_currentAvatarProject) { - //_currentAvatarProject->deleteLater(); - //_currentAvatarProject = nullptr; + _currentAvatarProject->deleteLater(); } _currentAvatarProject = AvatarProject::openAvatarProject(avatarProjectFSTPath); + qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; + QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); + emit avatarProjectChanged(); + return _currentAvatarProject; +} + +AvatarProject* AvatarPackager::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder) { + if (_currentAvatarProject) { + _currentAvatarProject->deleteLater(); + } + _currentAvatarProject = AvatarProject::createAvatarProject(avatarProjectName, avatarModelPath); + qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; + QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); emit avatarProjectChanged(); return _currentAvatarProject; } diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index f002631f17..e0268747c2 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -16,25 +16,28 @@ #include #include +#include "FileDialogHelper.h" + #include "avatar/AvatarProject.h" class AvatarPackager : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY - Q_PROPERTY(QObject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged) - -public: + Q_PROPERTY(AvatarProject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged) + Q_PROPERTY(QString AVATAR_PROJECTS_PATH READ getAvatarProjectsPath CONSTANT) +public: AvatarPackager(); bool open(); - Q_INVOKABLE QObject* openAvatarProject(QString avatarProjectFSTPath); + Q_INVOKABLE AvatarProject* createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder); + Q_INVOKABLE AvatarProject* openAvatarProject(const QString& avatarProjectFSTPath); signals: void avatarProjectChanged(); private: Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; - //Q_INVOKABLE QObject* openAvatarProject(); + Q_INVOKABLE QString getAvatarProjectsPath() const { return AvatarProject::getDefaultProjectsPath(); } Q_INVOKABLE QObject* uploadItem(); AvatarProject* _currentAvatarProject{ nullptr }; diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index c7ea7e52ac..4321d2ef40 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -15,44 +15,93 @@ #include #include -#include #include #include +#include "FBXSerializer.h" AvatarProject* AvatarProject::openAvatarProject(const QString& path) { - const auto pathToLower = path.toLower(); - if (pathToLower.endsWith(".fst")) { - QFile file{ path }; - if (!file.open(QIODevice::ReadOnly)) { - return nullptr; - } - auto project = new AvatarProject(path, file.readAll()); - QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership); - return project; + if (!path.toLower().endsWith(".fst")) { + return nullptr; + } + QFile file{ path }; + if (!file.open(QIODevice::ReadOnly)) { + return nullptr; + } + const auto project = new AvatarProject(path, file.readAll()); + QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership); + return project; +} + +AvatarProject* AvatarProject::createAvatarProject(const QString& avatarProjectName, const QString& avatarModelPath) { + if (!isValidNewProjectName(avatarProjectName)) { + return nullptr; + } + QDir dir(getDefaultProjectsPath() + "/" + avatarProjectName); + if (!dir.mkpath(".")) { + return nullptr; + } + const auto fileName = QFileInfo(avatarModelPath).fileName(); + const auto newModelPath = dir.absoluteFilePath(fileName); + const auto newFSTPath = dir.absoluteFilePath("avatar.fst"); + QFile::copy(avatarModelPath, newModelPath); + + QFileInfo fbxInfo(newModelPath); + QFile fbx(fbxInfo.filePath()); + if (!fbxInfo.exists() || !fbxInfo.isFile() || !fbx.open(QIODevice::ReadOnly)) { + // TODO: Can't open model FBX (throw error here) + return nullptr; } - if (pathToLower.endsWith(".fbx")) { - // TODO: Create FST here: + std::shared_ptr hfmModel; + + + try { + qDebug() << "Reading FBX file : " << fbxInfo.filePath(); + const QByteArray fbxContents = fbx.readAll(); + hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath()); + } + catch (const QString& error) { + qDebug() << "Error reading: " << error; + return nullptr; + } + //TODO: copy/fix textures here: + + + + FST* fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel); + + fst->setName(avatarProjectName); + + if (!fst->write()) { + return nullptr; } - return nullptr; + return new AvatarProject(fst); +} + +bool AvatarProject::isValidNewProjectName(const QString& projectName) { + QDir dir(getDefaultProjectsPath() + "/" + projectName); + return !dir.exists(); } AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : - _fstPath(fstPath), _fst(fstPath, FSTReader::readMapping(data)) { + AvatarProject::AvatarProject(new FST(fstPath, FSTReader::readMapping(data))) { - _directory = QFileInfo(_fstPath).absoluteDir(); +} +AvatarProject::AvatarProject(FST* fst) { + _fst = fst; + auto fileInfo = QFileInfo(getFSTPath()); + _directory = fileInfo.absoluteDir(); //_projectFiles = _directory.entryList(); refreshProjectFiles(); - auto fileInfo = QFileInfo(_fstPath); _projectPath = fileInfo.absoluteDir().absolutePath(); } void AvatarProject::appendDirectory(QString prefix, QDir dir) { qDebug() << "Inside of " << prefix << dir.absolutePath(); - auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; + const auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; for (auto& entry : dir.entryInfoList({}, flags)) { if (entry.isFile()) { _projectFiles.append(prefix + "/" + entry.fileName()); diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 6dc64cda6f..9708c9fa83 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -21,6 +21,7 @@ #include #include #include +#include class AvatarProject : public QObject { Q_OBJECT @@ -31,6 +32,7 @@ class AvatarProject : public QObject { Q_PROPERTY(QString projectFolderPath READ getProjectPath) Q_PROPERTY(QString projectFSTPath READ getFSTPath) Q_PROPERTY(QString projectFBXPath READ getFBXPath) + Q_PROPERTY(QString name READ getProjectName) public: Q_INVOKABLE bool write() { @@ -38,38 +40,41 @@ public: return false; } - Q_INVOKABLE QObject* upload() { - // TODO: create new AvatarProjectUploader here, launch it and return it for status tracking in QML - return nullptr; - } - /** * returns the AvatarProject or a nullptr on failure. */ static AvatarProject* openAvatarProject(const QString& path); + static AvatarProject* createAvatarProject(const QString& avatarProjectName, const QString& avatarModelPath); + + static bool isValidNewProjectName(const QString& projectName); + + static QString getDefaultProjectsPath() { + return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/High Fidelity Projects"; + } private: AvatarProject(const QString& fstPath, const QByteArray& data); + AvatarProject(FST* fst); ~AvatarProject() { // TODO: cleanup FST / AvatarProjectUploader etc. } + Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } Q_INVOKABLE QString getProjectPath() const { return _projectPath; } - Q_INVOKABLE QString getFSTPath() const { return _fstPath; } - Q_INVOKABLE QString getFBXPath() const { return _fst.getModelPath(); } + Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); } + Q_INVOKABLE QString getFBXPath() const { return _fst->getModelPath(); } - FST* getFST() { return &_fst; } + FST* getFST() { return _fst; } void refreshProjectFiles(); void appendDirectory(QString prefix, QDir dir); - FST _fst; + FST* _fst; QDir _directory; QStringList _projectFiles{}; QString _projectPath; - QString _fstPath; }; #endif // hifi_AvatarProject_h diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index 6574b66e51..2631fe951e 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -13,21 +13,125 @@ #include #include +#include -FST::FST(QString fstPath, QVariantHash data) : _fstPath(fstPath) { - if (data.contains("name")) { - _name = data["name"].toString(); - data.remove("name"); +FST::FST(const QString& fstPath, QVariantHash data) : _fstPath(fstPath) { + if (data.contains(NAME_FIELD)) { + _name = data[NAME_FIELD].toString(); + data.remove(NAME_FIELD); } - if (data.contains("filename")) { - _modelPath = data["filename"].toString(); - data.remove("filename"); + if (data.contains(FILENAME_FIELD)) { + _modelPath = data[FILENAME_FIELD].toString(); + data.remove(FILENAME_FIELD); } _other = data; } +FST* FST::createFSTFromModel(QString fstPath, 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()); + QDir root(modelFilePath); + mapping.insert(FILENAME_FIELD, root.relativeFilePath(fstPath)); + mapping.insert(TEXDIR_FIELD, "textures"); + mapping.insert(SCRIPT_FIELD, "scripts"); + + // mixamo/autodesk defaults + mapping.insert(SCALE_FIELD, 1.0); + 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); + + 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() }; @@ -42,4 +146,21 @@ void FST::setName(const QString& name) { void FST::setModelPath(const QString& modelPath) { _modelPath = modelPath; emit modelPathChanged(modelPath); -} \ No newline at end of file +} + +QVariantHash FST::getMapping() { + QVariantHash mapping; + mapping.insertMulti(NAME_FIELD, _name); + mapping.insertMulti(FILENAME_FIELD, _modelPath); + mapping.unite(_other); + return mapping; +} + +bool FST::write() { + QFile fst(_fstPath); + if (!fst.open(QIODevice::WriteOnly)) { + return false; + } + fst.write(FSTReader::writeMapping(getMapping())); + return true; +} diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h index e8c67c6c6b..524463b721 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -13,6 +13,11 @@ #include #include +#include "FSTReader.h" + +namespace hfm { + class Model; +}; class FST : public QObject { Q_OBJECT @@ -20,7 +25,9 @@ class FST : public QObject { Q_PROPERTY(QString modelPath READ getModelPath WRITE setModelPath NOTIFY modelPathChanged) Q_PROPERTY(QUuid marketplaceID READ getMarketplaceID) public: - FST(QString fstPath, QVariantHash data); + FST(const QString& fstPath, QVariantHash data); + + static FST* createFSTFromModel(QString fstPath, QString modelFilePath, const hfm::Model& hfmModel); QString absoluteModelPath() const; @@ -32,6 +39,12 @@ public: QUuid getMarketplaceID() const { return _marketplaceID; } + QString getPath() { return _fstPath; } + + QVariantHash getMapping(); + + bool write(); + signals: void nameChanged(const QString& name); void modelPathChanged(const QString& modelPath); From c2aca64b11efe5ffc95c961e6f6421fdaa1437a2 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 19 Dec 2018 11:30:21 -0800 Subject: [PATCH 08/72] Add start of opening uploaded avatar in inventory --- interface/resources/qml/hifi/AvatarPackager.qml | 1 - .../qml/hifi/avatarPackager/AvatarProject.qml | 2 ++ interface/src/avatar/AvatarProject.cpp | 16 ++++++++++++++-- interface/src/avatar/AvatarProject.h | 1 + 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index d5e21d9653..f0835c56bf 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -72,7 +72,6 @@ Windows.ScrollingWindow { height: 30 onClicked: function() { var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects"; - var avatarProjectsPath = "C:/Users/ryanh/Documents/High Fidelity Avatars"; console.log("path = " + avatarProjectsPath); // TODO: make the dialog modal diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 4f42927676..1ba41b1de6 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -156,6 +156,8 @@ Item { colorScheme: root.colorScheme onClicked: function() { console.log("Opening in inventory"); + + AvatarPackagerCore.currentAvatarProject.openInInventory(); } } } diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index cc23027562..d08eafb43c 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -19,6 +19,9 @@ #include #include +#include +#include "scripting/HMDScriptingInterface.h" + AvatarProject* AvatarProject::openAvatarProject(const QString& path) { const auto pathToLower = path.toLower(); if (pathToLower.endsWith(".fst")) { @@ -40,7 +43,6 @@ AvatarProject* AvatarProject::openAvatarProject(const QString& path) { AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : _fstPath(fstPath), _fst(fstPath, FSTReader::readMapping(data)) { - _fstFilename = QFileInfo(_fstPath).fileName(); qDebug() << "Pointers: " << this << &_fst; @@ -70,6 +72,16 @@ void AvatarProject::refreshProjectFiles() { appendDirectory("", _directory); } -Q_INVOKABLE MarketplaceItemUploader* AvatarProject::upload() { +MarketplaceItemUploader* AvatarProject::upload() { return new MarketplaceItemUploader("test_avatar", "blank description", _fstFilename, QUuid(), _projectFiles); } + +void AvatarProject::openInInventory() { + auto tablet = dynamic_cast( + DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); + tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml"); + DependencyManager::get()->openTablet(); + tablet->sendToQml(QVariantMap({ + { "method", "updatePurchases" }, + { "filterText", "filtertext" } })); +} diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 1a0ed5cc5e..de7751008f 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -40,6 +40,7 @@ public: } Q_INVOKABLE MarketplaceItemUploader* upload(); + Q_INVOKABLE void openInInventory(); /** * returns the AvatarProject or a nullptr on failure. From 05e0fb464ef700214d521f11638d0d29842a6a27 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 14 Dec 2018 15:52:00 -0800 Subject: [PATCH 09/72] new improved polylines --- .../src/RenderablePolyLineEntityItem.cpp | 345 ++++++++---------- .../src/RenderablePolyLineEntityItem.h | 54 ++- .../entities-renderer/src/paintStroke.slf | 32 +- .../entities-renderer/src/paintStroke.slh | 48 +++ .../entities-renderer/src/paintStroke.slv | 46 ++- .../src/paintStroke_Shared.slh | 25 ++ .../src/paintStroke_fade.slf | 52 --- .../src/paintStroke_fade.slv | 43 --- .../entities/src/EntityItemProperties.cpp | 24 ++ libraries/entities/src/EntityItemProperties.h | 2 + libraries/entities/src/EntityPropertyFlags.h | 2 + libraries/entities/src/PolyLineEntityItem.cpp | 173 +++------ libraries/entities/src/PolyLineEntityItem.h | 56 +-- .../networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 3 +- 15 files changed, 414 insertions(+), 493 deletions(-) create mode 100644 libraries/entities-renderer/src/paintStroke.slh create mode 100644 libraries/entities-renderer/src/paintStroke_Shared.slh delete mode 100644 libraries/entities-renderer/src/paintStroke_fade.slf delete mode 100644 libraries/entities-renderer/src/paintStroke_fade.slv diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index 0d9e948db8..de224103ce 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -19,66 +19,39 @@ #include #include -//#define POLYLINE_ENTITY_USE_FADE_EFFECT -#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT -# include -#endif +#include "paintStroke_Shared.slh" using namespace render; using namespace render::entities; -static uint8_t CUSTOM_PIPELINE_NUMBER { 0 }; -static const int32_t PAINTSTROKE_TEXTURE_SLOT { 0 }; -static gpu::Stream::FormatPointer polylineFormat; -static gpu::PipelinePointer polylinePipeline; -#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT -static gpu::PipelinePointer polylineFadePipeline; -#endif +gpu::PipelinePointer PolyLineEntityRenderer::_pipeline = nullptr; -static render::ShapePipelinePointer shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key, gpu::Batch& batch) { - if (!polylinePipeline) { - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke); -#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT - auto fadeVS = gpu::Shader::createVertex(std::string(paintStroke_fade_vert)); - auto fadePS = gpu::Shader::createPixel(std::string(paintStroke_fade_frag)); - gpu::ShaderPointer fadeProgram = gpu::Shader::createProgram(fadeVS, fadePS); -#endif - gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - state->setDepthTest(true, true, gpu::LESS_EQUAL); - PrepareStencil::testMask(*state); - state->setBlendFunction(true, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - polylinePipeline = gpu::Pipeline::create(program, state); -#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT - _fadePipeline = gpu::Pipeline::create(fadeProgram, state); -#endif - } - -#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT - if (key.isFaded()) { - auto fadeEffect = DependencyManager::get(); - return std::make_shared(_fadePipeline, nullptr, fadeEffect->getBatchSetter(), fadeEffect->getItemUniformSetter()); - } else { -#endif - return std::make_shared(polylinePipeline, nullptr, nullptr, nullptr); -#ifdef POLYLINE_ENTITY_USE_FADE_EFFECT - } -#endif -} +static const QUrl DEFAULT_POLYLINE_TEXTURE = QUrl(PathUtils::resourcesPath() + "images/paintStroke.png"); PolyLineEntityRenderer::PolyLineEntityRenderer(const EntityItemPointer& entity) : Parent(entity) { - static std::once_flag once; - std::call_once(once, [&] { - CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(shapePipelineFactory); - polylineFormat.reset(new gpu::Stream::Format()); - polylineFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), offsetof(Vertex, position)); - polylineFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), offsetof(Vertex, normal)); - polylineFormat->setAttribute(gpu::Stream::TEXCOORD, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV), offsetof(Vertex, uv)); - polylineFormat->setAttribute(gpu::Stream::COLOR, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB), offsetof(Vertex, color)); - }); + _texture = DependencyManager::get()->getTexture(DEFAULT_POLYLINE_TEXTURE); - _verticesBuffer = std::make_shared(); + { // Initialize our buffers + _polylineDataBuffer = std::make_shared(); + _polylineDataBuffer->resize(sizeof(PolylineData)); + PolylineData data { glm::vec2(_faceCamera, _glow), glm::vec2(0.0f) }; + _polylineDataBuffer->setSubData(0, data); + + _polylineGeometryBuffer = std::make_shared(); + } +} + +void PolyLineEntityRenderer::buildPipeline() { + // FIXME: opaque pipeline + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke); + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + state->setCullMode(gpu::State::CullMode::CULL_NONE); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + PrepareStencil::testMask(*state); + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + _pipeline = gpu::Pipeline::create(program, state); } ItemKey PolyLineEntityRenderer::getKey() { @@ -86,152 +59,164 @@ ItemKey PolyLineEntityRenderer::getKey() { } ShapeKey PolyLineEntityRenderer::getShapeKey() { - return ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER).build(); + return ShapeKey::Builder().withOwnPipeline().withTranslucent().withoutCullFace(); +} + +bool PolyLineEntityRenderer::needsRenderUpdate() const { + bool textureLoadedChanged = resultWithReadLock([&] { + return (!_textureLoaded && _texture && _texture->isLoaded()); + }); + + if (textureLoadedChanged) { + return true; + } + + return Parent::needsRenderUpdate(); } bool PolyLineEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { return ( entity->pointsChanged() || - entity->strokeWidthsChanged() || + entity->widthsChanged() || entity->normalsChanged() || entity->texturesChanged() || - entity->strokeColorsChanged() + entity->colorsChanged() || + _isUVModeStretch != entity->getIsUVModeStretch() || + _glow != entity->getGlow() || + _faceCamera != entity->getFaceCamera() ); } -void PolyLineEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { - static const QUrl DEFAULT_POLYLINE_TEXTURE = QUrl(PathUtils::resourcesPath() + "images/paintStroke.png"); - QUrl entityTextures = DEFAULT_POLYLINE_TEXTURE; +void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { + auto pointsChanged = entity->pointsChanged(); + auto widthsChanged = entity->widthsChanged(); + auto normalsChanged = entity->normalsChanged(); + auto colorsChanged = entity->colorsChanged(); + + bool isUVModeStretch = entity->getIsUVModeStretch(); + bool glow = entity->getGlow(); + bool faceCamera = entity->getFaceCamera(); + + entity->resetPolyLineChanged(); + + // Transform + updateModelTransformAndBound(); + _renderTransform = getModelTransform(); + + // Textures if (entity->texturesChanged()) { entity->resetTexturesChanged(); + QUrl entityTextures = DEFAULT_POLYLINE_TEXTURE; auto textures = entity->getTextures(); if (!textures.isEmpty()) { entityTextures = QUrl(textures); } _texture = DependencyManager::get()->getTexture(entityTextures); + _textureAspectRatio = 1.0f; + _textureLoaded = false; } - - - if (!_texture) { - _texture = DependencyManager::get()->getTexture(entityTextures); + + bool textureChanged = false; + if (!_textureLoaded && _texture && _texture->isLoaded()) { + textureChanged = true; + _textureAspectRatio = (float)_texture->getOriginalHeight() / (float)_texture->getOriginalWidth(); + _textureLoaded = true; } -} -void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { - auto pointsChanged = entity->pointsChanged(); - auto strokeWidthsChanged = entity->strokeWidthsChanged(); - auto normalsChanged = entity->normalsChanged(); - auto strokeColorsChanged = entity->strokeColorsChanged(); - - - bool isUVModeStretch = entity->getIsUVModeStretch(); - entity->resetPolyLineChanged(); - - _polylineTransform = Transform(); - _polylineTransform.setTranslation(entity->getWorldPosition()); - _polylineTransform.setRotation(entity->getWorldOrientation()); + // Data + if (faceCamera != _faceCamera || glow != _glow) { + _faceCamera = faceCamera; + _glow = glow; + updateData(); + } + // Geometry if (pointsChanged) { - _lastPoints = entity->getLinePoints(); + _points = entity->getLinePoints(); } - if (strokeWidthsChanged) { - _lastStrokeWidths = entity->getStrokeWidths(); + if (widthsChanged) { + _widths = entity->getStrokeWidths(); } if (normalsChanged) { - _lastNormals = entity->getNormals(); + _normals = entity->getNormals(); } - if (strokeColorsChanged) { - _lastStrokeColors = entity->getStrokeColors(); - _lastStrokeColors = _lastNormals.size() == _lastStrokeColors.size() ? _lastStrokeColors : QVector({ toGlm(entity->getColor()) }); + if (colorsChanged) { + _colors = entity->getStrokeColors(); + _color = toGlm(entity->getColor()); } - if (pointsChanged || strokeWidthsChanged || normalsChanged || strokeColorsChanged) { - _empty = std::min(_lastPoints.size(), std::min(_lastNormals.size(), _lastStrokeWidths.size())) < 2; - if (!_empty) { - updateGeometry(updateVertices(_lastPoints, _lastNormals, _lastStrokeWidths, _lastStrokeColors, isUVModeStretch, _textureAspectRatio)); - } + if (_isUVModeStretch != isUVModeStretch || pointsChanged || widthsChanged || normalsChanged || colorsChanged || textureChanged) { + _isUVModeStretch = isUVModeStretch; + updateGeometry(); } } -void PolyLineEntityRenderer::updateGeometry(const std::vector& vertices) { - _numVertices = (uint32_t)vertices.size(); - auto bufferSize = _numVertices * sizeof(Vertex); - if (bufferSize > _verticesBuffer->getSize()) { - _verticesBuffer->resize(bufferSize); - } - _verticesBuffer->setSubData(0, vertices); -} +void PolyLineEntityRenderer::updateGeometry() { + int maxNumVertices = std::min(_points.length(), _normals.length()); -std::vector PolyLineEntityRenderer::updateVertices(const QVector& points, - const QVector& normals, - const QVector& strokeWidths, - const QVector& strokeColors, - const bool isUVModeStretch, - const float textureAspectRatio) { - // Calculate the minimum vector size out of normals, points, and stroke widths - int size = std::min(points.size(), std::min(normals.size(), strokeWidths.size())); - - std::vector vertices; - - // Guard against an empty polyline - if (size <= 0) { - return vertices; - } - - float uCoordInc = 1.0f / size; - float uCoord = 0.0f; - int finalIndex = size - 1; - glm::vec3 binormal; - float accumulatedDistance = 0.0f; - float distanceToLastPoint = 0.0f; - float accumulatedStrokeWidth = 0.0f; - float strokeWidth = 0.0f; bool doesStrokeWidthVary = false; - - - for (int i = 1; i < strokeWidths.size(); i++) { - if (strokeWidths[i] != strokeWidths[i - 1]) { - doesStrokeWidthVary = true; - break; + if (_widths.size() >= 0) { + for (int i = 1; i < maxNumVertices; i++) { + float width = PolyLineEntityItem::DEFAULT_LINE_WIDTH; + if (i < _widths.length()) { + width = _widths[i]; + } + if (width != _widths[i - 1]) { + doesStrokeWidthVary = true; + break; + } } } - for (int i = 0; i <= finalIndex; i++) { - const float& width = strokeWidths.at(i); - const auto& point = points.at(i); - const auto& normal = normals.at(i); - const auto& color = strokeColors.size() == normals.size() ? strokeColors.at(i) : strokeColors.at(0); - int vertexIndex = i * 2; - + float uCoordInc = 1.0f / maxNumVertices; + float uCoord = 0.0f; + float accumulatedDistance = 0.0f; + float accumulatedStrokeWidth = 0.0f; + glm::vec3 binormal; - if (!isUVModeStretch && i >= 1) { - distanceToLastPoint = glm::distance(points.at(i), points.at(i - 1)); - accumulatedDistance += distanceToLastPoint; - strokeWidth = 2 * strokeWidths[i]; + std::vector vertices; + vertices.reserve(maxNumVertices); + for (int i = 0; i < maxNumVertices; i++) { + // Position + glm::vec3 point = _points[i]; - if (doesStrokeWidthVary) { - //If the stroke varies along the line the texture will stretch more or less depending on the speed - //because it looks better than using the same method as below - accumulatedStrokeWidth += strokeWidth; - float increaseValue = 1; - if (accumulatedStrokeWidth != 0) { - float newUcoord = glm::ceil(((1.0f / textureAspectRatio) * accumulatedDistance) / (accumulatedStrokeWidth / i)); - increaseValue = newUcoord - uCoord; + // uCoord + float width = i < _widths.size() ? _widths[i] : PolyLineEntityItem::DEFAULT_LINE_WIDTH; + if (i > 0) { // First uCoord is 0.0f + if (!_isUVModeStretch) { + accumulatedDistance += glm::distance(point, _points[i - 1]); + + if (doesStrokeWidthVary) { + //If the stroke varies along the line the texture will stretch more or less depending on the speed + //because it looks better than using the same method as below + accumulatedStrokeWidth += width; + float increaseValue = 1; + if (accumulatedStrokeWidth != 0) { + float newUcoord = glm::ceil((_textureAspectRatio * accumulatedDistance) / (accumulatedStrokeWidth / i)); + increaseValue = newUcoord - uCoord; + } + + increaseValue = increaseValue > 0 ? increaseValue : 1; + uCoord += increaseValue; + } else { + // If the stroke width is constant then the textures should keep the aspect ratio along the line + uCoord = (_textureAspectRatio * accumulatedDistance) / width; } - - increaseValue = increaseValue > 0 ? increaseValue : 1; - uCoord += increaseValue; } else { - //If the stroke width is constant then the textures should keep the aspect ratio along the line - uCoord = ((1.0f / textureAspectRatio) * accumulatedDistance) / strokeWidth; + uCoord += uCoordInc; } - } else if (vertexIndex >= 2) { - uCoord += uCoordInc; } + // Color + glm::vec3 color = i < _colors.length() ? _colors[i] : _color; + + // Normal + glm::vec3 normal = _normals[i]; + + // Binormal // For last point we can assume binormals are the same since it represents the last two vertices of quad - if (i < finalIndex) { - const auto tangent = points.at(i + 1) - point; - binormal = glm::normalize(glm::cross(tangent, normal)) * width; + if (i < maxNumVertices - 1) { + glm::vec3 tangent = _points[i + 1] - point; + binormal = glm::normalize(glm::cross(tangent, normal)); // Check to make sure binormal is not a NAN. If it is, don't add to vertices vector if (binormal.x != binormal.x) { @@ -239,54 +224,36 @@ std::vector PolyLineEntityRenderer::updateVertic } } - const auto v1 = points.at(i) + binormal; - const auto v2 = points.at(i) - binormal; - vertices.emplace_back(v1, normal, vec2(uCoord, 0.0f), color); - vertices.emplace_back(v2, normal, vec2(uCoord, 1.0f), color); + PolylineVertex vertex = { glm::vec4(point, uCoord), glm::vec4(color, 1.0f), glm::vec4(normal, 0.0f), glm::vec4(binormal, 0.5f * width) }; + vertices.push_back(vertex); } - return vertices; + _numVertices = vertices.size(); + _polylineGeometryBuffer->setData(vertices.size() * sizeof(PolylineVertex), (const gpu::Byte*) vertices.data()); } -scriptable::ScriptableModelBase PolyLineEntityRenderer::getScriptableModel() { - // TODO: adapt polyline into a triangles mesh... - return EntityRenderer::getScriptableModel(); +void PolyLineEntityRenderer::updateData() { + PolylineData data { glm::vec2(_faceCamera, _glow), glm::vec2(0.0f) }; + _polylineDataBuffer->setSubData(0, data); } void PolyLineEntityRenderer::doRender(RenderArgs* args) { - if (_empty) { + if (_numVertices < 2) { return; } PerformanceTimer perfTimer("RenderablePolyLineEntityItem::render"); Q_ASSERT(args->_batch); - gpu::Batch& batch = *args->_batch; - batch.setModelTransform(_polylineTransform); - if (_texture && _texture->isLoaded()) { - batch.setResourceTexture(PAINTSTROKE_TEXTURE_SLOT, _texture->getGPUTexture()); - } else { - batch.setResourceTexture(PAINTSTROKE_TEXTURE_SLOT, DependencyManager::get()->getWhiteTexture()); + if (!_pipeline) { + buildPipeline(); } - float textureWidth = (float)_texture->getOriginalWidth(); - float textureHeight = (float)_texture->getOriginalHeight(); - if (textureWidth != 0 && textureHeight != 0) { - _textureAspectRatio = textureWidth / textureHeight; - } - - batch.setInputFormat(polylineFormat); - batch.setInputBuffer(0, _verticesBuffer, 0, sizeof(Vertex)); - -#ifndef POLYLINE_ENTITY_USE_FADE_EFFECT - // glColor4f must be called after setInputFormat if it must be taken into account - if (_isFading) { - batch._glColor4f(1.0f, 1.0f, 1.0f, Interpolate::calculateFadeRatio(_fadeStartTime)); - } else { - batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f); - } -#endif - - batch.draw(gpu::TRIANGLE_STRIP, _numVertices, 0); + batch.setPipeline(_pipeline); + batch.setModelTransform(_renderTransform); + batch.setResourceTexture(0, _textureLoaded ? _texture->getGPUTexture() : DependencyManager::get()->getWhiteTexture()); + batch.setResourceBuffer(0, _polylineGeometryBuffer); + batch.setUniformBuffer(0, _polylineDataBuffer); + batch.draw(gpu::TRIANGLE_STRIP, (gpu::uint32)(2 * _numVertices), 0); } diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h index 8130171da8..fd37a49598 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h @@ -25,52 +25,40 @@ class PolyLineEntityRenderer : public TypedEntityRenderer { public: PolyLineEntityRenderer(const EntityItemPointer& entity); - virtual scriptable::ScriptableModelBase getScriptableModel() override; + // FIXME: shouldn't always be transparent: take into account texture and glow + virtual bool isTransparent() const override { return true; } + protected: + virtual bool needsRenderUpdate() const override; virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; - virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, - Transaction& transaction, - const TypedEntityPointer& entity) override; virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override; virtual ItemKey getKey() override; virtual ShapeKey getShapeKey() override; virtual void doRender(RenderArgs* args) override; - virtual bool isTransparent() const override { return true; } + void buildPipeline(); + void updateGeometry(); + void updateData(); - struct Vertex { - Vertex() {} - Vertex(const vec3& position, const vec3& normal, const vec2& uv, const vec3& color) : position(position), - normal(normal), - uv(uv), - color(color) {} - vec3 position; - vec3 normal; - vec2 uv; - vec3 color; - }; + QVector _points; + QVector _normals; + QVector _colors; + glm::vec3 _color; + QVector _widths; - void updateGeometry(const std::vector& vertices); - static std::vector updateVertices(const QVector& points, - const QVector& normals, - const QVector& strokeWidths, - const QVector& strokeColors, - const bool isUVModeStretch, - const float textureAspectRatio); - - Transform _polylineTransform; - QVector _lastPoints; - QVector _lastNormals; - QVector _lastStrokeColors; - QVector _lastStrokeWidths; - gpu::BufferPointer _verticesBuffer; - - uint32_t _numVertices { 0 }; - bool _empty{ true }; NetworkTexturePointer _texture; float _textureAspectRatio { 1.0f }; + bool _textureLoaded { false }; + bool _isUVModeStretch; + bool _faceCamera; + bool _glow; + + size_t _numVertices; + gpu::BufferPointer _polylineDataBuffer; + gpu::BufferPointer _polylineGeometryBuffer; + static gpu::PipelinePointer _pipeline; }; } } // namespace diff --git a/libraries/entities-renderer/src/paintStroke.slf b/libraries/entities-renderer/src/paintStroke.slf index f2c0d5572d..6ea088751f 100644 --- a/libraries/entities-renderer/src/paintStroke.slf +++ b/libraries/entities-renderer/src/paintStroke.slf @@ -14,21 +14,27 @@ <@include DeferredBufferWrite.slh@> -// the albedo texture -LAYOUT(binding=0) uniform sampler2D originalTexture; +<@include paintStroke.slh@> +<$declarePolyLineBuffers()$> -// the interpolated normal -layout(location=0) in vec3 interpolatedNormal; -layout(location=1) in vec2 varTexcoord; -layout(location=2) in vec4 varColor; +LAYOUT(binding=0) uniform sampler2D _texture; + +layout(location=0) in vec3 _normalWS; +layout(location=1) in vec2 _texCoord; +layout(location=2) in vec4 _color; +layout(location=3) in float _distanceFromCenter; void main(void) { - vec4 texel = texture(originalTexture, varTexcoord); - int frontCondition = 1 -int(gl_FrontFacing) * 2; - vec3 color = varColor.rgb; + vec4 texel = texture(_texture, _texCoord); + int frontCondition = 1 - 2 * int(gl_FrontFacing); + vec3 color = _color.rgb * texel.rgb; + float alpha = texel.a * _color.a; + + alpha *= mix(1.0, pow(1.0 - abs(_distanceFromCenter), 10.0), _polylineData.faceCameraGlow.y); + packDeferredFragmentTranslucent( - float(frontCondition) * interpolatedNormal, - texel.a * varColor.a, - color * texel.rgb, - 10.0); + float(frontCondition) * _normalWS, + alpha, + color, + DEFAULT_ROUGHNESS); } diff --git a/libraries/entities-renderer/src/paintStroke.slh b/libraries/entities-renderer/src/paintStroke.slh new file mode 100644 index 0000000000..6189ac461b --- /dev/null +++ b/libraries/entities-renderer/src/paintStroke.slh @@ -0,0 +1,48 @@ + + +<@if not PAINTSTROKE_SLH@> +<@def PAINTSTROKE_SLH@> + +<@include paintStroke_Shared.slh@> +<@include gpu/ShaderConstants.h@> + +<@func declarePolyLineBuffers() @> + +// Hack comment to absorb the extra '//' scribe prepends + +#if !defined(GPU_SSBO_TRANSFORM_OBJECT) +LAYOUT(binding=GPU_RESOURCE_BUFFER_SLOT0_TEXTURE) uniform samplerBuffer polylineVerticesBuffer; +PolylineVertex getPolylineVertex(int i) { + int offset = 4 * i; + PolylineVertex vertex; + vertex.positionAndUCoord = texelFetch(polylineVerticesBuffer, offset); + vertex.color = texelFetch(polylineVerticesBuffer, offset + 1); + vertex.normal = texelFetch(polylineVerticesBuffer, offset + 2); + vertex.binormalAndHalfWidth = texelFetch(polylineVerticesBuffer, offset + 3); + return vertex; +} +#else +LAYOUT_STD140(binding=GPU_RESOURCE_BUFFER_SLOT0_STORAGE) buffer polylineVerticesBuffer { + PolylineVertex _vertices[]; +}; +PolylineVertex getPolylineVertex(int i) { + PolylineVertex vertex = _vertices[i]; + return vertex; +} +#endif + +LAYOUT_STD140(binding=0) uniform polylineDataBuffer { + PolylineData _polylineData; +}; + +<@endfunc@> + +<@endif@> diff --git a/libraries/entities-renderer/src/paintStroke.slv b/libraries/entities-renderer/src/paintStroke.slv index ecf52d61cf..c033d2c247 100644 --- a/libraries/entities-renderer/src/paintStroke.slv +++ b/libraries/entities-renderer/src/paintStroke.slv @@ -17,23 +17,45 @@ <@include gpu/Transform.slh@> <$declareStandardTransform()$> -// the interpolated normal -layout(location=0) out vec3 interpolatedNormal; +<@include paintStroke.slh@> +<$declarePolyLineBuffers()$> -//the diffuse texture -layout(location=1) out vec2 varTexcoord; - -layout(location=2) out vec4 varColor; +layout(location=0) out vec3 _normalWS; +layout(location=1) out vec2 _texCoord; +layout(location=2) out vec4 _color; +layout(location=3) out float _distanceFromCenter; void main(void) { - varTexcoord = inTexCoord0.st; + PolylineVertex vertex = getPolylineVertex(gl_VertexID / 2); + float evenVertex = float(gl_VertexID % 2 == 0); - // pass along the diffuse color - varColor = color_sRGBAToLinear(inColor); + _texCoord = vec2(vertex.positionAndUCoord.w, mix(1.0, 0.0, evenVertex)); + _color = color_sRGBAToLinear(vertex.color); - // standard transform TransformCamera cam = getTransformCamera(); TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, interpolatedNormal)$> + _distanceFromCenter = -1.0 + 2.0 * evenVertex; + vec4 position = vec4(vertex.positionAndUCoord.xyz, 1.0); + vec3 normal = vertex.normal.xyz; + vec3 binormal = vertex.binormalAndHalfWidth.xyz; + if (_polylineData.faceCameraGlow.x != 0.0) { + vec4 posEye; + vec3 normalEye; + vec3 binormalEye; + <$transformModelToEyePos(cam, obj, position, posEye)$> + <$transformModelToEyeDir(cam, obj, normal, normalEye)$> + <$transformModelToEyeDir(cam, obj, binormal, binormalEye)$> + + vec3 tangentEye = cross(binormalEye, normalEye); + // new normal faces the camera + normalEye = normalize(posEye.xyz); + binormalEye = normalize(cross(normalEye, tangentEye)); + posEye.xyz += _distanceFromCenter * vertex.binormalAndHalfWidth.w * binormalEye; + <$transformEyeToClipPos(cam, posEye, gl_Position)$> + <$transformEyeToWorldDir(cam, normalEye, _normalWS)$> + } else { + position.xyz += _distanceFromCenter * vertex.binormalAndHalfWidth.w * binormal; + <$transformModelToClipPos(cam, obj, position, gl_Position)$> + <$transformModelToWorldDir(cam, obj, normal, _normalWS)$> + } } \ No newline at end of file diff --git a/libraries/entities-renderer/src/paintStroke_Shared.slh b/libraries/entities-renderer/src/paintStroke_Shared.slh new file mode 100644 index 0000000000..52c10df99b --- /dev/null +++ b/libraries/entities-renderer/src/paintStroke_Shared.slh @@ -0,0 +1,25 @@ +// glsl / C++ compatible source as interface for FadeEffect +#ifdef __cplusplus +# define _PL_VEC4 glm::vec4 +# define _PL_VEC2 glm::vec2 +#else +# define _PL_VEC4 vec4 +# define _PL_VEC2 vec2 +#endif + +struct PolylineVertex { + _PL_VEC4 positionAndUCoord; + _PL_VEC4 color; + _PL_VEC4 normal; + _PL_VEC4 binormalAndHalfWidth; +}; + +struct PolylineData { + _PL_VEC2 faceCameraGlow; + _PL_VEC2 spare; +}; + +// <@if 1@> +// Trigger Scribe include +// <@endif@> +// \ No newline at end of file diff --git a/libraries/entities-renderer/src/paintStroke_fade.slf b/libraries/entities-renderer/src/paintStroke_fade.slf deleted file mode 100644 index fa6d0aab75..0000000000 --- a/libraries/entities-renderer/src/paintStroke_fade.slf +++ /dev/null @@ -1,52 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// paintStroke_fade.frag -// fragment shader -// -// Created by Olivier Prat on 19/07/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -<@include DeferredBufferWrite.slh@> - -<@include Fade.slh@> -<$declareFadeFragment()$> - -// the albedo texture -LAYOUT(binding=0) uniform sampler2D originalTexture; - -// the interpolated normal -layout(location=0) in vec3 interpolatedNormal; -layout(location=1) in vec2 varTexcoord; -layout(location=2) in vec4 varColor; -layout(location=3) in vec4 _worldPosition; - -struct PolyLineUniforms { - vec3 color; -}; - -LAYOUT(binding=0) uniform polyLineBuffer { - PolyLineUniforms polyline; -}; - -void main(void) { - vec3 fadeEmissive; - FadeObjectParams fadeParams; - - <$fetchFadeObjectParams(fadeParams)$> - applyFade(fadeParams, _worldPosition.xyz, fadeEmissive); - - vec4 texel = texture(originalTexture, varTexcoord); - int frontCondition = 1 -int(gl_FrontFacing) * 2; - vec3 color = varColor.rgb; - packDeferredFragmentTranslucent( - interpolatedNormal * float(frontCondition), - texel.a * varColor.a, - polyline.color * texel.rgb + fadeEmissive, - 10.0); -} diff --git a/libraries/entities-renderer/src/paintStroke_fade.slv b/libraries/entities-renderer/src/paintStroke_fade.slv deleted file mode 100644 index f6fcb18c98..0000000000 --- a/libraries/entities-renderer/src/paintStroke_fade.slv +++ /dev/null @@ -1,43 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// paintStroke_fade.vert -// vertex shader -// -// Created by Olivier Prat on 19/07/17. -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -<@include gpu/Inputs.slh@> -<@include gpu/Color.slh@> -<@include gpu/Transform.slh@> -<$declareStandardTransform()$> - -// the interpolated normal -layout(location=0) out vec3 interpolatedNormal; - -//the diffuse texture -layout(location=1) out vec2 varTexcoord; - -layout(location=2) out vec4 varColor; -layout(location=3) out vec4 _worldPosition; - -void main(void) { - - varTexcoord = inTexCoord0.st; - - // pass along the diffuse color - varColor = color_sRGBAToLinear(inColor); - - - // standard transform - TransformCamera cam = getTransformCamera(); - TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, interpolatedNormal)$> - <$transformModelToWorldPos(obj, inPosition, _worldPosition)$> -} \ No newline at end of file diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 86ad35b8f5..576f621a53 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -527,6 +527,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_STROKE_NORMALS, normals); CHECK_PROPERTY_CHANGE(PROP_STROKE_COLORS, strokeColors); CHECK_PROPERTY_CHANGE(PROP_IS_UV_MODE_STRETCH, isUVModeStretch); + CHECK_PROPERTY_CHANGE(PROP_LINE_GLOW, glow); + CHECK_PROPERTY_CHANGE(PROP_LINE_FACE_CAMERA, faceCamera); // Shape CHECK_PROPERTY_CHANGE(PROP_SHAPE, shape); @@ -1051,6 +1053,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * format. * @property {boolean} isUVModeStretch=true - If true, the texture is stretched to fill the whole line, otherwise * the texture repeats along the line. + * @property {bool} glow=false - If true, the alpha of the strokes will drop off farther from the center. + * @property {bool} faceCamera=false - If true, each line segment will rotate to face the camera. * @example Draw a textured "V". * var entity = Entities.addEntity({ * type: "PolyLine", @@ -1634,6 +1638,8 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_STROKE_NORMALS, normals); COPY_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_STROKE_COLORS, strokeColors, qVectorVec3Color); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IS_UV_MODE_STRETCH, isUVModeStretch); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_GLOW, glow); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_FACE_CAMERA, faceCamera); } // Materials @@ -1956,6 +1962,8 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(normals, qVectorVec3, setNormals); COPY_PROPERTY_FROM_QSCRIPTVALUE(strokeColors, qVectorVec3, setStrokeColors); COPY_PROPERTY_FROM_QSCRIPTVALUE(isUVModeStretch, bool, setIsUVModeStretch); + COPY_PROPERTY_FROM_QSCRIPTVALUE(glow, bool, setGlow); + COPY_PROPERTY_FROM_QSCRIPTVALUE(faceCamera, bool, setFaceCamera); // Shape COPY_PROPERTY_FROM_QSCRIPTVALUE(shape, QString, setShape); @@ -2208,6 +2216,8 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(normals); COPY_PROPERTY_IF_CHANGED(strokeColors); COPY_PROPERTY_IF_CHANGED(isUVModeStretch); + COPY_PROPERTY_IF_CHANGED(glow); + COPY_PROPERTY_IF_CHANGED(faceCamera); // Shape COPY_PROPERTY_IF_CHANGED(shape); @@ -2523,6 +2533,8 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_STROKE_NORMALS, Normals, normals, QVector); ADD_PROPERTY_TO_MAP(PROP_STROKE_COLORS, StrokeColors, strokeColors, QVector); ADD_PROPERTY_TO_MAP(PROP_IS_UV_MODE_STRETCH, IsUVModeStretch, isUVModeStretch, QVector); + ADD_PROPERTY_TO_MAP(PROP_LINE_GLOW, Glow, glow, bool); + ADD_PROPERTY_TO_MAP(PROP_LINE_FACE_CAMERA, FaceCamera, faceCamera, bool); // Shape ADD_PROPERTY_TO_MAP(PROP_SHAPE, Shape, shape, QString); @@ -2887,6 +2899,8 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy APPEND_ENTITY_PROPERTY(PROP_STROKE_NORMALS, properties.getPackedNormals()); APPEND_ENTITY_PROPERTY(PROP_STROKE_COLORS, properties.getPackedStrokeColors()); APPEND_ENTITY_PROPERTY(PROP_IS_UV_MODE_STRETCH, properties.getIsUVModeStretch()); + APPEND_ENTITY_PROPERTY(PROP_LINE_GLOW, properties.getGlow()); + APPEND_ENTITY_PROPERTY(PROP_LINE_FACE_CAMERA, properties.getFaceCamera()); } // NOTE: Spheres and Boxes are just special cases of Shape, and they need to include their PROP_SHAPE @@ -3320,6 +3334,8 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_STROKE_NORMALS, QByteArray, setPackedNormals); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_STROKE_COLORS, QByteArray, setPackedStrokeColors); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_IS_UV_MODE_STRETCH, bool, setIsUVModeStretch); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LINE_GLOW, bool, setGlow); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LINE_FACE_CAMERA, bool, setFaceCamera); } // NOTE: Spheres and Boxes are just special cases of Shape, and they need to include their PROP_SHAPE @@ -3665,6 +3681,8 @@ void EntityItemProperties::markAllChanged() { _normalsChanged = true; _strokeColorsChanged = true; _isUVModeStretchChanged = true; + _glowChanged = true; + _faceCameraChanged = true; // Shape _shapeChanged = true; @@ -4263,6 +4281,12 @@ QList EntityItemProperties::listChangedProperties() { if (isUVModeStretchChanged()) { out += "isUVModeStretch"; } + if (glowChanged()) { + out += "glow"; + } + if (faceCameraChanged()) { + out += "faceCamera"; + } // Shape if (shapeChanged()) { diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index fc4848bd20..8f74b7072b 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -309,6 +309,8 @@ public: DEFINE_PROPERTY(PROP_STROKE_NORMALS, Normals, normals, QVector, ENTITY_ITEM_DEFAULT_EMPTY_VEC3_QVEC); DEFINE_PROPERTY(PROP_STROKE_COLORS, StrokeColors, strokeColors, QVector, ENTITY_ITEM_DEFAULT_EMPTY_VEC3_QVEC); DEFINE_PROPERTY(PROP_IS_UV_MODE_STRETCH, IsUVModeStretch, isUVModeStretch, bool, true); + DEFINE_PROPERTY(PROP_LINE_GLOW, Glow, glow, bool, false); + DEFINE_PROPERTY(PROP_LINE_FACE_CAMERA, FaceCamera, faceCamera, bool, false); // Shape DEFINE_PROPERTY_REF(PROP_SHAPE, Shape, shape, QString, "Sphere"); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index e7cccc31df..812b788b06 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -290,6 +290,8 @@ enum EntityPropertyList { PROP_STROKE_NORMALS = PROP_DERIVED_2, PROP_STROKE_COLORS = PROP_DERIVED_3, PROP_IS_UV_MODE_STRETCH = PROP_DERIVED_4, + PROP_LINE_GLOW = PROP_DERIVED_5, + PROP_LINE_FACE_CAMERA = PROP_DERIVED_6, // Shape PROP_SHAPE = PROP_DERIVED_0, diff --git a/libraries/entities/src/PolyLineEntityItem.cpp b/libraries/entities/src/PolyLineEntityItem.cpp index 88f2d14ae6..6ab885b32b 100644 --- a/libraries/entities/src/PolyLineEntityItem.cpp +++ b/libraries/entities/src/PolyLineEntityItem.cpp @@ -24,7 +24,6 @@ const float PolyLineEntityItem::DEFAULT_LINE_WIDTH = 0.1f; const int PolyLineEntityItem::MAX_POINTS_PER_LINE = 60; - EntityItemPointer PolyLineEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer entity(new PolyLineEntityItem(entityID), [](EntityItem* ptr) { ptr->deleteLater(); }); entity->setProperties(properties); @@ -37,7 +36,6 @@ PolyLineEntityItem::PolyLineEntityItem(const EntityItemID& entityItemID) : Entit } EntityItemProperties PolyLineEntityItem::getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const { - QWriteLocker lock(&_quadReadWriteLock); EntityItemProperties properties = EntityItem::getProperties(desiredProperties, allowEmptyDesiredProperties); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getColor); @@ -48,11 +46,13 @@ EntityItemProperties PolyLineEntityItem::getProperties(const EntityPropertyFlags COPY_ENTITY_PROPERTY_TO_PROPERTIES(normals, getNormals); COPY_ENTITY_PROPERTY_TO_PROPERTIES(strokeColors, getStrokeColors); COPY_ENTITY_PROPERTY_TO_PROPERTIES(isUVModeStretch, getIsUVModeStretch); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(glow, getGlow); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(faceCamera, getFaceCamera); + return properties; } bool PolyLineEntityItem::setProperties(const EntityItemProperties& properties) { - QWriteLocker lock(&_quadReadWriteLock); bool somethingChanged = false; somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class @@ -64,6 +64,8 @@ bool PolyLineEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(normals, setNormals); SET_ENTITY_PROPERTY_FROM_PROPERTIES(strokeColors, setStrokeColors); SET_ENTITY_PROPERTY_FROM_PROPERTIES(isUVModeStretch, setIsUVModeStretch); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(glow, setGlow); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(faceCamera, setFaceCamera); if (somethingChanged) { bool wantDebug = false; @@ -78,125 +80,59 @@ bool PolyLineEntityItem::setProperties(const EntityItemProperties& properties) { return somethingChanged; } - -bool PolyLineEntityItem::appendPoint(const glm::vec3& point) { - if (_points.size() > MAX_POINTS_PER_LINE - 1) { - qCDebug(entities) << "MAX POINTS REACHED!"; - return false; - } - - _points << point; - _pointsChanged = true; - - calculateScaleAndRegistrationPoint(); - - return true; -} - - -bool PolyLineEntityItem::setStrokeWidths(const QVector& strokeWidths) { +void PolyLineEntityItem::setLinePoints(const QVector& points) { withWriteLock([&] { - _strokeWidths = strokeWidths; - _strokeWidthsChanged = true; + _points = points; + _pointsChanged = true; }); - return true; + computeAndUpdateDimensionsAndPosition(); } -bool PolyLineEntityItem::setNormals(const QVector& normals) { +void PolyLineEntityItem::setStrokeWidths(const QVector& strokeWidths) { + withWriteLock([&] { + _widths = strokeWidths; + _widthsChanged = true; + }); + computeAndUpdateDimensionsAndPosition(); +} + +void PolyLineEntityItem::setNormals(const QVector& normals) { withWriteLock([&] { _normals = normals; _normalsChanged = true; }); - return true; } -bool PolyLineEntityItem::setStrokeColors(const QVector& strokeColors) { +void PolyLineEntityItem::setStrokeColors(const QVector& strokeColors) { withWriteLock([&] { - _strokeColors = strokeColors; - _strokeColorsChanged = true; + _colors = strokeColors; + _colorsChanged = true; }); - return true; } +void PolyLineEntityItem::computeAndUpdateDimensionsAndPosition() { + QVector points; + QVector widths; -bool PolyLineEntityItem::setLinePoints(const QVector& points) { - if (points.size() > MAX_POINTS_PER_LINE) { - return false; - } - bool result = false; - withWriteLock([&] { - //Check to see if points actually changed. If they haven't, return before doing anything else - if (points.size() != _points.size()) { - _pointsChanged = true; - } else if (points.size() == _points.size()) { - //same number of points, so now compare every point - for (int i = 0; i < points.size(); i++) { - if (points.at(i) != _points.at(i)) { - _pointsChanged = true; - break; - } - } - } - if (!_pointsChanged) { - return; - } - - _points = points; - - result = true; - }); - - if (result) { - calculateScaleAndRegistrationPoint(); - } - - return result; -} - -void PolyLineEntityItem::calculateScaleAndRegistrationPoint() { - glm::vec3 high(0.0f, 0.0f, 0.0f); - glm::vec3 low(0.0f, 0.0f, 0.0f); - int pointCount = 0; - glm::vec3 firstPoint; withReadLock([&] { - pointCount = _points.size(); - if (pointCount > 0) { - firstPoint = _points.at(0); - } - for (int i = 0; i < pointCount; i++) { - const glm::vec3& point = _points.at(i); - high = glm::max(point, high); - low = glm::min(point, low); - } + points = _points; + widths = _widths; }); - float magnitudeSquared = glm::length2(low - high); - vec3 newScale { 1 }; - vec3 newRegistrationPoint { 0.5f }; + glm::vec3 maxHalfDim(0.5f * ENTITY_ITEM_DEFAULT_WIDTH); + float maxWidth = 0.0f; + for (int i = 0; i < points.length(); i++) { + maxHalfDim = glm::max(maxHalfDim, glm::abs(points[i])); + maxWidth = glm::max(maxWidth, i < widths.length() ? widths[i] : DEFAULT_LINE_WIDTH); + } - const float EPSILON = 0.0001f; - const float EPSILON_SQUARED = EPSILON * EPSILON; - const float HALF_LINE_WIDTH = 0.075f; // sadly _strokeWidths() don't seem to correspond to reality, so just use a flat assumption of the stroke width - const vec3 QUARTER_LINE_WIDTH { HALF_LINE_WIDTH * 0.5f }; - if (pointCount > 1 && magnitudeSquared > EPSILON_SQUARED) { - newScale = glm::abs(high) + glm::abs(low) + vec3(HALF_LINE_WIDTH); - // Center the poly line in the bounding box - glm::vec3 startPointInScaleSpace = firstPoint - low; - startPointInScaleSpace += QUARTER_LINE_WIDTH; - newRegistrationPoint = startPointInScaleSpace / newScale; - } - - // if Polyline has only one or fewer points, use default dimension settings - setScaledDimensions(newScale); - EntityItem::setRegistrationPoint(newRegistrationPoint); + setScaledDimensions(2.0f * (maxHalfDim + maxWidth)); } int PolyLineEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args, EntityPropertyFlags& propertyFlags, bool overwriteLocalData, bool& somethingChanged) { - - QWriteLocker lock(&_quadReadWriteLock); int bytesRead = 0; const unsigned char* dataAt = data; @@ -208,6 +144,8 @@ int PolyLineEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* da READ_ENTITY_PROPERTY(PROP_STROKE_NORMALS, QVector, setNormals); READ_ENTITY_PROPERTY(PROP_STROKE_COLORS, QVector, setStrokeColors); READ_ENTITY_PROPERTY(PROP_IS_UV_MODE_STRETCH, bool, setIsUVModeStretch); + READ_ENTITY_PROPERTY(PROP_LINE_GLOW, bool, setGlow); + READ_ENTITY_PROPERTY(PROP_LINE_FACE_CAMERA, bool, setFaceCamera); return bytesRead; } @@ -222,6 +160,8 @@ EntityPropertyFlags PolyLineEntityItem::getEntityProperties(EncodeBitstreamParam requestedProperties += PROP_STROKE_NORMALS; requestedProperties += PROP_STROKE_COLORS; requestedProperties += PROP_IS_UV_MODE_STRETCH; + requestedProperties += PROP_LINE_GLOW; + requestedProperties += PROP_LINE_FACE_CAMERA; return requestedProperties; } @@ -233,7 +173,6 @@ void PolyLineEntityItem::appendSubclassData(OctreePacketData* packetData, Encode int& propertyCount, OctreeElement::AppendState& appendState) const { - QWriteLocker lock(&_quadReadWriteLock); bool successPropertyFits = true; APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); @@ -244,6 +183,8 @@ void PolyLineEntityItem::appendSubclassData(OctreePacketData* packetData, Encode APPEND_ENTITY_PROPERTY(PROP_STROKE_NORMALS, getNormals()); APPEND_ENTITY_PROPERTY(PROP_STROKE_COLORS, getStrokeColors()); APPEND_ENTITY_PROPERTY(PROP_IS_UV_MODE_STRETCH, getIsUVModeStretch()); + APPEND_ENTITY_PROPERTY(PROP_LINE_GLOW, getGlow()); + APPEND_ENTITY_PROPERTY(PROP_LINE_FACE_CAMERA, getFaceCamera()); } void PolyLineEntityItem::debugDump() const { @@ -255,61 +196,49 @@ void PolyLineEntityItem::debugDump() const { qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); } - - QVector PolyLineEntityItem::getLinePoints() const { - QVector result; - withReadLock([&] { - result = _points; + return resultWithReadLock>([&] { + return _points; }); - return result; } QVector PolyLineEntityItem::getNormals() const { - QVector result; - withReadLock([&] { - result = _normals; + return resultWithReadLock>([&] { + return _normals; }); - return result; } QVector PolyLineEntityItem::getStrokeColors() const { - QVector result; - withReadLock([&] { - result = _strokeColors; + return resultWithReadLock>([&] { + return _colors; }); - return result; } QVector PolyLineEntityItem::getStrokeWidths() const { - QVector result; - withReadLock([&] { - result = _strokeWidths; + return resultWithReadLock>([&] { + return _widths; }); - return result; } QString PolyLineEntityItem::getTextures() const { - QString result; - withReadLock([&] { - result = _textures; + return resultWithReadLock([&] { + return _textures; }); - return result; } void PolyLineEntityItem::setTextures(const QString& textures) { withWriteLock([&] { if (_textures != textures) { _textures = textures; - _texturesChangedFlag = true; + _texturesChanged = true; } }); } void PolyLineEntityItem::setColor(const glm::u8vec3& value) { withWriteLock([&] { - _strokeColorsChanged = true; _color = value; + _colorsChanged = true; }); } diff --git a/libraries/entities/src/PolyLineEntityItem.h b/libraries/entities/src/PolyLineEntityItem.h index 4420a123c5..41acc6d6d8 100644 --- a/libraries/entities/src/PolyLineEntityItem.h +++ b/libraries/entities/src/PolyLineEntityItem.h @@ -44,35 +44,40 @@ class PolyLineEntityItem : public EntityItem { glm::u8vec3 getColor() const; void setColor(const glm::u8vec3& value); - bool setLinePoints(const QVector& points); - bool appendPoint(const glm::vec3& point); + static const int MAX_POINTS_PER_LINE; + void setLinePoints(const QVector& points); QVector getLinePoints() const; - bool setNormals(const QVector& normals); + static const float DEFAULT_LINE_WIDTH; + void setStrokeWidths(const QVector& strokeWidths); + QVector getStrokeWidths() const; + + void setNormals(const QVector& normals); QVector getNormals() const; - bool setStrokeColors(const QVector& strokeColors); + void setStrokeColors(const QVector& strokeColors); QVector getStrokeColors() const; - bool setStrokeWidths(const QVector& strokeWidths); - QVector getStrokeWidths() const; - void setIsUVModeStretch(bool isUVModeStretch){ _isUVModeStretch = isUVModeStretch; } bool getIsUVModeStretch() const{ return _isUVModeStretch; } QString getTextures() const; void setTextures(const QString& textures); - virtual ShapeType getShapeType() const override { return SHAPE_TYPE_NONE; } + void setGlow(bool glow) { _glow = glow; } + bool getGlow() const { return _glow; } + + void setFaceCamera(bool faceCamera) { _faceCamera = faceCamera; } + bool getFaceCamera() const { return _faceCamera; } bool pointsChanged() const { return _pointsChanged; } bool normalsChanged() const { return _normalsChanged; } - bool strokeColorsChanged() const { return _strokeColorsChanged; } - bool strokeWidthsChanged() const { return _strokeWidthsChanged; } - bool texturesChanged() const { return _texturesChangedFlag; } - void resetTexturesChanged() { _texturesChangedFlag = false; } - void resetPolyLineChanged() { _strokeColorsChanged = _strokeWidthsChanged = _normalsChanged = _pointsChanged = false; } + bool colorsChanged() const { return _colorsChanged; } + bool widthsChanged() const { return _widthsChanged; } + bool texturesChanged() const { return _texturesChanged; } + void resetTexturesChanged() { _texturesChanged = false; } + void resetPolyLineChanged() { _colorsChanged = _widthsChanged = _normalsChanged = _pointsChanged = false; } // never have a ray intersection pick a PolyLineEntityItem. virtual bool supportsDetailedIntersection() const override { return true; } @@ -85,29 +90,26 @@ class PolyLineEntityItem : public EntityItem { BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo, bool precisionPicking) const override { return false; } - // disable these external interfaces as PolyLineEntities caculate their own dimensions based on the points they contain - virtual void setRegistrationPoint(const glm::vec3& value) override {}; // FIXME: this is suspicious! - virtual void debugDump() const override; - static const float DEFAULT_LINE_WIDTH; - static const int MAX_POINTS_PER_LINE; private: - void calculateScaleAndRegistrationPoint(); + void computeAndUpdateDimensionsAndPosition(); protected: glm::u8vec3 _color; - bool _pointsChanged { true }; - bool _normalsChanged { true }; - bool _strokeColorsChanged { true }; - bool _strokeWidthsChanged { true }; QVector _points; QVector _normals; - QVector _strokeColors; - QVector _strokeWidths; + QVector _colors; + QVector _widths; QString _textures; bool _isUVModeStretch; - bool _texturesChangedFlag { false }; - mutable QReadWriteLock _quadReadWriteLock; + bool _glow; + bool _faceCamera; + + bool _pointsChanged { false }; + bool _normalsChanged { false }; + bool _colorsChanged { false }; + bool _widthsChanged { false }; + bool _texturesChanged { false }; }; #endif // hifi_PolyLineEntityItem_h diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index c8e2aaf51e..785ac26b36 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -33,7 +33,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return static_cast(EntityVersion::MorePropertiesCleanup); + return static_cast(EntityVersion::UpdatedPolyLines); case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::ConicalFrustums); case PacketType::AvatarIdentity: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 33f3f967c9..bf58866f97 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -251,7 +251,8 @@ enum class EntityVersion : PacketVersion { ImageEntities, GridEntities, MissingTextProperties, - MorePropertiesCleanup + MorePropertiesCleanup, + UpdatedPolyLines }; enum class EntityScriptCallMethodVersion : PacketVersion { From 556f516be669721be33e9537e930a93017ecb792 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 20 Dec 2018 16:30:08 -0800 Subject: [PATCH 10/72] Update uploader screen --- .../resources/qml/hifi/AvatarPackager.qml | 25 +- .../avatarPackager/AvatarPackagerFooter.qml | 6 +- .../qml/hifi/avatarPackager/AvatarProject.qml | 223 +++++++++------- interface/src/avatar/AvatarProject.cpp | 39 ++- interface/src/avatar/AvatarProject.h | 2 +- .../src/avatar/MarketplaceItemUploader.cpp | 240 +++++++++++------- .../src/avatar/MarketplaceItemUploader.h | 5 + libraries/fbx/src/FST.h | 2 + 8 files changed, 339 insertions(+), 203 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 8eb765716e..4d27e6fce4 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -8,6 +8,7 @@ import "../stylesUit" 1.0 import "../windows" as Windows import "../dialogs" import "avatarPackager" +import "avatarapp" 1.0 as AvatarApp Windows.ScrollingWindow { id: root @@ -27,18 +28,14 @@ Windows.ScrollingWindow { id: windowContent height: pane.height width: pane.width - anchors.fill: parent - // FIXME: modal overlay does not show - Rectangle { - id: modalOverlay + AvatarApp.MessageBox { + id: popup anchors.fill: parent - z: 20000 - color: "#aa031b33" - clip: true - visible: true + visible: false } + // FIXME: modal overlay does not show Column { id: avatarPackager anchors.fill: parent @@ -61,6 +58,12 @@ Windows.ScrollingWindow { PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } PropertyChanges { target: avatarProject; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer } + }, + State { + name: "project-upload" + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } + PropertyChanges { target: avatarUploader; visible: true } + PropertyChanges { target: avatarPackagerFooter; color: "blue"; visible: false } } ] @@ -86,6 +89,12 @@ Windows.ScrollingWindow { anchors.fill: parent } + AvatarProjectUpload { + id: avatarUploader + anchors.fill: parent + root: avatarProject + } + CreateAvatarProject { id: createAvatarProject colorScheme: root.colorScheme diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml index 526a2047e3..8498d20858 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml @@ -7,10 +7,10 @@ Rectangle { id: avatarPackagerFooter color: "#404040" - height: 74 + height: content === defaultContent ? 0 : 74 width: parent.width - property var content: Item { } + property var content: Item { id: defaultContent } children: [background, content] @@ -21,4 +21,4 @@ Rectangle { border.width: 2; } -} +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 0aca722352..286ebb23ba 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -25,6 +25,7 @@ Item { anchors.rightMargin: 17 HifiControls.Button { id: uploadButton + enabled: Account.loggedIn //width: parent.width //anchors.bottom: parent.bottom anchors.verticalCenter: parent.verticalCenter @@ -35,26 +36,82 @@ Item { width: 133 height: 40 onClicked: function() { - console.log("Uploading"); - root.uploader = AvatarPackagerCore.currentAvatarProject.upload(); - console.log("uploader: "+ root.uploader); - root.uploader.uploadProgress.connect(function(uploaded, total) { - console.log("Uploader progress: " + uploaded + " / " + total); - }); - root.uploader.completed.connect(function() { - try { - var response = JSON.parse(root.uploader.responseData); - console.log("Uploader complete! " + response); - uploadStatus.text = response.status; - } catch (e) { - console.log("Error parsing JSON: " + root.uploader.reponseData); - } - }); - root.uploader.send(); + if (AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID()) { + showConfirmUploadPopup(uploadNew, uploadUpdate); + } else { + uploadNew(); + } } } } + function uploadNew() { + console.log("Uploading new"); + upload(false); + } + function uploadUpdate() { + console.log("Uploading update"); + upload(true); + } + + function upload(updateExisting) { + root.uploader = AvatarPackagerCore.currentAvatarProject.upload(updateExisting); + console.log("uploader: "+ root.uploader); + root.uploader.uploadProgress.connect(function(uploaded, total) { + console.log("Uploader progress: " + uploaded + " / " + total); + }); + root.uploader.completed.connect(function() { + try { + var response = JSON.parse(root.uploader.responseData); + console.log("Uploader complete! " + response); + uploadStatus.text = response.status; + } catch (e) { + console.log("Error parsing JSON: " + root.uploader.reponseData); + } + }); + root.uploader.send(); + avatarPackager.state = "project-upload"; + } + + 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(); + //popup.forceActiveFocus(); + } + + 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(); + //popup.forceActiveFocus(); + } + RalewaySemiBold { id: avatarFBXNameLabel size: 14 @@ -79,11 +136,15 @@ Item { } Rectangle { + id: fileList + + visible: false + color: "white" - visible: AvatarPackagerCore.currentAvatarProject !== null anchors.top: openFolderButton.bottom anchors.left: parent.left anchors.right: parent.right + anchors.bottom: showFilesText.top //anchors.bottom: uploadButton.top anchors.topMargin: 10 anchors.bottomMargin: 10 @@ -92,96 +153,82 @@ Item { ListView { anchors.fill: parent model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles - delegate: Text { text: 'File: ' + modelData } + delegate: Rectangle { + width: parent.width + height: fileText.implicitHeight + 10 + color: (index % 2 == 0) ? "white" : "grey" + Text { + id: fileText + anchors.top: parent.top + 5 + anchors.left: parent.left + 5 + text: modelData + } + } } } + Text { + id: showFilesText + + visible: AvatarPackagerCore.currentAvatarProject !== null + + anchors.bottom: loginRequiredMessage.top + anchors.bottomMargin: 10 + + font.pointSize: 12 + text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in the project. " + (fileList.visible ? "Hide" : "Show") + " list" + + onLinkActivated: fileList.visible = !fileList.visible + } + Rectangle { - id: uploadingScreen + id: loginRequiredMessage - visible: !!root.uploader - anchors.fill: parent + visible: !Account.loggedIn + height: loginRequiredTextRow.height + 20 - color: "black" - - Item { - visible: !!root.uploader && !root.uploader.complete - - anchors.fill: parent - - AnimatedImage { - id: uploadSpinner - - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } - - source: "../../../icons/loader-snake-64-w.gif" - playing: true - z: 10000 - } + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right } - Item { - visible: !!root.uploader && root.uploader.complete + 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: successIcon + id: loginWarningGlyph - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left - size: 128 - text: "\ue01a" - color: "#1FC6A6" - } - - Text { - text: "Congratulations!" - - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: successIcon.bottom - - color: "white" - } - - HifiControls.Button { width: implicitWidth - height: implicitHeight - anchors.bottom: parent.bottom + font.pointSize: 20 + text: "+" + color: "black" + } + Text { + id: loginWarningText + + anchors.verticalCenter: parent.verticalCenter + anchors.left: loginWarningGlyph.right anchors.right: parent.right - text: "View in Inventory" - - color: hifi.buttons.blue - colorScheme: root.colorScheme - onClicked: function() { - console.log("Opening in inventory"); - - AvatarPackagerCore.currentAvatarProject.openInInventory(); - } + text: "Please login to upload your avatar to High Fidelity hosting." + font.pointSize: 12 + wrapMode: Text.Wrap } } - - Column { - Text { - id: uploadStatus - - text: "Uploading" - color: "white" - - } - Text { - text: "State: " + (!!root.uploader ? root.uploader.state : " NONE") - color: "white" - } - } - } } diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 7ba9d395eb..4a14479cce 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -17,8 +17,9 @@ #include #include #include -#include "FBXSerializer.h" +#include +#include "FBXSerializer.h" #include #include "scripting/HMDScriptingInterface.h" @@ -57,20 +58,16 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& avatarProjectNa std::shared_ptr hfmModel; - try { qDebug() << "Reading FBX file : " << fbxInfo.filePath(); const QByteArray fbxContents = fbx.readAll(); hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath()); - } - catch (const QString& error) { + } catch (const QString& error) { qDebug() << "Error reading: " << error; return nullptr; } //TODO: copy/fix textures here: - - FST* fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel); fst->setName(avatarProjectName); @@ -89,7 +86,6 @@ bool AvatarProject::isValidNewProjectName(const QString& projectName) { AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : AvatarProject::AvatarProject(new FST(fstPath, FSTReader::readMapping(data))) { - } AvatarProject::AvatarProject(FST* fst) { _fst = fst; @@ -119,8 +115,22 @@ void AvatarProject::refreshProjectFiles() { appendDirectory("", _directory); } -MarketplaceItemUploader* AvatarProject::upload() { - return new MarketplaceItemUploader("test_avatar", "blank description", QFileInfo(getFSTPath()).fileName(), QUuid(), _projectFiles); +MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { + QUuid itemID; + if (updateExisting) { + itemID = _fst->getMarketplaceID(); + } + auto uploader = new MarketplaceItemUploader(getProjectName(), "Empty description", QFileInfo(getFSTPath()).fileName(), itemID, + _projectFiles); + connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() { + if (uploader->getError() == MarketplaceItemUploader::Error::None) { + _fst->setMarketplaceID(uploader->getMarketplaceID()); + // TODO(thoys) uncomment this + //_fst->write(); + } + }); + + return uploader; } void AvatarProject::openInInventory() { @@ -128,7 +138,12 @@ void AvatarProject::openInInventory() { DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml"); DependencyManager::get()->openTablet(); - tablet->sendToQml(QVariantMap({ - { "method", "updatePurchases" }, - { "filterText", "filtertext" } })); + auto name = getProjectName(); + + // I'm not a fan of this, but it's the only current option. + QTimer::singleShot(1000, [name]() { + auto tablet = dynamic_cast( + DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); + tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } })); + }); } diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 023cd7b079..b164e67e0d 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -41,7 +41,7 @@ public: return false; } - Q_INVOKABLE MarketplaceItemUploader* upload(); + Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); Q_INVOKABLE void openInInventory(); /** diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 7a5abacce4..1559f359a7 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -67,10 +67,9 @@ void MarketplaceItemUploader::doGetCategories() { QNetworkReply* reply = networkAccessManager.get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { - auto doc = QJsonDocument::fromJson(reply->readAll()); - 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()) { @@ -119,116 +118,175 @@ void MarketplaceItemUploader::doGetCategories() { } void MarketplaceItemUploader::doUploadAvatar() { - QBuffer buffer{ &_fileData }; - //buffer.open(QIODevice::WriteOnly); - QuaZip zip{ &buffer }; - if (!zip.open(QuaZip::Mode::mdAdd)) { - qWarning() << "Failed to open zip!!"; + QBuffer buffer{ &_fileData }; + //buffer.open(QIODevice::WriteOnly); + QuaZip zip{ &buffer }; + if (!zip.open(QuaZip::Mode::mdAdd)) { + qWarning() << "Failed to open zip!!"; + } + + for (auto& filePath : _filePaths) { + qWarning() << "Zipping: " << filePath; + QFileInfo fileInfo{ filePath }; + + QuaZipFile zipFile{ &zip }; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileInfo.fileName()))) { + qWarning() << "Could not open zip file:" << zipFile.getZipError(); + _error = Error::Unknown; + setState(State::Complete); + return; } - - for (auto& filePath : _filePaths) { - qWarning() << "Zipping: " << filePath; - QFileInfo fileInfo{ filePath }; - - QuaZipFile zipFile{ &zip }; - if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileInfo.fileName()))) { - qWarning() << "Could not open zip file:" << zipFile.getZipError(); - _error = Error::Unknown; - setState(State::Complete); - return; - } - QFile file{ filePath }; - if (file.open(QIODevice::ReadOnly)) { - zipFile.write(file.readAll()); - } else { - qWarning() << "Failed to open: " << filePath; - } - 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); - qWarning() << "Request url is: " << request.url(); - - QJsonObject root{ { "marketplace_item", - QJsonObject{ { "title", _title }, - { "description", _description }, - { "root_file_key", _rootFilename }, - { "category_ids", QJsonArray({ 5 }) }, - //{ "attributions", QJsonArray({ QJsonObject{ { "name", "" }, { "link", "" } } }) }, - { "license", 0 }, - { "files", QString::fromLatin1(_fileData.toBase64()) } } } }; - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - QJsonDocument doc{ root }; - - qWarning() << "data: " << doc.toJson(); - - _fileData.toBase64(); - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - - QNetworkReply* reply{ nullptr }; - if (creating) { - reply = networkAccessManager.post(request, doc.toJson()); + QFile file{ filePath }; + if (file.open(QIODevice::ReadOnly)) { + zipFile.write(file.readAll()); } else { - reply = networkAccessManager.put(request, doc.toJson()); + qWarning() << "Failed to open: " << filePath; } + file.close(); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qWarning() << "Could not close zip file: " << zipFile.getZipError(); + setState(State::Complete); + return; + } + } - connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + zip.close(); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { + 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); + qWarning() << "Request url is: " << request.url(); + + QJsonObject root{ { "marketplace_item", + QJsonObject{ { "title", _title }, + { "description", _description }, + { "root_file_key", _rootFilename }, + { "category_ids", QJsonArray({ 5 }) }, + //{ "attributions", QJsonArray({ QJsonObject{ { "name", "" }, { "link", "" } } }) }, + { "license", 0 }, + { "files", QString::fromLatin1(_fileData.toBase64()) } } } }; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + QJsonDocument doc{ root }; + + qWarning() << "data: " << doc.toJson(); + + _fileData.toBase64(); + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkReply* reply{ nullptr }; + if (creating) { + reply = networkAccessManager.post(request, doc.toJson()); + } else { + reply = networkAccessManager.put(request, doc.toJson()); + } + + connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + //_responseData = reply->readAll(); + auto error = reply->error(); + if (error == QNetworkReply::NoError) { _responseData = reply->readAll(); qWarning() << "Finished request " << _responseData; - 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(); doWaitForInventory(); } else { _error = Error::Unknown; setState(State::Complete); } - }); + } else { + _error = Error::Unknown; + setState(State::Complete); + } + }); - setState(State::UploadingAvatar); + setState(State::UploadingAvatar); } void MarketplaceItemUploader::doWaitForInventory() { - static const QString path = "/api/v1/commerce/inventory"; + static const QString path = "/api/v1/commerce/inventory"; - auto accountManager = DependencyManager::get(); - auto request = accountManager->createRequest(path, AccountManagerAuth::Required); + auto accountManager = DependencyManager::get(); + auto request = accountManager->createRequest(path, AccountManagerAuth::Required); - qWarning() << "Request url is: " << request.url(); + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkReply* reply = networkAccessManager.post(request, ""); - QNetworkReply* reply = networkAccessManager.post(request, ""); + _numRequestsForInventory++; - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - auto data = reply->readAll(); - qWarning() << "Finished inventory request " << data; + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + auto data = reply->readAll(); - auto error = reply->error(); - if (error == QNetworkReply::NoError) { - } else { - _error = Error::Unknown; - } + 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"]; + if (version.isDouble()) { + int versionInt = version.toDouble(); + if (versionInt >= _itemVersion) { + return true; + } + } + } + } + return false; + }; + + success = isAssetAvailable(); + } + if (success) { setState(State::Complete); - }); + } else { + qDebug() << "Failed to find item in inventory"; + if (_numRequestsForInventory > 8) { + _error = Error::Unknown; + setState(State::Complete); + } else { + QTimer::singleShot(5000, [this]() { doWaitForInventory(); }); + } + } + }); } diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index a0ec3f6991..9cfd531aca 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -56,6 +56,8 @@ public: State getState() const { return _state; } bool getComplete() const { return _state == State::Complete; } + QUuid getMarketplaceID() const { return _marketplaceID; } + Error getError() const { return _error; } signals: @@ -77,9 +79,12 @@ private: QString _description; QString _rootFilename; QUuid _marketplaceID; + int _itemVersion; QString _responseData; + int _numRequestsForInventory{ 0 }; + QStringList _filePaths; QByteArray _fileData; }; diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h index 524463b721..83bb1e1933 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -37,7 +37,9 @@ public: QString getModelPath() const { return _modelPath; } void setModelPath(const QString& modelPath); + Q_INVOKABLE bool hasMarketplaceID() const { return !_marketplaceID.isNull(); } QUuid getMarketplaceID() const { return _marketplaceID; } + void setMarketplaceID(QUuid marketplaceID) { _marketplaceID = marketplaceID; } QString getPath() { return _fstPath; } From 7602da6f928925afa5eb42aaa76ae31c85f9b355 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 21 Dec 2018 08:16:18 -0800 Subject: [PATCH 11/72] Add AvatarProjectUpload.qml --- .../avatarPackager/AvatarProjectUpload.qml | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml new file mode 100644 index 0000000000..bbeca6ab3b --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -0,0 +1,180 @@ +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: !!root.uploader + visible: false + anchors.fill: parent + + Timer { + id: backToProjectTimer + interval: 2000 + running: false + repeat: false + onTriggered: avatarPackager.state = "project" + } + + onVisibleChanged: { + console.log("Visibility changed"); + if (visible) { + root.uploader.completed.connect(function() { + console.log("Did complete"); + backToProjectTimer.start(); + }); + } + } + + Item { + visible: !!root.uploader && !root.uploader.complete + + anchors.fill: parent + + AnimatedImage { + id: uploadSpinner + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + source: "../../../icons/loader-snake-64-w.gif" + playing: true + z: 10000 + } + } + + Item { + id: failureScreen + + visible: !!root.uploader && root.uploader.complete && root.uploader.error !== 0 + + anchors.fill: parent + + HiFiGlyphs { + id: errorIcon + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + size: 128 + text: "w" + color: "red" + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: errorIcon.bottom + Text { + anchors.horizontalCenter: parent.horizontalCenter + + text: "Error" + font.pointSize: 24 + + color: "white" + } + Text { + text: "Your avatar has not been uploaded." + font.pointSize: 16 + + anchors.horizontalCenter: parent.horizontalCenter + + color: "white" + } + } + } + + Item { + id: successScreen + + visible: !!root.uploader && root.uploader.complete && root.uploader.error === 0 + + anchors.fill: parent + + HiFiGlyphs { + id: successIcon + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + size: 128 + text: "\ue01a" + color: "#1FC6A6" + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: successIcon.bottom + + Text { + id: successText + + anchors.horizontalCenter: parent.horizontalCenter + + text: "Congratulations!" + font.pointSize: 24 + + color: "white" + } + Text { + text: "Your avatar has been uploaded." + font.pointSize: 16 + + anchors.horizontalCenter: parent.horizontalCenter + + color: "white" + } + } + + HifiControls.Button { + width: implicitWidth + height: implicitHeight + + anchors.bottom: parent.bottom + anchors.right: parent.right + + text: "View in Inventory" + + color: hifi.buttons.blue + colorScheme: root.colorScheme + onClicked: function() { + console.log("Opening in inventory"); + + AvatarPackagerCore.currentAvatarProject.openInInventory(); + } + } + } + + Column { + id: debugInfo + + visible: false + + Text { + id: uploadStatus + + text: "Uploading" + color: "white" + + } + Text { + text: "State: " + (!!root.uploader ? root.uploader.state : " NONE") + color: "white" + } + } + +} \ No newline at end of file From ad2d9bc79a9c4a2691468fc7c37ceb8229125370 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 21 Dec 2018 19:34:54 +0100 Subject: [PATCH 12/72] - fst read/write should work - images are being copied into the correct directory - scripts are added to fst upon project load - modal overlay fix --- .../resources/qml/hifi/AvatarPackager.qml | 35 ++++++--- .../avatarPackager/AvatarPackagerFooter.qml | 1 + .../avatarPackager/AvatarPackagerHeader.qml | 2 +- .../avatarPackager/AvatarPackagerState.qml | 9 +++ .../avatarPackager/CreateAvatarProject.qml | 48 +++++++----- .../avatarPackager/ProjectInputControl.qml | 5 +- .../resources/qml/hifi/avatarPackager/qmldir | 2 + interface/src/avatar/AvatarPackager.cpp | 5 +- interface/src/avatar/AvatarProject.cpp | 76 ++++++++++++++++--- interface/src/avatar/AvatarProject.h | 11 +-- libraries/fbx/src/FST.cpp | 35 ++++++--- libraries/fbx/src/FST.h | 5 ++ libraries/fbx/src/FSTReader.cpp | 2 +- libraries/fbx/src/FSTReader.h | 1 + 14 files changed, 177 insertions(+), 60 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/qmldir diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 4d27e6fce4..3dce2c1126 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -7,7 +7,7 @@ import "../controlsUit" 1.0 as HifiControls import "../stylesUit" 1.0 import "../windows" as Windows import "../dialogs" -import "avatarPackager" +import "./avatarPackager" 1.0 import "avatarapp" 1.0 as AvatarApp Windows.ScrollingWindow { @@ -29,32 +29,46 @@ Windows.ScrollingWindow { height: pane.height width: pane.width + Rectangle { + id: modalOverlay + anchors.fill: parent + z: 20 + color: "#aa031b33" + 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 } - // FIXME: modal overlay does not show Column { id: avatarPackager anchors.fill: parent state: "main" states: [ State { - name: "main" + name: AvatarPackagerState.main PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); faqEnabled: true; backButtonEnabled: false } PropertyChanges { target: avatarPackagerMain; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer } }, State { - name: "createProject" + name: AvatarPackagerState.createProject PropertyChanges { target: avatarPackagerHeader; title: qsTr("Create Project") } PropertyChanges { target: createAvatarProject; visible: true } PropertyChanges { target: avatarPackagerFooter; content: createAvatarProject.footer } }, State { - name: "project" + name: AvatarPackagerState.project PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } PropertyChanges { target: avatarProject; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer } @@ -67,6 +81,8 @@ Windows.ScrollingWindow { } ] + property alias showModalOverlay: modalOverlay.visible + AvatarPackagerHeader { id: avatarPackagerHeader onBackButtonClicked: { @@ -119,7 +135,7 @@ Windows.ScrollingWindow { text: qsTr("New Project") colorScheme: root.colorScheme onClicked: { - avatarPackager.state = "createProject"; + avatarPackager.state = AvatarPackagerState.createProject; } } @@ -133,7 +149,7 @@ Windows.ScrollingWindow { color: hifi.buttons.blue colorScheme: root.colorScheme onClicked: { - // TODO: make the dialog modal + avatarPackager.showModalOverlay = true; let browser = desktop.fileDialog({ selectDirectory: false, dir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH), @@ -142,14 +158,15 @@ Windows.ScrollingWindow { }); browser.canceled.connect(function() { - + avatarPackager.showModalOverlay = false; }); browser.selectedFile.connect(function(fileUrl) { let fstFilePath = fileDialogHelper.urlToPath(fileUrl); let currentAvatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); if (currentAvatarProject) { - avatarPackager.state = "project"; + avatarPackager.state = AvatarPackagerState.project; + avatarPackager.showModalOverlay = false; } }); } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml index 8498d20858..d7725b250e 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml @@ -8,6 +8,7 @@ Rectangle { color: "#404040" height: content === defaultContent ? 0 : 74 + visible: content !== defaultContent width: parent.width property var content: Item { id: defaultContent } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 6dcb1267d4..84096e352c 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -65,7 +65,7 @@ Rectangle { anchors.right: parent.right anchors.rightMargin: 16 anchors.verticalCenter: faq.verticalCenter - text: qsTr("FAQ") + text: qsTr("Docs") color: "white" } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml new file mode 100644 index 0000000000..f12edf4952 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml @@ -0,0 +1,9 @@ +pragma Singleton +import QtQuick 2.6 + +Item { + id: singleton + readonly property string main: "main" + readonly property string project: "project" + readonly property string createProject: "createProject" +} diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml index 4d1f745fa5..41d33e6058 100644 --- a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -27,7 +27,7 @@ Item { Window.alert('Failed to create project') return; } - avatarPackager.state = "project"; + avatarPackager.state = AvatarPackagerState.project; } } } @@ -37,6 +37,26 @@ Item { height: parent.height width: parent.width + + property var errorMessages: QtObject { + readonly property string fileExists: "A folder with that name already exists at that location. Please choose a different project name or location." + } + + RalewayRegular { + id: errorMessage + visible: text !== "" + text: "" + color: "#EA4C5F"; + wrapMode: Text.WordWrap + size: 20 + anchors { + top: createAvatarColumns.bottom + bottom: parent.bottom + left: parent.left + right: parent.right + } + } + Column { id: createAvatarColumns anchors.left: parent.left @@ -45,6 +65,8 @@ Item { spacing: 17 + property string defaultFileBrowserPath: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + ProjectInputControl { id: name label: "Name" @@ -57,9 +79,8 @@ Item { colorScheme: root.colorScheme browseEnabled: true browseFolder: true - browseDir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath browseTitle: "Project Location" - text: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) onTextChanged: { //TODO: valid folder? Does project with name exist here already? } @@ -71,11 +92,13 @@ Item { colorScheme: root.colorScheme browseEnabled: true browseFolder: false - browseDir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath browseFilter: "Avatar Model File (*.fbx)" browseTitle: "Open Avatar Model (.fbx)" onTextChanged: { - //TODO: try to get texture folder from fbx if none is set? + if (avatarModel.text !== "") { + textureFolder.browseDir = fileDialogHelper.pathToUrl(avatarModel.text.split('/')[0]); + } } } @@ -85,7 +108,7 @@ Item { colorScheme: root.colorScheme browseEnabled: true browseFolder: true - browseDir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath browseTitle: "Texture Folder" onTextChanged: { //TODO: valid folder? @@ -93,16 +116,5 @@ Item { } } } - RalewayRegular { - text: "A folder with that name already exists at that location. Please choose a different project name or location." - color: "#EA4C5F"; - wrapMode: Text.WordWrap - size: 20 - anchors { - top: createAvatarColumns.bottom - bottom: parent.bottom - left: parent.left - right: parent.right - } - } + } diff --git a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml index 472db47c2f..664acd6f22 100644 --- a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml +++ b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml @@ -55,7 +55,7 @@ Column { text: qsTr("Browse") colorScheme: root.colorScheme onClicked: { - // TODO: make the dialog modal + avatarPackager.showModalOverlay = true; let browser = desktop.fileDialog({ selectDirectory: browseFolder, dir: browseDir, @@ -64,11 +64,12 @@ Column { }); 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/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/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index ba3089aa2f..c7f15d616c 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -30,6 +30,9 @@ AvatarPackager::AvatarPackager() { qRegisterMetaType(); qRegisterMetaType(); }); + + QDir defaultProjectsDir(AvatarProject::getDefaultProjectsPath()); + defaultProjectsDir.mkpath("."); } bool AvatarPackager::open() { @@ -57,7 +60,7 @@ AvatarProject* AvatarPackager::createAvatarProject(const QString& projectsFolder if (_currentAvatarProject) { _currentAvatarProject->deleteLater(); } - _currentAvatarProject = AvatarProject::createAvatarProject(avatarProjectName, avatarModelPath); + _currentAvatarProject = AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder); qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); emit avatarProjectChanged(); diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 4a14479cce..901c0621fb 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -21,6 +21,7 @@ #include "FBXSerializer.h" #include +#include #include "scripting/HMDScriptingInterface.h" AvatarProject* AvatarProject::openAvatarProject(const QString& path) { @@ -36,17 +37,25 @@ AvatarProject* AvatarProject::openAvatarProject(const QString& path) { return project; } -AvatarProject* AvatarProject::createAvatarProject(const QString& avatarProjectName, const QString& avatarModelPath) { +AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder) { if (!isValidNewProjectName(avatarProjectName)) { return nullptr; } - QDir dir(getDefaultProjectsPath() + "/" + avatarProjectName); - if (!dir.mkpath(".")) { + QDir projectDir(projectsFolder + "/" + avatarProjectName); + if (!projectDir.mkpath(".")) { + return nullptr; + } + QDir projectTexturesDir(projectDir.path() + "/textures"); + if (!projectTexturesDir.mkpath(".")) { + return nullptr; + } + QDir projectScriptsDir(projectDir.path() + "/scripts"); + if (!projectScriptsDir.mkpath(".")) { return nullptr; } const auto fileName = QFileInfo(avatarModelPath).fileName(); - const auto newModelPath = dir.absoluteFilePath(fileName); - const auto newFSTPath = dir.absoluteFilePath("avatar.fst"); + const auto newModelPath = projectDir.absoluteFilePath(fileName); + const auto newFSTPath = projectDir.absoluteFilePath("avatar.fst"); QFile::copy(avatarModelPath, newModelPath); QFileInfo fbxInfo(newModelPath); @@ -66,10 +75,41 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& avatarProjectNa qDebug() << "Error reading: " << error; return nullptr; } - //TODO: copy/fix textures here: + QStringList textures{}; - FST* fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel); + auto addTextureToList = [&textures](hfm::Texture texture) mutable { + if (!texture.filename.isEmpty() && texture.content.isEmpty() && !textures.contains(texture.filename)) { + textures << texture.filename; + } + }; + foreach(const HFMMaterial mat, hfmModel->materials) { + addTextureToList(mat.normalTexture); + addTextureToList(mat.albedoTexture); + addTextureToList(mat.opacityTexture); + addTextureToList(mat.glossTexture); + addTextureToList(mat.roughnessTexture); + addTextureToList(mat.specularTexture); + addTextureToList(mat.metallicTexture); + addTextureToList(mat.emissiveTexture); + addTextureToList(mat.occlusionTexture); + addTextureToList(mat.scatteringTexture); + addTextureToList(mat.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()) { @@ -79,6 +119,22 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& avatarProjectNa return new AvatarProject(fst); } +QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) { + QStringList result{}; + constexpr auto flags = QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; + if (!scriptsDir.exists()) { + return result; + } + + for (auto& script : scriptsDir.entryInfoList({}, flags)) { + if (script.fileName().endsWith(".js")) { + result.push_back("scripts/" + script.fileName()); + } + } + + return result; +} + bool AvatarProject::isValidNewProjectName(const QString& projectName) { QDir dir(getDefaultProjectsPath() + "/" + projectName); return !dir.exists(); @@ -92,6 +148,9 @@ AvatarProject::AvatarProject(FST* fst) { auto fileInfo = QFileInfo(getFSTPath()); _directory = fileInfo.absoluteDir(); + _fst->setScriptPaths(getScriptPaths(QDir(_directory.path() + "/scripts"))); + _fst->write(); + //_projectFiles = _directory.entryList(); refreshProjectFiles(); @@ -125,8 +184,7 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() { if (uploader->getError() == MarketplaceItemUploader::Error::None) { _fst->setMarketplaceID(uploader->getMarketplaceID()); - // TODO(thoys) uncomment this - //_fst->write(); + _fst->write(); } }); diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index b164e67e0d..07a6713557 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -36,11 +36,6 @@ class AvatarProject : public QObject { Q_PROPERTY(QString name READ getProjectName) public: - Q_INVOKABLE bool write() { - // Write FST here - return false; - } - Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); Q_INVOKABLE void openInInventory(); @@ -48,7 +43,8 @@ public: * returns the AvatarProject or a nullptr on failure. */ static AvatarProject* openAvatarProject(const QString& path); - static AvatarProject* createAvatarProject(const QString& avatarProjectName, const QString& avatarModelPath); + static AvatarProject* createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, + const QString& avatarModelPath, const QString& textureFolder); static bool isValidNewProjectName(const QString& projectName); @@ -61,7 +57,7 @@ private: AvatarProject(FST* fst); ~AvatarProject() { - // TODO: cleanup FST / AvatarProjectUploader etc. + _fst->deleteLater(); } Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } @@ -73,6 +69,7 @@ private: void refreshProjectFiles(); void appendDirectory(QString prefix, QDir dir); + QStringList getScriptPaths(const QDir& scriptsDir); FST* _fst; diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index 2631fe951e..af00428a51 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -16,14 +16,23 @@ #include FST::FST(const QString& fstPath, QVariantHash data) : _fstPath(fstPath) { - if (data.contains(NAME_FIELD)) { - _name = data[NAME_FIELD].toString(); - data.remove(NAME_FIELD); - } - if (data.contains(FILENAME_FIELD)) { - _modelPath = data[FILENAME_FIELD].toString(); - data.remove(FILENAME_FIELD); + 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; @@ -42,10 +51,8 @@ FST* FST::createFSTFromModel(QString fstPath, QString modelFilePath, const hfm:: hfmModel.blendshapeChannelNames.contains("Squint_Right")); mapping.insert(NAME_FIELD, QFileInfo(fstPath).baseName()); - QDir root(modelFilePath); - mapping.insert(FILENAME_FIELD, root.relativeFilePath(fstPath)); + mapping.insert(FILENAME_FIELD, QFileInfo(modelFilePath).fileName()); mapping.insert(TEXDIR_FIELD, "textures"); - mapping.insert(SCRIPT_FIELD, "scripts"); // mixamo/autodesk defaults mapping.insert(SCALE_FIELD, 1.0); @@ -150,9 +157,13 @@ void FST::setModelPath(const QString& modelPath) { QVariantHash FST::getMapping() { QVariantHash mapping; - mapping.insertMulti(NAME_FIELD, _name); - mapping.insertMulti(FILENAME_FIELD, _modelPath); 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; } diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h index 83bb1e1933..813d4f3bc5 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -41,6 +41,9 @@ public: QUuid getMarketplaceID() const { return _marketplaceID; } void setMarketplaceID(QUuid marketplaceID) { _marketplaceID = marketplaceID; } + QStringList getScriptPaths() const { return _scriptPaths; } + void setScriptPaths(QStringList scriptPaths) { _scriptPaths = scriptPaths; } + QString getPath() { return _fstPath; } QVariantHash getMapping(); @@ -58,6 +61,8 @@ private: QString _modelPath{}; QUuid _marketplaceID{}; + QStringList _scriptPaths{}; + QVariantHash _other{}; }; 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"; From 748135eef90174326d5b7ae097511a82bfbdb93b Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 21 Dec 2018 11:43:20 -0800 Subject: [PATCH 13/72] Fix login-required size and projectFiles warning --- interface/resources/qml/hifi/AvatarPackager.qml | 2 +- interface/resources/qml/hifi/avatarPackager/AvatarProject.qml | 2 +- interface/src/avatar/AvatarProject.h | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 4d27e6fce4..7e76418e33 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -63,7 +63,7 @@ Windows.ScrollingWindow { name: "project-upload" PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } PropertyChanges { target: avatarUploader; visible: true } - PropertyChanges { target: avatarPackagerFooter; color: "blue"; visible: false } + PropertyChanges { target: avatarPackagerFooter; visible: false } } ] diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 286ebb23ba..5f84452cc1 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -185,7 +185,7 @@ Item { id: loginRequiredMessage visible: !Account.loggedIn - height: loginRequiredTextRow.height + 20 + height: !Account.loggedIn ? loginRequiredTextRow.height + 20 : 0 anchors { bottom: parent.bottom diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index b164e67e0d..6910e2af10 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -28,7 +28,7 @@ class AvatarProject : public QObject { Q_OBJECT Q_PROPERTY(FST* fst READ getFST) - Q_PROPERTY(QStringList projectFiles MEMBER _projectFiles) + Q_PROPERTY(QStringList projectFiles READ getProjectFiles NOTIFY projectFilesChanged) Q_PROPERTY(QString projectFolderPath READ getProjectPath) Q_PROPERTY(QString projectFSTPath READ getFSTPath) @@ -43,6 +43,7 @@ public: Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); Q_INVOKABLE void openInInventory(); + Q_INVOKABLE QStringList getProjectFiles() const { return _projectFiles; } /** * returns the AvatarProject or a nullptr on failure. From b717188ed08711fb5df08b51262d401bd5571a66 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Sat, 22 Dec 2018 21:57:09 -0800 Subject: [PATCH 14/72] Update avatar project styling --- .../avatarPackager/AvatarPackagerFooter.qml | 21 +++++- .../qml/hifi/avatarPackager/AvatarProject.qml | 69 +++++++++++++------ .../qml/hifi/avatarPackager/Style.qml | 20 ++++++ interface/src/avatar/AvatarProject.cpp | 16 ++++- interface/src/avatar/AvatarProject.h | 18 +++-- libraries/avatars/src/ProjectFile.h | 11 +++ 6 files changed, 123 insertions(+), 32 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/Style.qml create mode 100644 libraries/avatars/src/ProjectFile.h diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml index d7725b250e..e1d9396e04 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml @@ -18,8 +18,25 @@ Rectangle { property var background: Rectangle { anchors.fill: parent color: "#404040" - // TODO Use a shadow instead / border is just here for element debug purposes - border.width: 2; + + 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 + } } } \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 5f84452cc1..669748e7c9 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -8,11 +8,14 @@ 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: undefined; @@ -21,6 +24,7 @@ Item { anchors.margins: 10 property var footer: Item { + id: uploadFooter anchors.fill: parent anchors.rightMargin: 17 HifiControls.Button { @@ -91,7 +95,6 @@ Item { }; popup.open(); - //popup.forceActiveFocus(); } function showConfirmCreateNewPopup(confirmCallback) { @@ -102,30 +105,40 @@ Item { popup.button1text = 'CANCEL'; popup.button2text = 'CONFIRM'; - popup.onButton1Clicked = function() { popup.close() }; + popup.onButton1Clicked = function() { + popup.close() + }; popup.onButton2Clicked = function() { popup.close(); uploadNew(); }; popup.open(); - //popup.forceActiveFocus(); } - RalewaySemiBold { - id: avatarFBXNameLabel - size: 14 + RalewayRegular { + id: infoMessage + + color: 'white' + size: 20 + anchors.left: parent.left + anchors.right: parent.right anchors.top: parent.top - anchors.topMargin: 25 - anchors.bottomMargin: 25 - text: qsTr("FBX file here") + + anchors.bottomMargin: 24 + + wrapMode: Text.Wrap + + 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." } HifiControls.Button { id: openFolderButton + + visible: false width: parent.width - anchors.top: avatarFBXNameLabel.bottom + anchors.top: infoMessage.bottom anchors.topMargin: 10 text: qsTr("Open Project Folder") colorScheme: root.colorScheme @@ -140,7 +153,8 @@ Item { visible: false - color: "white" + color: "#6A6A6A" + anchors.top: openFolderButton.bottom anchors.left: parent.left anchors.right: parent.right @@ -155,27 +169,39 @@ Item { model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles delegate: Rectangle { width: parent.width - height: fileText.implicitHeight + 10 - color: (index % 2 == 0) ? "white" : "grey" - Text { + height: fileText.implicitHeight + 8 + color: (index % 2 == 0) ? "#6A6A6A" : "grey" + RalewaySemiBold { id: fileText - anchors.top: parent.top + 5 - anchors.left: parent.left + 5 + 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 } } } } - Text { + RalewayRegular { id: showFilesText + color: 'white' + linkColor: style.colors.blueHighlight + visible: AvatarPackagerCore.currentAvatarProject !== null anchors.bottom: loginRequiredMessage.top anchors.bottomMargin: 10 - font.pointSize: 12 + size: 20 + text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in the project. " + (fileList.visible ? "Hide" : "Show") + " list" onLinkActivated: fileList.visible = !fileList.visible @@ -214,19 +240,20 @@ Item { width: implicitWidth - font.pointSize: 20 + size: 48 text: "+" color: "black" } - Text { + 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." - font.pointSize: 12 + size: 18 wrapMode: Text.Wrap } } 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/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 901c0621fb..2a2ec7c1cb 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -162,7 +162,7 @@ void AvatarProject::appendDirectory(QString prefix, QDir dir) { for (auto& entry : dir.entryInfoList({}, flags)) { if (entry.isFile()) { //_projectFiles.append(prefix + "/" + entry.fileName()); - _projectFiles.append(entry.absoluteFilePath()); + _projectFiles.append({ entry.absoluteFilePath(), prefix + "/" + entry.fileName() }); } else if (entry.isDir()) { appendDirectory(prefix + dir.dirName() + "/", entry.absoluteFilePath()); } @@ -174,13 +174,25 @@ void AvatarProject::refreshProjectFiles() { 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(); } + QStringList projectFilePaths; + for (auto& path : _projectFiles) { + projectFilePaths.append(path.absolutePath); + } auto uploader = new MarketplaceItemUploader(getProjectName(), "Empty description", QFileInfo(getFSTPath()).fileName(), itemID, - _projectFiles); + projectFilePaths); connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() { if (uploader->getError() == MarketplaceItemUploader::Error::None) { _fst->setMarketplaceID(uploader->getMarketplaceID()); diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 3b4a4b74f9..d5cd7762a1 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -14,6 +14,7 @@ #define hifi_AvatarProject_h #include "MarketplaceItemUploader.h" +#include "ProjectFile.h" #include "FST.h" #include @@ -38,14 +39,16 @@ class AvatarProject : public QObject { public: Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); Q_INVOKABLE void openInInventory(); - Q_INVOKABLE QStringList getProjectFiles() const { return _projectFiles; } + Q_INVOKABLE QStringList getProjectFiles() const; /** * returns the AvatarProject or a nullptr on failure. */ static AvatarProject* openAvatarProject(const QString& path); - static AvatarProject* createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, - const QString& avatarModelPath, const QString& textureFolder); + static AvatarProject* createAvatarProject(const QString& projectsFolder, + const QString& avatarProjectName, + const QString& avatarModelPath, + const QString& textureFolder); static bool isValidNewProjectName(const QString& projectName); @@ -53,13 +56,14 @@ public: return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/High Fidelity Projects"; } +signals: + void projectFilesChanged(); + private: AvatarProject(const QString& fstPath, const QByteArray& data); AvatarProject(FST* fst); - ~AvatarProject() { - _fst->deleteLater(); - } + ~AvatarProject() { _fst->deleteLater(); } Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } Q_INVOKABLE QString getProjectPath() const { return _projectPath; } @@ -75,7 +79,7 @@ private: FST* _fst; QDir _directory; - QStringList _projectFiles{}; + QList _projectFiles{}; QString _projectPath; }; diff --git a/libraries/avatars/src/ProjectFile.h b/libraries/avatars/src/ProjectFile.h new file mode 100644 index 0000000000..df92513a1b --- /dev/null +++ b/libraries/avatars/src/ProjectFile.h @@ -0,0 +1,11 @@ +#ifndef hifi_AvatarProjectFile_h +#define hifi_AvatarProjectFile_h + +class ProjectFilePath { + Q_GADGET; +public: + QString absolutePath; + QString relativePath; +}; + +#endif // hifi_AvatarProjectFile_h \ No newline at end of file From 8c56e35f69f71d0d648cb8b313b2b7407307da1c Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Sun, 23 Dec 2018 00:13:01 -0800 Subject: [PATCH 15/72] Add granular status to avatar upload --- .../qml/hifi/avatarPackager/AvatarProject.qml | 4 +- .../avatarPackager/AvatarProjectUpload.qml | 163 +++++++----------- .../src/avatar/MarketplaceItemUploader.cpp | 29 ++-- .../src/avatar/MarketplaceItemUploader.h | 10 +- 4 files changed, 93 insertions(+), 113 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 669748e7c9..81afacf28c 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -25,8 +25,10 @@ Item { property var footer: Item { id: uploadFooter + anchors.fill: parent anchors.rightMargin: 17 + HifiControls.Button { id: uploadButton enabled: Account.loggedIn @@ -64,7 +66,7 @@ Item { root.uploader.uploadProgress.connect(function(uploaded, total) { console.log("Uploader progress: " + uploaded + " / " + total); }); - root.uploader.completed.connect(function() { + root.uploader.finished.connect(function() { try { var response = JSON.parse(root.uploader.responseData); console.log("Uploader complete! " + response); diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index bbeca6ab3b..6e011b1ec7 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -28,7 +28,7 @@ Item { onVisibleChanged: { console.log("Visibility changed"); if (visible) { - root.uploader.completed.connect(function() { + root.uploader.finished.connect(function() { console.log("Did complete"); backToProjectTimer.start(); }); @@ -36,127 +36,94 @@ Item { } Item { - visible: !!root.uploader && !root.uploader.complete + id: uploadStatus + + visible: !!root.uploader anchors.fill: parent - AnimatedImage { - id: uploadSpinner + Item { + id: statusItem - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter + width: parent.width + height: 128 + + AnimatedImage { + id: uploadSpinner + + visible: !!root.uploader && !root.uploader.complete + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + source: "../../../icons/loader-snake-64-w.gif" + playing: true + z: 10000 } - source: "../../../icons/loader-snake-64-w.gif" - playing: true - z: 10000 - } - } + HiFiGlyphs { + id: errorIcon + visible: !!root.uploader && root.uploader.complete && root.uploader.error !== 0 - Item { - id: failureScreen + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } - visible: !!root.uploader && root.uploader.complete && root.uploader.error !== 0 - - anchors.fill: parent - - HiFiGlyphs { - id: errorIcon - - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter + size: 128 + text: "w" + color: "red" } - size: 128 - text: "w" - color: "red" - } + HiFiGlyphs { + id: successIcon - Column { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: errorIcon.bottom - Text { - anchors.horizontalCenter: parent.horizontalCenter + visible: !!root.uploader && root.uploader.complete && root.uploader.error === 0 - text: "Error" - font.pointSize: 24 + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } - color: "white" - } - Text { - text: "Your avatar has not been uploaded." - font.pointSize: 16 - - anchors.horizontalCenter: parent.horizontalCenter - - color: "white" + size: 128 + text: "\ue01a" + color: "#1FC6A6" } } - } + Item { + id: statusRows - Item { - id: successScreen + anchors.top: statusItem.bottom - visible: !!root.uploader && root.uploader.complete && root.uploader.error === 0 + AvatarUploadStatusItem { + id: statusCategories + text: "Retreiving categories" - anchors.fill: parent - - HiFiGlyphs { - id: successIcon - - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter + state: root.uploader.state == 1 ? "running" : (root.uploader.state > 1 ? "success" : (root.uploader.error ? "fail" : "")) } + AvatarUploadStatusItem { + id: statusUploading + anchors.top: statusCategories.bottom + text: "Uploading data" - size: 128 - text: "\ue01a" - color: "#1FC6A6" - } - - Column { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: successIcon.bottom - - Text { - id: successText - - anchors.horizontalCenter: parent.horizontalCenter - - text: "Congratulations!" - font.pointSize: 24 - - color: "white" + state: root.uploader.state == 2 ? "running" : (root.uploader.state > 2 ? "success" : (root.uploader.error ? "fail" : "")) } - Text { - text: "Your avatar has been uploaded." - font.pointSize: 16 + // TODO add waiting for response + //AvatarUploadStatusItem { + //id: statusResponse + //text: "Waiting for completion" + //} + AvatarUploadStatusItem { + id: statusInventory + anchors.top: statusUploading.bottom + text: "Waiting for inventory" - anchors.horizontalCenter: parent.horizontalCenter - - color: "white" + state: root.uploader.state == 3 ? "running" : (root.uploader.state > 3 ? "success" : (root.uploader.error ? "fail" : "")) } } - HifiControls.Button { - width: implicitWidth - height: implicitHeight - - anchors.bottom: parent.bottom - anchors.right: parent.right - - text: "View in Inventory" - - color: hifi.buttons.blue - colorScheme: root.colorScheme - onClicked: function() { - console.log("Opening in inventory"); - - AvatarPackagerCore.currentAvatarProject.openInInventory(); - } - } } Column { @@ -165,8 +132,6 @@ Item { visible: false Text { - id: uploadStatus - text: "Uploading" color: "white" diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 1559f359a7..0a14ea9af4 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -39,15 +39,25 @@ MarketplaceItemUploader::MarketplaceItemUploader(QString title, } void MarketplaceItemUploader::setState(State newState) { + Q_ASSERT(newState != _state); qDebug() << "Setting uploader state to: " << newState; _state = newState; emit stateChanged(newState); if (newState == State::Complete) { emit completed(); + emit finished(); } } +void MarketplaceItemUploader::setError(Error error) { + Q_ASSERT(_error == Error::None); + + _error = error; + emit errorChanged(error); + emit finished(); +} + void MarketplaceItemUploader::send() { doGetCategories(); } @@ -105,14 +115,12 @@ void MarketplaceItemUploader::doGetCategories() { qDebug() << "Done " << success << id; if (!success) { qWarning() << "Failed to find marketplace category id"; - _error = Error::Unknown; - setState(State::Complete); + setError(Error::Unknown); } else { doUploadAvatar(); } } else { - _error = Error::Unknown; - setState(State::Complete); + setError(Error::Unknown); } }); } @@ -132,8 +140,7 @@ void MarketplaceItemUploader::doUploadAvatar() { QuaZipFile zipFile{ &zip }; if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileInfo.fileName()))) { qWarning() << "Could not open zip file:" << zipFile.getZipError(); - _error = Error::Unknown; - setState(State::Complete); + setError(Error::Unknown); return; } QFile file{ filePath }; @@ -204,14 +211,13 @@ void MarketplaceItemUploader::doUploadAvatar() { if (status == "success") { _marketplaceID = QUuid::fromString(doc["data"].toObject()["marketplace_id"].toString()); _itemVersion = doc["data"].toObject()["version"].toDouble(); + setState(State::WaitingForInventory); doWaitForInventory(); } else { - _error = Error::Unknown; - setState(State::Complete); + setError(Error::Unknown); } } else { - _error = Error::Unknown; - setState(State::Complete); + setError(Error::Unknown); } }); @@ -282,8 +288,7 @@ void MarketplaceItemUploader::doWaitForInventory() { } else { qDebug() << "Failed to find item in inventory"; if (_numRequestsForInventory > 8) { - _error = Error::Unknown; - setState(State::Complete); + setError(Error::Unknown); } else { QTimer::singleShot(5000, [this]() { doWaitForInventory(); }); } diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index 9cfd531aca..1b1589af96 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -23,7 +23,7 @@ class MarketplaceItemUploader : public QObject { Q_PROPERTY(bool complete READ getComplete NOTIFY stateChanged) Q_PROPERTY(State state READ getState NOTIFY stateChanged) - Q_PROPERTY(Error error READ getError) + Q_PROPERTY(Error error READ getError NOTIFY errorChanged) Q_PROPERTY(QString responseData READ getResponseData) public: enum class Error @@ -51,11 +51,14 @@ public: 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; } @@ -63,7 +66,12 @@ public: 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 finished(); private: void doGetCategories(); From 577b3cbc9058962a8867ee7b6942ed6feb542fbe Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Wed, 26 Dec 2018 09:06:25 -0800 Subject: [PATCH 16/72] Implemented online help. --- tools/nitpick/src/ui/HelpWindow.cpp | 14 --------- tools/nitpick/src/ui/HelpWindow.h | 22 -------------- tools/nitpick/src/ui/HelpWindow.ui | 46 ----------------------------- tools/nitpick/src/ui/Nitpick.cpp | 9 +++--- tools/nitpick/src/ui/Nitpick.h | 3 -- tools/nitpick/src/ui/Nitpick.ui | 4 +-- 6 files changed, 6 insertions(+), 92 deletions(-) delete mode 100644 tools/nitpick/src/ui/HelpWindow.cpp delete mode 100644 tools/nitpick/src/ui/HelpWindow.h delete mode 100644 tools/nitpick/src/ui/HelpWindow.ui diff --git a/tools/nitpick/src/ui/HelpWindow.cpp b/tools/nitpick/src/ui/HelpWindow.cpp deleted file mode 100644 index 21c5d9d375..0000000000 --- a/tools/nitpick/src/ui/HelpWindow.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// -// HelpWindow.cpp -// -// Created by Nissim Hadar on 8 Aug 2017. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "HelpWindow.h" - -HelpWindow::HelpWindow(QWidget *parent) { - setupUi(this); -} diff --git a/tools/nitpick/src/ui/HelpWindow.h b/tools/nitpick/src/ui/HelpWindow.h deleted file mode 100644 index 5ce91b360d..0000000000 --- a/tools/nitpick/src/ui/HelpWindow.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// HelpWindow.h -// -// Created by Nissim Hadar on 8 Aug 2017. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#ifndef hifi_HelpWindow_h -#define hifi_HelpWindow_h - -#include "ui_HelpWindow.h" - -class HelpWindow : public QDialog, public Ui::HelpWindow { - Q_OBJECT - -public: - HelpWindow(QWidget* parent = Q_NULLPTR); -}; - -#endif \ No newline at end of file diff --git a/tools/nitpick/src/ui/HelpWindow.ui b/tools/nitpick/src/ui/HelpWindow.ui deleted file mode 100644 index 1ce6e8c321..0000000000 --- a/tools/nitpick/src/ui/HelpWindow.ui +++ /dev/null @@ -1,46 +0,0 @@ - - - HelpWindow - - - Qt::ApplicationModal - - - - 0 - 0 - 696 - 546 - - - - Nitpick Help - - - - - 50 - 50 - 581 - 381 - - - - - - - 300 - 460 - 93 - 28 - - - - Close - - - - - - - diff --git a/tools/nitpick/src/ui/Nitpick.cpp b/tools/nitpick/src/ui/Nitpick.cpp index cdd2ff89d9..38124e1f39 100644 --- a/tools/nitpick/src/ui/Nitpick.cpp +++ b/tools/nitpick/src/ui/Nitpick.cpp @@ -15,6 +15,8 @@ #include #endif +#include + Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.setupUi(this); @@ -36,10 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.statusLabel->setText(""); _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v1.2"); - - // Coming soon to a nitpick near you... - //// _helpWindow.textBrowser->setText() + setWindowTitle("Nitpick - v1.3"); } Nitpick::~Nitpick() { @@ -287,7 +286,7 @@ void Nitpick::about() { } void Nitpick::content() { - _helpWindow.show(); + QDesktopServices::openUrl(QUrl("https://github.com/highfidelity/hifi/blob/master/tools/nitpick/README.md")); } void Nitpick::setUserText(const QString& user) { diff --git a/tools/nitpick/src/ui/Nitpick.h b/tools/nitpick/src/ui/Nitpick.h index 21b917654b..08e41e0a90 100644 --- a/tools/nitpick/src/ui/Nitpick.h +++ b/tools/nitpick/src/ui/Nitpick.h @@ -18,7 +18,6 @@ #include "../Downloader.h" #include "../Test.h" -#include "HelpWindow.h" #include "../TestRunner.h" #include "../AWSInterface.h" @@ -116,8 +115,6 @@ private: bool _isRunningFromCommandline{ false }; - HelpWindow _helpWindow; - void* _caller; }; diff --git a/tools/nitpick/src/ui/Nitpick.ui b/tools/nitpick/src/ui/Nitpick.ui index 5e20e75553..78f7dcf2bf 100644 --- a/tools/nitpick/src/ui/Nitpick.ui +++ b/tools/nitpick/src/ui/Nitpick.ui @@ -803,7 +803,7 @@ 0 0 720 - 22 + 21 @@ -843,7 +843,7 @@ - Content + Online readme From 70a2412a71f31508cb330452676c2f8f7eef57c9 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Wed, 26 Dec 2018 12:23:26 -0800 Subject: [PATCH 17/72] Windows version now deletes entities before starting tests. --- tools/nitpick/src/TestRunner.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestRunner.cpp b/tools/nitpick/src/TestRunner.cpp index 9b99e114a7..bbdd36a12a 100644 --- a/tools/nitpick/src/TestRunner.cpp +++ b/tools/nitpick/src/TestRunner.cpp @@ -469,12 +469,30 @@ void TestRunner::runInterfaceWithTestScript() { url = "hifi://localhost"; } + QString deleteScript = + QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/utils/deleteNearbyEntities.js"; + QString testScript = QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js"; QString commandLine; #ifdef Q_OS_WIN - QString exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; + QString exeFile; + // First, run script to delete any entities in test area + // Note that this will run to completion before continuing + exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; + commandLine = "start /wait \"\" " + exeFile + + " --url " + url + + " --no-updater" + + " --no-login-suggestion" + " --testScript " + deleteScript + " quitWhenFinished" + + " --testResultsLocation " + _snapshotFolder; + + + system(commandLine.toStdString().c_str()); + + // Now run the test suite + exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; commandLine = exeFile + " --url " + url + " --no-updater" + From 1da179dc04973c7c5b3d7d8055d97e55ad46230e Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 27 Dec 2018 00:13:45 -0800 Subject: [PATCH 18/72] Update avatar packager styling --- .../resources/images/loader-snake-128.png | Bin 0 -> 463 bytes .../resources/qml/hifi/AvatarPackager.qml | 1 + .../qml/hifi/avatarPackager/AvatarProject.qml | 159 +++++++++++++++--- .../avatarPackager/AvatarProjectUpload.qml | 94 +++++++---- .../avatarPackager/AvatarUploadStatusItem.qml | 96 +++++++++++ .../qml/hifi/avatarPackager/LoadingCircle.qml | 20 +++ .../qml/hifi/avatarapp/MessageBox.qml | 16 ++ interface/src/avatar/AvatarProject.h | 3 +- .../src/avatar/MarketplaceItemUploader.cpp | 23 ++- .../src/avatar/MarketplaceItemUploader.h | 6 +- libraries/fbx/src/FST.cpp | 5 + libraries/fbx/src/FST.h | 6 +- 12 files changed, 368 insertions(+), 61 deletions(-) create mode 100644 interface/resources/images/loader-snake-128.png create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml diff --git a/interface/resources/images/loader-snake-128.png b/interface/resources/images/loader-snake-128.png new file mode 100644 index 0000000000000000000000000000000000000000..b8ee57766488aed3627111d0dd6a1f0bb2a7dbaf GIT binary patch literal 463 zcmV;=0WkiFP)w!o@;$>0gGA+Fq)u?0a-$u1CxnfTGIa^5mSWSEh$GnaXjy&^`e zPrPbH%)n^08iN;ay=%^xyPw!L}2gYuE!%c-{nAkChx8*Uz>Z2ojhOq=5uHLPX`@xu3{>6{a~(gmcGG% zq9U~swXRF%aQMzSpPDWV0wFHD7~H-YW8$pIiA3Vu_ynabg<_e%Q|SNz002ovPDHLk FV1jQ+#bE#d literal 0 HcmV?d00001 diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 0dea524991..46fd98daab 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -48,6 +48,7 @@ Windows.ScrollingWindow { id: popup anchors.fill: parent visible: false + closeOnClickOutside: true } Column { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 81afacf28c..d4a67f31fb 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -19,35 +19,130 @@ Item { property int colorScheme; property var uploader: undefined; + property bool hasSuccessfullyUploaded: true; + visible: false anchors.fill: parent anchors.margins: 10 property var footer: Item { - id: uploadFooter - anchors.fill: parent - anchors.rightMargin: 17 - HifiControls.Button { - id: uploadButton - enabled: Account.loggedIn - //width: parent.width - //anchors.bottom: parent.bottom - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - text: qsTr("Upload") - color: hifi.buttons.blue - colorScheme: root.colorScheme - width: 133 - height: 40 - onClicked: function() { - if (AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID()) { - showConfirmUploadPopup(uploadNew, uploadUpdate); - } else { + 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.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded + enabled: Account.loggedIn + //width: parent.width + //anchors.bottom: parent.bottom + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + text: qsTr("Upload") + color: hifi.buttons.blue + colorScheme: root.colorScheme + width: 133 + height: 40 + onClicked: function() { uploadNew(); } } + HifiControls.Button { + id: updateButton + + visible: 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: function() { + 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: function() { + 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 + } + RalewayRegular { + id: stepText + + size: 20 + + anchors.verticalCenter: parent.verticalCenter + anchors.left: runningImage.right + anchors.leftMargin: 16 + + text: "Adding item to Inventory" + color: "white" + } } } @@ -66,7 +161,10 @@ Item { root.uploader.uploadProgress.connect(function(uploaded, total) { console.log("Uploader progress: " + uploaded + " / " + total); }); - root.uploader.finished.connect(function() { + root.uploader.completed.connect(function() { + root.hasSuccessfullyUploaded = true; + }); + root.uploader.finishedChanged.connect(function() { try { var response = JSON.parse(root.uploader.responseData); console.log("Uploader complete! " + response); @@ -121,6 +219,25 @@ Item { RalewayRegular { id: infoMessage + states: [ + State { + when: root.hasSuccessfullyUploaded + name: "upload-success" + PropertyChanges { + target: infoMessage + text: "Your avatar has been uploaded to our servers. You can modify the project files and update it again to make changes on the uploaded avatar." + } + }, + 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 @@ -132,7 +249,7 @@ Item { wrapMode: Text.Wrap - 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." + text: "You can upload your files to our servers to always access them, and to make your avatar visible to other users." } HifiControls.Button { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index 6e011b1ec7..8b80df3d95 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -8,28 +8,35 @@ 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: !!root.uploader visible: false anchors.fill: parent Timer { id: backToProjectTimer - interval: 2000 + interval: 5000 running: false repeat: false - onTriggered: avatarPackager.state = "project" + onTriggered: { + if (avatarPackager.state =="project-upload") { + avatarPackager.state = "project" + } + } } + function stateChangedCallback(newState) { + if (newState >= 4) { + root.uploader.stateChanged.disconnect(stateChangedCallback) + backToProjectTimer.start(); + } + } onVisibleChanged: { - console.log("Visibility changed"); if (visible) { - root.uploader.finished.connect(function() { - console.log("Did complete"); + root.uploader.stateChanged.connect(stateChangedCallback); + root.uploader.finishedChanged.connect(function() { backToProjectTimer.start(); }); } @@ -46,48 +53,62 @@ Item { id: statusItem width: parent.width - height: 128 + height: 192 - AnimatedImage { + states: [ + State { + name: "success" + when: !!root.uploader && 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 && root.uploader.finished && root.uploader.error !== 0 + PropertyChanges { target: uploadSpinner; visible: false } + PropertyChanges { target: errorIcon; visible: true } + PropertyChanges { target: successIcon; visible: false } + } + ] + + LoadingCircle { id: uploadSpinner - visible: !!root.uploader && !root.uploader.complete + visible: true anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } - - source: "../../../icons/loader-snake-64-w.gif" - playing: true - z: 10000 } HiFiGlyphs { id: errorIcon - visible: !!root.uploader && root.uploader.complete && root.uploader.error !== 0 + + visible: false anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } - size: 128 + size: 164 text: "w" - color: "red" + color: "#EA4C5F" } HiFiGlyphs { id: successIcon - visible: !!root.uploader && root.uploader.complete && root.uploader.error === 0 + visible: false anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } - size: 128 + size: 164 text: "\ue01a" color: "#1FC6A6" } @@ -96,34 +117,49 @@ Item { id: statusRows anchors.top: statusItem.bottom + anchors.left: parent.left + anchors.leftMargin: 12 AvatarUploadStatusItem { id: statusCategories + uploader: root.uploader text: "Retreiving categories" - state: root.uploader.state == 1 ? "running" : (root.uploader.state > 1 ? "success" : (root.uploader.error ? "fail" : "")) + uploaderState: 1 } AvatarUploadStatusItem { id: statusUploading + uploader: root.uploader anchors.top: statusCategories.bottom text: "Uploading data" - state: root.uploader.state == 2 ? "running" : (root.uploader.state > 2 ? "success" : (root.uploader.error ? "fail" : "")) + uploaderState: 2 } - // TODO add waiting for response - //AvatarUploadStatusItem { - //id: statusResponse - //text: "Waiting for completion" - //} AvatarUploadStatusItem { - id: statusInventory + id: statusResponse + uploader: root.uploader anchors.top: statusUploading.bottom - text: "Waiting for inventory" + text: "Waiting for response" - state: root.uploader.state == 3 ? "running" : (root.uploader.state > 3 ? "success" : (root.uploader.error ? "fail" : "")) + uploaderState: 3 } } + RalewayRegular { + visible: root.uploader.error + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + + size: 28 + wrapMode: Text.Wrap + color: "white" + text: "We couldn't upload your avatar at this time. Please try again later." + } } Column { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml new file mode 100644 index 0000000000..d93afbd4e8 --- /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; + + state: root.uploader.state > uploaderState + ? "success" + : (root.uploader.error !== 0 ? "fail" : (root.uploader.state === uploaderState ? "running" : "")) + + states: [ + State { + name: "running" + PropertyChanges { target: stepText; color: "white" } + PropertyChanges { target: runningImage; visible: true; playing: true } + }, + State { + name: "fail" + PropertyChanges { target: stepText; color: "#EA4C5F" } + PropertyChanges { target: failGlyph; visible: true } + }, + State { + name: "success" + PropertyChanges { target: stepText; color: "white" } + PropertyChanges { target: successGlyph; visible: true } + } + ] + + Item { + id: statusItem + + width: 48 + height: parent.height + + AnimatedImage { + id: runningImage + + visible: false + + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + + width: 32 + height: 32 + + source: "../../../icons/loader-snake-64-w.gif" + playing: false + } + HiFiGlyphs { + id: successGlyph + + visible: false + + width: implicitWidth + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + + size: 48 + text: "\ue01a" + color: "#1FC6A6" + } + 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" + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml new file mode 100644 index 0000000000..f6ba81a96f --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml @@ -0,0 +1,20 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.0 + +Image { + id: root + + width: 128 + height: 128 + + source: "../../../images/loader-snake-128.png" + + RotationAnimation on rotation { + duration: 2000 + loops: Animation.Infinite + from: 0 + to: 360 + } +} \ No newline at end of file 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/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index d5cd7762a1..6da9f710cc 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -34,7 +34,7 @@ class AvatarProject : public QObject { Q_PROPERTY(QString projectFolderPath READ getProjectPath) Q_PROPERTY(QString projectFSTPath READ getFSTPath) Q_PROPERTY(QString projectFBXPath READ getFBXPath) - Q_PROPERTY(QString name READ getProjectName) + Q_PROPERTY(QString name READ getProjectName NOTIFY nameChanged) public: Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); @@ -57,6 +57,7 @@ public: } signals: + void nameChanged(); void projectFilesChanged(); private: diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 0a14ea9af4..31dcf8e9a0 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -39,6 +39,8 @@ MarketplaceItemUploader::MarketplaceItemUploader(QString title, } void MarketplaceItemUploader::setState(State newState) { + Q_ASSERT(_state != State::Complete); + Q_ASSERT(_error == Error::None); Q_ASSERT(newState != _state); qDebug() << "Setting uploader state to: " << newState; @@ -46,16 +48,17 @@ void MarketplaceItemUploader::setState(State newState) { emit stateChanged(newState); if (newState == State::Complete) { emit completed(); - emit finished(); + emit finishedChanged(); } } void MarketplaceItemUploader::setError(Error error) { + Q_ASSERT(_state != State::Complete); Q_ASSERT(_error == Error::None); _error = error; emit errorChanged(error); - emit finished(); + emit finishedChanged(); } void MarketplaceItemUploader::send() { @@ -179,7 +182,6 @@ void MarketplaceItemUploader::doUploadAvatar() { { "description", _description }, { "root_file_key", _rootFilename }, { "category_ids", QJsonArray({ 5 }) }, - //{ "attributions", QJsonArray({ QJsonObject{ { "name", "" }, { "link", "" } } }) }, { "license", 0 }, { "files", QString::fromLatin1(_fileData.toBase64()) } } } }; request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); @@ -197,14 +199,21 @@ void MarketplaceItemUploader::doUploadAvatar() { reply = networkAccessManager.put(request, doc.toJson()); } - connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + 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(); + _responseData = reply->readAll(); + qWarning() << "Finished request " << _responseData; + auto error = reply->error(); if (error == QNetworkReply::NoError) { - _responseData = reply->readAll(); - qWarning() << "Finished request " << _responseData; auto doc = QJsonDocument::fromJson(_responseData.toLatin1()); auto status = doc.object()["status"].toString(); diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index 1b1589af96..4b8b675255 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -21,6 +21,8 @@ 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) @@ -38,6 +40,7 @@ public: Idle, GettingCategories, UploadingAvatar, + WaitingForUploadResponse, WaitingForInventory, Complete }; @@ -62,6 +65,7 @@ public: 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); @@ -71,7 +75,7 @@ signals: void errorChanged(Error error); // Triggered when the upload has finished, either succesfully completing, or stopping with an error - void finished(); + void finishedChanged(); private: void doGetCategories(); diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index af00428a51..f0e444ba33 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -175,3 +175,8 @@ bool FST::write() { 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 index 813d4f3bc5..6fd654987e 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -24,6 +24,7 @@ class FST : public QObject { 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(const QString& fstPath, QVariantHash data); @@ -37,9 +38,9 @@ public: QString getModelPath() const { return _modelPath; } void setModelPath(const QString& modelPath); - Q_INVOKABLE bool hasMarketplaceID() const { return !_marketplaceID.isNull(); } + Q_INVOKABLE bool getHasMarketplaceID() const { return !_marketplaceID.isNull(); } QUuid getMarketplaceID() const { return _marketplaceID; } - void setMarketplaceID(QUuid marketplaceID) { _marketplaceID = marketplaceID; } + void setMarketplaceID(QUuid marketplaceID); QStringList getScriptPaths() const { return _scriptPaths; } void setScriptPaths(QStringList scriptPaths) { _scriptPaths = scriptPaths; } @@ -53,6 +54,7 @@ public: signals: void nameChanged(const QString& name); void modelPathChanged(const QString& modelPath); + void marketplaceIDChanged(); private: QString _fstPath; From 2f32458f72311a694e479325c2dfe4a2cdaea4c4 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 27 Dec 2018 19:52:56 +0100 Subject: [PATCH 19/72] recent projects --- .../resources/qml/hifi/AvatarPackager.qml | 29 ++++++- .../avatarPackager/AvatarPackagerHeader.qml | 1 + .../hifi/avatarPackager/AvatarProjectCard.qml | 85 +++++++++++++++++++ .../avatarPackager/AvatarProjectUpload.qml | 2 +- .../avatarPackager/CreateAvatarProject.qml | 50 ++++++----- .../avatarPackager/ProjectInputControl.qml | 1 + interface/src/avatar/AvatarPackager.cpp | 31 +++++++ interface/src/avatar/AvatarPackager.h | 68 +++++++++++++++ interface/src/avatar/AvatarProject.cpp | 11 +-- interface/src/avatar/AvatarProject.h | 15 ++-- .../src/avatar/MarketplaceItemUploader.cpp | 4 +- 11 files changed, 259 insertions(+), 38 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 46fd98daab..686bdd28da 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -33,7 +33,7 @@ Windows.ScrollingWindow { id: modalOverlay anchors.fill: parent z: 20 - color: "#aa031b33" + color: "#a15d5d5d" visible: false // This mouse area captures the cursor events while the modalOverlay is active @@ -70,7 +70,7 @@ Windows.ScrollingWindow { }, State { name: AvatarPackagerState.project - PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; canRename: true } PropertyChanges { target: avatarProject; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer } }, @@ -136,6 +136,7 @@ Windows.ScrollingWindow { text: qsTr("New Project") colorScheme: root.colorScheme onClicked: { + createAvatarProject.clearInputs(); avatarPackager.state = AvatarPackagerState.createProject; } } @@ -173,7 +174,10 @@ Windows.ScrollingWindow { } } } + + Flow { + visible: AvatarPackagerCore.recentProjects.length === 0 anchors { fill: parent topMargin: 18 @@ -190,6 +194,27 @@ Windows.ScrollingWindow { color: "white" text: qsTr("To learn more about using this tool, visit our docs") } + + + } + + Column { + visible: AvatarPackagerCore.recentProjects.length > 0 + anchors { + fill: parent + topMargin: 18 + leftMargin: 16 + rightMargin: 16 + } + spacing: 10 + + Repeater { + model: AvatarPackagerCore.recentProjects + AvatarProjectCard { + title: modelData.name + path: modelData.path + } + } } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 84096e352c..663d4d0f3a 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -13,6 +13,7 @@ Rectangle { property alias title: title.text property alias faqEnabled: faq.visible property alias backButtonEnabled: back.visible + property bool canRename: false; signal backButtonClicked RalewaySemiBold { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml new file mode 100644 index 0000000000..be1363850e --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -0,0 +1,85 @@ +import QtQuick 2.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" + + 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 + anchors { + top: parent.top + topMargin: 13 + left: parent.left + leftMargin: 16 + } + text: "" + size: 16 + } + + RalewayRegular { + id: path + anchors { + top: title.bottom + left: parent.left + leftMargin: 32 + } + text: "<path missing>" + size: 20 + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + AvatarPackagerCore.openAvatarProject(path.text); + avatarPackager.state = "project"; + } + } + } +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index 8b80df3d95..e71d8a4f2f 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -178,4 +178,4 @@ Item { } } -} \ No newline at end of file +} diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml index 41d33e6058..a5d335feba 100644 --- a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -22,9 +22,10 @@ Item { height: 30 width: 133 text: qsTr("Create") + enabled: false onClicked: { if (!AvatarPackagerCore.createAvatarProject(projectLocation.text, name.text, avatarModel.text, textureFolder.text)) { - Window.alert('Failed to create project') + Window.alert('Failed to create project'); return; } avatarPackager.state = AvatarPackagerState.project; @@ -37,21 +38,39 @@ Item { height: parent.height width: parent.width + function clearInputs() { + name.text = projectLocation.text = avatarModel.text = textureFolder.text = ""; + } - property var errorMessages: QtObject { - readonly property string fileExists: "A folder with that name already exists at that location. Please choose a different project name or location." + 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"; + color: "#EA4C5F" wrapMode: Text.WordWrap size: 20 anchors { - top: createAvatarColumns.bottom - bottom: parent.bottom left: parent.left right: parent.right } @@ -59,6 +78,7 @@ Item { Column { id: createAvatarColumns + anchors.top: errorMessage.visible ? errorMessage.bottom : parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: 10 @@ -71,6 +91,7 @@ Item { id: name label: "Name" colorScheme: root.colorScheme + onTextChanged: checkErrors() } ProjectInputControl { @@ -81,9 +102,7 @@ Item { browseFolder: true browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath browseTitle: "Project Location" - onTextChanged: { - //TODO: valid folder? Does project with name exist here already? - } + onTextChanged: checkErrors() } ProjectInputControl { @@ -95,25 +114,18 @@ Item { browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath browseFilter: "Avatar Model File (*.fbx)" browseTitle: "Open Avatar Model (.fbx)" - onTextChanged: { - if (avatarModel.text !== "") { - textureFolder.browseDir = fileDialogHelper.pathToUrl(avatarModel.text.split('/')[0]); - } - } + onTextChanged: checkErrors() } ProjectInputControl { id: textureFolder - label: "Specify Texture Folder" + label: "Specify Texture Folder <i> - optional</i>" colorScheme: root.colorScheme browseEnabled: true browseFolder: true browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath browseTitle: "Texture Folder" - onTextChanged: { - //TODO: valid folder? - - } + onTextChanged: checkErrors() } } diff --git a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml index 664acd6f22..2ac4a37d02 100644 --- a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml +++ b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml @@ -22,6 +22,7 @@ Column { property string browseTitle: "Open file" property string browseDir: "" property alias text: input.text + property alias error: input.error property int colorScheme diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index c7f15d616c..d8aadeb4e0 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -31,6 +31,8 @@ AvatarPackager::AvatarPackager() { qRegisterMetaType<AvatarProject*>(); }); + recentProjectsFromVariantList(_recentProjectsSetting.get()); + QDir defaultProjectsDir(AvatarProject::getDefaultProjectsPath()); defaultProjectsDir.mkpath("."); } @@ -50,17 +52,46 @@ AvatarProject* AvatarPackager::openAvatarProject(const QString& avatarProjectFST _currentAvatarProject->deleteLater(); } _currentAvatarProject = AvatarProject::openAvatarProject(avatarProjectFSTPath); + if (_currentAvatarProject) { + addRecentProject(avatarProjectFSTPath, _currentAvatarProject->getProjectName()); + } qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); emit avatarProjectChanged(); return _currentAvatarProject; } +void AvatarPackager::addRecentProject(QString fstPath, QString projectName) { + const int MAX_RECENT_PROJECTS = 5; + auto removeProjects = QVector<RecentAvatarProject>(); + for (auto project : _recentProjects) { + if (project.getProjectFSTPath() == fstPath) { + removeProjects.append(project); + } + } + for (const auto removeProject : removeProjects) { + _recentProjects.removeOne(removeProject); + } + + RecentAvatarProject newRecentProject = RecentAvatarProject(projectName, fstPath); + _recentProjects.prepend(newRecentProject); + + while (_recentProjects.size() > MAX_RECENT_PROJECTS) { + _recentProjects.pop_back(); + } + + _recentProjectsSetting.set(recentProjectsToVariantList()); + emit recentProjectsChanged(); +} + AvatarProject* AvatarPackager::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder) { if (_currentAvatarProject) { _currentAvatarProject->deleteLater(); } _currentAvatarProject = AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder); + if (_currentAvatarProject) { + addRecentProject(_currentAvatarProject->getFSTPath(), _currentAvatarProject->getProjectName()); + } qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); emit avatarProjectChanged(); diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index 13a42a73d0..8cf641dbaa 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -19,27 +19,95 @@ #include "FileDialogHelper.h" #include "avatar/AvatarProject.h" +#include "SettingHandle.h" + +class RecentAvatarProject { +public: + RecentAvatarProject() { + + } + + 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; } + + bool operator==(const RecentAvatarProject& other) const { + return _projectName == other._projectName && _projectFSTPath == other._projectFSTPath; + } + +private: + QString _projectName; + QString _projectFSTPath; + +}; + +inline QDebug operator<<(QDebug debug, const RecentAvatarProject& recentAvatarProject) { + debug << "[recentAvatarProject:" << recentAvatarProject.getProjectFSTPath() << "]"; + return debug; +} + +Q_DECLARE_METATYPE(RecentAvatarProject); + +Q_DECLARE_METATYPE(QVector<RecentAvatarProject>); 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 AvatarProject* createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder); Q_INVOKABLE AvatarProject* openAvatarProject(const QString& avatarProjectFSTPath); + Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) { 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() { return recentProjectsToVariantList(); } + + void addRecentProject(QString fstPath, QString projectName); AvatarProject* _currentAvatarProject{ nullptr }; + QVector<RecentAvatarProject> _recentProjects; + QVariantList recentProjectsToVariantList() { + QVariantList result; + for (const auto& project : _recentProjects) { + QVariantMap projectVariant; + projectVariant.insert("name", project.getProjectName()); + projectVariant.insert("path", project.getProjectFSTPath()); + result.append(projectVariant); + } + + return result; + } + + void 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())); + } + } + + + Setting::Handle<QVariantList> _recentProjectsSetting{ "io.highfidelity.avatarPackager.recentProjects", QVariantList() }; }; #endif // hifi_AvatarPackager_h diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 2a2ec7c1cb..038ded64d8 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -15,13 +15,11 @@ #include <QFile> #include <QFileInfo> -#include <QDebug> #include <QQmlEngine> #include <QTimer> #include "FBXSerializer.h" #include <ui/TabletScriptingInterface.h> -#include <graphics/TextureMap.h> #include "scripting/HMDScriptingInterface.h" AvatarProject* AvatarProject::openAvatarProject(const QString& path) { @@ -38,7 +36,7 @@ AvatarProject* AvatarProject::openAvatarProject(const QString& path) { } AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder) { - if (!isValidNewProjectName(avatarProjectName)) { + if (!isValidNewProjectName(projectsFolder, avatarProjectName)) { return nullptr; } QDir projectDir(projectsFolder + "/" + avatarProjectName); @@ -135,8 +133,11 @@ QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) { return result; } -bool AvatarProject::isValidNewProjectName(const QString& projectName) { - QDir dir(getDefaultProjectsPath() + "/" + projectName); +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(); } diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 6da9f710cc..506dd7d40b 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -17,12 +17,9 @@ #include "ProjectFile.h" #include "FST.h" -#include <QDir> #include <QObject> #include <QDir> -#include <QFileInfo> #include <QVariantHash> -#include <QUuid> #include <QStandardPaths> class AvatarProject : public QObject { @@ -41,6 +38,11 @@ public: Q_INVOKABLE void openInInventory(); Q_INVOKABLE QStringList getProjectFiles() const; + Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } + Q_INVOKABLE QString getProjectPath() const { return _projectPath; } + Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); } + Q_INVOKABLE QString getFBXPath() const { return _fst->getModelPath(); } + /** * returns the AvatarProject or a nullptr on failure. */ @@ -50,7 +52,7 @@ public: const QString& avatarModelPath, const QString& textureFolder); - static bool isValidNewProjectName(const QString& projectName); + static bool isValidNewProjectName(const QString& projectPath, const QString& projectName); static QString getDefaultProjectsPath() { return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/High Fidelity Projects"; @@ -66,11 +68,6 @@ private: ~AvatarProject() { _fst->deleteLater(); } - Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } - Q_INVOKABLE QString getProjectPath() const { return _projectPath; } - Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); } - Q_INVOKABLE QString getFBXPath() const { return _fst->getModelPath(); } - FST* getFST() { return _fst; } void refreshProjectFiles(); diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 31dcf8e9a0..ebb3ccdf53 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -15,8 +15,8 @@ #include <DependencyManager.h> #include <QBuffer> -#include <quazip5\quazip.h> -#include <quazip5\quazipfile.h> +#include <quazip5/quazip.h> +#include <quazip5/quazipfile.h> #include <qtimer.h> #include <QFile> From af65e59ba3c52c3454584e388e2f31f0228eacd2 Mon Sep 17 00:00:00 2001 From: howard-stearns <howard@highfidelity.io> Date: Thu, 27 Dec 2018 14:34:59 -0500 Subject: [PATCH 20/72] Recognize data: URLs in Web entities. --- libraries/entities-renderer/src/RenderableWebEntityItem.cpp | 1 + libraries/networking/src/NetworkingConstants.h | 1 + 2 files changed, 2 insertions(+) diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 476372160e..2942de0ba4 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -55,6 +55,7 @@ WebEntityRenderer::ContentType WebEntityRenderer::getContentType(const QString& const QUrl url(urlString); auto scheme = url.scheme(); if (scheme == HIFI_URL_SCHEME_ABOUT || scheme == HIFI_URL_SCHEME_HTTP || scheme == HIFI_URL_SCHEME_HTTPS || + scheme == URL_SCHEME_DATA || urlString.toLower().endsWith(".htm") || urlString.toLower().endsWith(".html")) { return ContentType::HtmlContent; } diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index 302e0efa02..117a41c976 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -33,6 +33,7 @@ namespace NetworkingConstants { const QString HIFI_URL_SCHEME_ABOUT = "about"; const QString URL_SCHEME_HIFI = "hifi"; const QString URL_SCHEME_HIFIAPP = "hifiapp"; +const QString URL_SCHEME_DATA = "data"; const QString URL_SCHEME_QRC = "qrc"; const QString HIFI_URL_SCHEME_FILE = "file"; const QString HIFI_URL_SCHEME_HTTP = "http"; From 882975f01ca90aa24ba8012b34b357b41ee61cbf Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 12:43:43 -0800 Subject: [PATCH 21/72] Update icons in avatar packager --- .../resources/icons/checkmark-stroke.svg | 4 +++ .../resources/icons/loader-snake-256-wf.gif | Bin 0 -> 27304 bytes .../resources/icons/loader-snake-256.gif | Bin 0 -> 26070 bytes .../qml/hifi/avatarPackager/AvatarProject.qml | 8 ++++-- .../hifi/avatarPackager/AvatarProjectCard.qml | 1 + .../avatarPackager/AvatarProjectUpload.qml | 25 +++++++++++------- .../avatarPackager/AvatarUploadStatusItem.qml | 15 +++++------ .../qml/hifi/avatarPackager/LoadingCircle.qml | 14 ++++------ 8 files changed, 37 insertions(+), 30 deletions(-) create mode 100644 interface/resources/icons/checkmark-stroke.svg create mode 100644 interface/resources/icons/loader-snake-256-wf.gif create mode 100644 interface/resources/icons/loader-snake-256.gif 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 @@ +<svg width="149" height="150" viewBox="0 0 149 150" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M74.3055 0C115.543 0 149 33.5916 149 74.6047C149 116.008 115.543 149.6 74.3055 149.6C33.4569 149.6 0 116.008 0 74.6047C0 33.5916 33.4569 0 74.3055 0ZM74.3055 139.054C109.708 139.054 138.496 110.149 138.496 74.6047C138.496 39.4507 109.708 10.5462 74.3055 10.5462C39.2924 10.5462 10.5039 39.4507 10.5039 74.6047C10.5039 110.149 39.2924 139.054 74.3055 139.054Z" fill="#1FC6A6"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M65.3575 89.8376L106.595 43.3562C110.874 38.2784 119.044 45.3092 114.376 50.387L70.0259 100.384C68.0807 102.727 64.9684 102.727 63.0233 101.165L35.0128 78.9008C29.9554 74.6042 36.569 66.4016 41.6264 70.6982L65.3575 89.8376Z" fill="#1FC6A6"/> +</svg> 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 0000000000000000000000000000000000000000..c0d5eec1ef6b83e3994bdaa1ee6e033e6e32f78d GIT binary patch literal 27304 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 literal 0 HcmV?d00001 diff --git a/interface/resources/icons/loader-snake-256.gif b/interface/resources/icons/loader-snake-256.gif new file mode 100644 index 0000000000000000000000000000000000000000..ebcbf54bd7759edfde7cab91d0a3e1be78c8390e GIT binary patch literal 26070 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 aJ^ES?TC8C)Fcoo0^4Grm_&0EHWd8?&-`BDL literal 0 HcmV?d00001 diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index d4a67f31fb..babe3cda84 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -25,6 +25,8 @@ Item { anchors.fill: parent anchors.margins: 10 + function reset() { hasSuccessfullyUploaded = false } + property var footer: Item { anchors.fill: parent @@ -41,10 +43,10 @@ Item { visible: !AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded enabled: Account.loggedIn - //width: parent.width - //anchors.bottom: parent.bottom + anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right + text: qsTr("Upload") color: hifi.buttons.blue colorScheme: root.colorScheme @@ -130,6 +132,8 @@ Item { width: 28 height: 28 + + white: true } RalewayRegular { id: stepText diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml index be1363850e..e31115efe5 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -78,6 +78,7 @@ Item { hoverEnabled: true onClicked: { AvatarPackagerCore.openAvatarProject(path.text); + avatarProject.reset(); avatarPackager.state = "project"; } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index e71d8a4f2f..f02dc5a218 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -72,7 +72,7 @@ Item { } ] - LoadingCircle { + AnimatedImage { id: uploadSpinner visible: true @@ -81,6 +81,12 @@ Item { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } + + width: 164 + height: 164 + + source: "../../../icons/loader-snake-256.gif" + playing: true } HiFiGlyphs { @@ -94,23 +100,22 @@ Item { } size: 164 - text: "w" + text: "+" color: "#EA4C5F" } - HiFiGlyphs { + Image { id: successIcon visible: false - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter - size: 164 - text: "\ue01a" - color: "#1FC6A6" + width: 148 + height: 148 + + source: "../../../icons/checkmark-stroke.svg" } } Item { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml index d93afbd4e8..ca01e453e9 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml @@ -42,7 +42,7 @@ Item { width: 48 height: parent.height - AnimatedImage { + LoadingCircle { id: runningImage visible: false @@ -52,22 +52,19 @@ Item { width: 32 height: 32 - - source: "../../../icons/loader-snake-64-w.gif" - playing: false } - HiFiGlyphs { + Image { id: successGlyph visible: false - width: implicitWidth anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - size: 48 - text: "\ue01a" - color: "#1FC6A6" + width: 30 + height: 30 + + source: "../../../icons/checkmark-stroke.svg" } HiFiGlyphs { id: failGlyph diff --git a/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml index f6ba81a96f..a1fac72ae4 100644 --- a/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml +++ b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml @@ -3,18 +3,14 @@ import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import QtGraphicalEffects 1.0 -Image { +AnimatedImage { id: root width: 128 height: 128 - source: "../../../images/loader-snake-128.png" + property bool white: false - RotationAnimation on rotation { - duration: 2000 - loops: Animation.Infinite - from: 0 - to: 360 - } -} \ No newline at end of file + source: white ? "../../../icons/loader-snake-256-wf.gif" : "../../../icons/loader-snake-256.gif" + playing: true +} From 1a38abe23014f924cd74989df6883b56f89563f2 Mon Sep 17 00:00:00 2001 From: Thijs Wenker <me@thoys.nl> Date: Thu, 27 Dec 2018 21:47:10 +0100 Subject: [PATCH 22/72] - add jointIndexes to new - attempt to fix OSX / linux build - ability to actually load a recent project (previously was only able to load the top recent project) --- interface/resources/qml/hifi/AvatarPackager.qml | 9 ++++++--- .../hifi/avatarPackager/AvatarProjectCard.qml | 5 +++-- interface/src/avatar/AvatarPackager.h | 16 ++++------------ interface/src/avatar/AvatarProject.h | 2 +- interface/src/avatar/MarketplaceItemUploader.cpp | 2 +- libraries/fbx/src/FST.cpp | 6 ++++++ 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 686bdd28da..79c130fadb 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -84,6 +84,11 @@ Windows.ScrollingWindow { property alias showModalOverlay: modalOverlay.visible + function openProject(path) { + AvatarPackagerCore.openAvatarProject(path); + avatarPackager.state = "project"; + } + AvatarPackagerHeader { id: avatarPackagerHeader onBackButtonClicked: { @@ -175,7 +180,6 @@ Windows.ScrollingWindow { } } - Flow { visible: AvatarPackagerCore.recentProjects.length === 0 anchors { @@ -194,8 +198,6 @@ Windows.ScrollingWindow { color: "white" text: qsTr("To learn more about using this tool, visit our docs") } - - } Column { @@ -213,6 +215,7 @@ Windows.ScrollingWindow { AvatarProjectCard { title: modelData.name path: modelData.path + onOpen: avatarPackager.openProject(modelData.path) } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml index be1363850e..7c29991792 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -20,6 +20,8 @@ Item { property color hoverBackgroundColor: "#E3E3E3" property color pressedBackgroundColor: "#6A6A6A" + signal open; + state: mouseArea.pressed ? "pressed" : (mouseArea.containsMouse ? "hover" : "normal") states: [ State { @@ -77,8 +79,7 @@ Item { anchors.fill: parent hoverEnabled: true onClicked: { - AvatarPackagerCore.openAvatarProject(path.text); - avatarPackager.state = "project"; + open(); } } } diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index 8cf641dbaa..57cbf046a7 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -23,9 +23,8 @@ class RecentAvatarProject { public: - RecentAvatarProject() { - - } + RecentAvatarProject() = default; + RecentAvatarProject(QString projectName, QString projectFSTPath) { _projectName = projectName; @@ -36,6 +35,8 @@ public: _projectFSTPath = other._projectFSTPath; } + ~RecentAvatarProject() = default; + QString getProjectName() const { return _projectName; } QString getProjectFSTPath() const { return _projectFSTPath; } @@ -50,15 +51,6 @@ private: }; -inline QDebug operator<<(QDebug debug, const RecentAvatarProject& recentAvatarProject) { - debug << "[recentAvatarProject:" << recentAvatarProject.getProjectFSTPath() << "]"; - return debug; -} - -Q_DECLARE_METATYPE(RecentAvatarProject); - -Q_DECLARE_METATYPE(QVector<RecentAvatarProject>); - class AvatarPackager : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 506dd7d40b..e950fd7379 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -24,7 +24,7 @@ class AvatarProject : public QObject { Q_OBJECT - Q_PROPERTY(FST* fst READ getFST) + Q_PROPERTY(FST* fst READ getFST CONSTANT) Q_PROPERTY(QStringList projectFiles READ getProjectFiles NOTIFY projectFilesChanged) diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index ebb3ccdf53..6bc499c0fb 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -33,7 +33,7 @@ MarketplaceItemUploader::MarketplaceItemUploader(QString title, QUuid marketplaceID, QStringList filePaths) : _title(title), - _description(description), _rootFilename(rootFilename), _filePaths(filePaths), _marketplaceID(marketplaceID) { + _description(description), _rootFilename(rootFilename), _marketplaceID(marketplaceID), _filePaths(filePaths) { qWarning() << "File paths: " << _filePaths.join(", "); //_marketplaceID = QUuid::fromString(QLatin1String("{50dbd62f-cb6b-4be4-afb8-1ef8bd2dffa8}")); } diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index f0e444ba33..5d3737319f 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -74,6 +74,12 @@ FST* FST::createFSTFromModel(QString fstPath, QString modelFilePath, const hfm:: 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"); From 89b03a3cdcce5fdee5282fc29f0fe171df8a1867 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 14:30:44 -0800 Subject: [PATCH 23/72] Add info popup box for avatar project files --- .../resources/qml/hifi/AvatarPackager.qml | 42 +++++++ .../qml/hifi/avatarPackager/AvatarProject.qml | 45 +------ .../qml/hifi/avatarPackager/InfoBox.qml | 112 ++++++++++++++++++ 3 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/InfoBox.qml diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index cd1bfb34f1..b3535395db 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -29,6 +29,48 @@ Windows.ScrollingWindow { height: pane.height width: pane.width + 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 + + 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 + } + } + } + } + } + Rectangle { id: modalOverlay anchors.fill: parent diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index babe3cda84..ffe012258a 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -271,47 +271,6 @@ Item { } } - Rectangle { - id: fileList - - visible: false - - color: "#6A6A6A" - - anchors.top: openFolderButton.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: showFilesText.top - //anchors.bottom: uploadButton.top - anchors.topMargin: 10 - anchors.bottomMargin: 10 - height: 1000 - - ListView { - anchors.fill: parent - model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles - delegate: Rectangle { - width: parent.width - height: fileText.implicitHeight + 8 - color: (index % 2 == 0) ? "#6A6A6A" : "grey" - 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 - } - } - } - } - RalewayRegular { id: showFilesText @@ -325,9 +284,9 @@ Item { size: 20 - text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in the project. <a href='toggle'>" + (fileList.visible ? "Hide" : "Show") + " list</a>" + text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>Show list</a>" - onLinkActivated: fileList.visible = !fileList.visible + onLinkActivated: fileListPopup.open() } Rectangle { diff --git a/interface/resources/qml/hifi/avatarPackager/InfoBox.qml b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml new file mode 100644 index 0000000000..89f5d5c7f8 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml @@ -0,0 +1,112 @@ +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; + + 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; + } + } + + } +} From 9e7f89e2b0cee6943130358fc57cdc8abf2bd7d1 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 14:38:46 -0800 Subject: [PATCH 24/72] Update link text for avatar project files --- interface/resources/qml/hifi/avatarPackager/AvatarProject.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index ffe012258a..96daf2c0ca 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -284,7 +284,7 @@ Item { size: 20 - text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>Show list</a>" + text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>See list</a>" onLinkActivated: fileListPopup.open() } From c21d0378005f98cef70ea5fef8903b63f804dc83 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 17:51:27 -0800 Subject: [PATCH 25/72] Fix marketplace uploader not working with projects larger than 128MB --- .../resources/qml/hifi/AvatarPackager.qml | 12 ++-- interface/src/avatar/AvatarProject.cpp | 13 ++--- .../src/avatar/MarketplaceItemUploader.cpp | 56 +++++++++---------- .../src/avatar/MarketplaceItemUploader.h | 8 ++- libraries/avatars/src/ProjectFile.h | 2 + 5 files changed, 45 insertions(+), 46 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index b3535395db..ff275923ac 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -127,9 +127,12 @@ Windows.ScrollingWindow { property alias showModalOverlay: modalOverlay.visible function openProject(path) { - AvatarPackagerCore.openAvatarProject(path); - avatarProject.reset(); - avatarPackager.state = "project"; + let project = AvatarPackagerCore.openAvatarProject(path); + if (project) { + avatarProject.reset(); + avatarPackager.state = "project"; + } + return project; } AvatarPackagerHeader { @@ -213,9 +216,8 @@ Windows.ScrollingWindow { browser.selectedFile.connect(function(fileUrl) { let fstFilePath = fileDialogHelper.urlToPath(fileUrl); - let currentAvatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); + let currentAvatarProject = avatarPackager.openProject(fstFilePath); if (currentAvatarProject) { - avatarPackager.state = AvatarPackagerState.project; avatarPackager.showModalOverlay = false; } }); diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 038ded64d8..17ed4b6921 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -162,10 +162,9 @@ void AvatarProject::appendDirectory(QString prefix, 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(prefix + "/" + entry.fileName()); - _projectFiles.append({ entry.absoluteFilePath(), prefix + "/" + entry.fileName() }); + _projectFiles.append({ entry.absoluteFilePath(), prefix + entry.fileName() }); } else if (entry.isDir()) { - appendDirectory(prefix + dir.dirName() + "/", entry.absoluteFilePath()); + appendDirectory(prefix + entry.fileName() + "/", entry.absoluteFilePath()); } } } @@ -188,12 +187,8 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { if (updateExisting) { itemID = _fst->getMarketplaceID(); } - QStringList projectFilePaths; - for (auto& path : _projectFiles) { - projectFilePaths.append(path.absolutePath); - } - auto uploader = new MarketplaceItemUploader(getProjectName(), "Empty description", QFileInfo(getFSTPath()).fileName(), itemID, - projectFilePaths); + auto uploader = new MarketplaceItemUploader(getProjectName(), "Empty description", QFileInfo(getFSTPath()).fileName(), + itemID, _projectFiles); connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() { if (uploader->getError() == MarketplaceItemUploader::Error::None) { _fst->setMarketplaceID(uploader->getMarketplaceID()); diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 6bc499c0fb..33cfb85c8e 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -31,18 +31,15 @@ MarketplaceItemUploader::MarketplaceItemUploader(QString title, QString description, QString rootFilename, QUuid marketplaceID, - QStringList filePaths) : + QList<ProjectFilePath> filePaths) : _title(title), _description(description), _rootFilename(rootFilename), _marketplaceID(marketplaceID), _filePaths(filePaths) { - qWarning() << "File paths: " << _filePaths.join(", "); - //_marketplaceID = QUuid::fromString(QLatin1String("{50dbd62f-cb6b-4be4-afb8-1ef8bd2dffa8}")); } void MarketplaceItemUploader::setState(State newState) { Q_ASSERT(_state != State::Complete); Q_ASSERT(_error == Error::None); Q_ASSERT(newState != _state); - qDebug() << "Setting uploader state to: " << newState; _state = newState; emit stateChanged(newState); @@ -113,13 +110,12 @@ void MarketplaceItemUploader::doGetCategories() { }; bool success; - int id; - std::tie(success, id) = extractCategoryID(); - qDebug() << "Done " << success << id; + 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 { @@ -133,24 +129,26 @@ void MarketplaceItemUploader::doUploadAvatar() { //buffer.open(QIODevice::WriteOnly); QuaZip zip{ &buffer }; if (!zip.open(QuaZip::Mode::mdAdd)) { - qWarning() << "Failed to open zip!!"; + qWarning() << "Failed to open zip"; + setError(Error::Unknown); + return; } for (auto& filePath : _filePaths) { - qWarning() << "Zipping: " << filePath; - QFileInfo fileInfo{ filePath }; + qWarning() << "Zipping: " << filePath.absolutePath << filePath.relativePath; + QFileInfo fileInfo{ filePath.absolutePath }; QuaZipFile zipFile{ &zip }; - if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileInfo.fileName()))) { + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(filePath.relativePath))) { qWarning() << "Could not open zip file:" << zipFile.getZipError(); setError(Error::Unknown); return; } - QFile file{ filePath }; + QFile file{ filePath.absolutePath }; if (file.open(QIODevice::ReadOnly)) { zipFile.write(file.readAll()); } else { - qWarning() << "Failed to open: " << filePath; + qWarning() << "Failed to open: " << filePath.absolutePath; } file.close(); zipFile.close(); @@ -175,28 +173,26 @@ void MarketplaceItemUploader::doUploadAvatar() { } auto accountManager = DependencyManager::get<AccountManager>(); auto request = accountManager->createRequest(path, AccountManagerAuth::Required); - qWarning() << "Request url is: " << request.url(); - - QJsonObject root{ { "marketplace_item", - QJsonObject{ { "title", _title }, - { "description", _description }, - { "root_file_key", _rootFilename }, - { "category_ids", QJsonArray({ 5 }) }, - { "license", 0 }, - { "files", QString::fromLatin1(_fileData.toBase64()) } } } }; request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - QJsonDocument doc{ root }; - qWarning() << "data: " << doc.toJson(); + // TODO(huffman) add JSON escaping + auto escapeJson = [](QString str) -> QString { return str; }; + + QString jsonString = "{\"marketplace_item\":{"; + jsonString += "\"title\":\"" + escapeJson(_title) + "\""; + 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()) + "\"}}"; - _fileData.toBase64(); QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkReply* reply{ nullptr }; if (creating) { - reply = networkAccessManager.post(request, doc.toJson()); + reply = networkAccessManager.post(request, jsonString.toUtf8()); } else { - reply = networkAccessManager.put(request, doc.toJson()); + reply = networkAccessManager.put(request, jsonString.toUtf8()); } connect(reply, &QNetworkReply::uploadProgress, this, [this](float bytesSent, float bytesTotal) { @@ -210,11 +206,9 @@ void MarketplaceItemUploader::doUploadAvatar() { connect(reply, &QNetworkReply::finished, this, [this, reply]() { _responseData = reply->readAll(); - qWarning() << "Finished request " << _responseData; auto error = reply->error(); if (error == QNetworkReply::NoError) { - auto doc = QJsonDocument::fromJson(_responseData.toLatin1()); auto status = doc.object()["status"].toString(); if (status == "success") { @@ -223,9 +217,11 @@ void MarketplaceItemUploader::doUploadAvatar() { 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); } }); @@ -265,7 +261,7 @@ void MarketplaceItemUploader::doWaitForInventory() { } auto data = root["data"]; if (!data.isObject()) { - return false; + return false; } auto assets = data.toObject()["assets"]; if (!assets.isArray()) { diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index 4b8b675255..4fb1713b7d 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -13,6 +13,8 @@ #ifndef hifi_MarketplaceItemUploader_h #define hifi_MarketplaceItemUploader_h +#include "ProjectFile.h" + #include <QObject> #include <QUuid> @@ -50,7 +52,7 @@ public: QString description, QString rootFilename, QUuid marketplaceID, - QStringList filePaths); + QList<ProjectFilePath> filePaths); Q_INVOKABLE void send(); @@ -91,13 +93,15 @@ private: QString _description; QString _rootFilename; QUuid _marketplaceID; + int _categoryID; int _itemVersion; QString _responseData; int _numRequestsForInventory{ 0 }; - QStringList _filePaths; + QString _rootFilePath; + QList<ProjectFilePath> _filePaths; QByteArray _fileData; }; diff --git a/libraries/avatars/src/ProjectFile.h b/libraries/avatars/src/ProjectFile.h index df92513a1b..82930a3464 100644 --- a/libraries/avatars/src/ProjectFile.h +++ b/libraries/avatars/src/ProjectFile.h @@ -1,6 +1,8 @@ #ifndef hifi_AvatarProjectFile_h #define hifi_AvatarProjectFile_h +#include <QObject> + class ProjectFilePath { Q_GADGET; public: From 2355499ae110bdadf202d44277f75b5295237276 Mon Sep 17 00:00:00 2001 From: Brad Davis <bdavis@saintandreas.org> Date: Thu, 27 Dec 2018 18:16:03 -0800 Subject: [PATCH 26/72] Fix version code for android APKs --- android/build_android.sh | 4 ++-- android/containerized_build.sh | 23 ++++++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) 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" + + From 83a60a541b14723e542da8544607db5f5744121e Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 18:53:07 -0800 Subject: [PATCH 27/72] Fix styling and flow issues in avatar uploader --- .../resources/qml/hifi/AvatarPackager.qml | 2 + .../qml/hifi/avatarPackager/AvatarProject.qml | 2 +- .../avatarPackager/AvatarProjectUpload.qml | 46 ++++++++++++++++--- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index ff275923ac..1300184591 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -45,6 +45,8 @@ Windows.ScrollingWindow { anchors.leftMargin: 29 anchors.rightMargin: 29 + clip: true + ListView { anchors.fill: parent model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 96daf2c0ca..c94c9570ae 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -229,7 +229,7 @@ Item { name: "upload-success" PropertyChanges { target: infoMessage - text: "Your avatar has been uploaded to our servers. You can modify the project files and update it again to make changes on the uploaded avatar." + text: "Your avatar has been successfully uploaded to our servers. Make changes to your avatar by editing and uploading the project files." } }, State { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index f02dc5a218..b21bd38070 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -17,7 +17,7 @@ Item { Timer { id: backToProjectTimer - interval: 5000 + interval: 2000 running: false repeat: false onTriggered: { @@ -37,7 +37,9 @@ Item { if (visible) { root.uploader.stateChanged.connect(stateChangedCallback); root.uploader.finishedChanged.connect(function() { - backToProjectTimer.start(); + if (root.uploader.error === 0) { + backToProjectTimer.start(); + } }); } } @@ -53,7 +55,7 @@ Item { id: statusItem width: parent.width - height: 192 + height: 256 states: [ State { @@ -69,6 +71,8 @@ Item { 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 } } ] @@ -99,7 +103,7 @@ Item { verticalCenter: parent.verticalCenter } - size: 164 + size: 315 text: "+" color: "#EA4C5F" } @@ -151,20 +155,48 @@ Item { } RalewayRegular { - visible: root.uploader.error + id: errorMessage + + visible: false anchors.left: parent.left anchors.right: parent.right - anchors.bottom: parent.bottom + anchors.bottom: errorFooter.top anchors.leftMargin: 16 anchors.rightMargin: 16 - anchors.bottomMargin: 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: function() { + avatarPackager.state = "project" + } + } + } + } } Column { From 8f70865cf71df1cbcec7fc73d91f084a3bd0e5e2 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 21:02:56 -0800 Subject: [PATCH 28/72] Fix marketplace upload finishing before inventory item is valid --- interface/src/avatar/MarketplaceItemUploader.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 33cfb85c8e..45a13f04c4 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -275,9 +275,10 @@ void MarketplaceItemUploader::doWaitForInventory() { } if (id == _marketplaceID) { auto version = assetObject["version"]; - if (version.isDouble()) { + auto valid = assetObject["valid"]; + if (version.isDouble() && valid.isBool()) { int versionInt = version.toDouble(); - if (versionInt >= _itemVersion) { + if ((int)version.toDouble() >= _itemVersion && valid.toBool()) { return true; } } From 24aaeee5fd82d7f29fe00c9986ccdd487c54ab40 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 22:55:54 -0800 Subject: [PATCH 29/72] Disable back button on avatar upload page --- .../resources/qml/hifi/AvatarPackager.qml | 4 ++-- .../avatarPackager/AvatarPackagerHeader.qml | 13 ++++++----- .../avatarPackager/AvatarProjectUpload.qml | 23 ++----------------- interface/src/avatar/AvatarProject.cpp | 2 +- 4 files changed, 12 insertions(+), 30 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 1300184591..2492746627 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -102,7 +102,7 @@ Windows.ScrollingWindow { states: [ State { name: AvatarPackagerState.main - PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); faqEnabled: true; backButtonEnabled: false } + PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); faqEnabled: true; backButtonVisible: false } PropertyChanges { target: avatarPackagerMain; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer } }, @@ -120,7 +120,7 @@ Windows.ScrollingWindow { }, State { name: "project-upload" - PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; backButtonEnabled: false } PropertyChanges { target: avatarUploader; visible: true } PropertyChanges { target: avatarPackagerFooter; visible: false } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 663d4d0f3a..5b01005edd 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -4,7 +4,7 @@ import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 Rectangle { - id: avatarPackagerHeader + id: root width: parent.width height: 74 @@ -12,13 +12,14 @@ Rectangle { property alias title: title.text property alias faqEnabled: faq.visible - property alias backButtonEnabled: back.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; signal backButtonClicked RalewaySemiBold { id: back - visible: true + visible: backButtonEnabled && backButtonVisible size: 28 anchors.top: parent.top anchors.bottom: parent.bottom @@ -29,7 +30,7 @@ Rectangle { color: "white" MouseArea { anchors.fill: parent - onClicked: avatarPackagerHeader.backButtonClicked() + onClicked: root.backButtonClicked() hoverEnabled: true onEntered: { state = "hovering" } onExited: { state = "" } @@ -50,8 +51,8 @@ Rectangle { size: 28 anchors.top: parent.top anchors.bottom: parent.bottom - anchors.left: back.visible ? back.right : parent.left - anchors.leftMargin: back.visible ? 11 : 21 + anchors.left: root.backButtonVisible ? back.right : parent.left + anchors.leftMargin: root.backButtonVisible ? 11 : 21 anchors.verticalCenter: title.verticalCenter text: qsTr("Avatar Packager") color: "white" diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index b21bd38070..2031392ca2 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -47,8 +47,6 @@ Item { Item { id: uploadStatus - visible: !!root.uploader - anchors.fill: parent Item { @@ -60,14 +58,14 @@ Item { states: [ State { name: "success" - when: !!root.uploader && root.uploader.state >= 4 && root.uploader.error === 0 + 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 && root.uploader.finished && root.uploader.error !== 0 + when: root.uploader.finished && root.uploader.error !== 0 PropertyChanges { target: uploadSpinner; visible: false } PropertyChanges { target: errorIcon; visible: true } PropertyChanges { target: successIcon; visible: false } @@ -198,21 +196,4 @@ Item { } } } - - Column { - id: debugInfo - - visible: false - - Text { - text: "Uploading" - color: "white" - - } - Text { - text: "State: " + (!!root.uploader ? root.uploader.state : " NONE") - color: "white" - } - } - } diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 17ed4b6921..09d60163b6 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -187,7 +187,7 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { if (updateExisting) { itemID = _fst->getMarketplaceID(); } - auto uploader = new MarketplaceItemUploader(getProjectName(), "Empty description", QFileInfo(getFSTPath()).fileName(), + auto uploader = new MarketplaceItemUploader(getProjectName(), "", QFileInfo(getFSTPath()).fileName(), itemID, _projectFiles); connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() { if (uploader->getError() == MarketplaceItemUploader::Error::None) { From 493a05f905504a3357c89ef4f28bc52a143ba7ef Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Fri, 28 Dec 2018 08:45:45 -0800 Subject: [PATCH 30/72] Remove bad comment. --- tools/nitpick/src/TestRunner.cpp | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tools/nitpick/src/TestRunner.cpp b/tools/nitpick/src/TestRunner.cpp index bbdd36a12a..9aca2bf3e6 100644 --- a/tools/nitpick/src/TestRunner.cpp +++ b/tools/nitpick/src/TestRunner.cpp @@ -485,9 +485,7 @@ void TestRunner::runInterfaceWithTestScript() { " --url " + url + " --no-updater" + " --no-login-suggestion" - " --testScript " + deleteScript + " quitWhenFinished" + - " --testResultsLocation " + _snapshotFolder; - + " --testScript " + deleteScript + " quitWhenFinished"; system(commandLine.toStdString().c_str()); @@ -503,10 +501,6 @@ void TestRunner::runInterfaceWithTestScript() { _interfaceWorker->setCommandLine(commandLine); emit startInterface(); #elif defined Q_OS_MAC - // On The Mac, we need to resize Interface. The Interface window opens a few seconds after the process - // has started. - // Before starting interface, start a process that will resize interface 10s after it opens - // This is performed by creating a bash script that runs to processes QFile script; script.setFileName(_workingFolder + "/runInterfaceTests.sh"); if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { @@ -516,7 +510,20 @@ void TestRunner::runInterfaceWithTestScript() { } script.write("#!/bin/sh\n\n"); - + + // First, run script to delete any entities in test area + commandLine = + "open -W \"" +_installationFolder + "/interface.app\" --args" + + " --url " + url + + " --no-updater" + + " --no-login-suggestion" + " --testScript " + deleteScript + " quitWhenFinished\n"; + + script.write(commandLine.toStdString().c_str()); + + // On The Mac, we need to resize Interface. The Interface window opens a few seconds after the process + // has started. + // Before starting interface, start a process that will resize interface 10s after it opens commandLine = _workingFolder +"/waitForStart.sh interface && sleep 10 && " + _workingFolder +"/setInterfaceSizeAndPosition.sh &\n"; script.write(commandLine.toStdString().c_str()); @@ -527,7 +534,7 @@ void TestRunner::runInterfaceWithTestScript() { " --no-login-suggestion" " --testScript " + testScript + " quitWhenFinished" + " --testResultsLocation " + _snapshotFolder + - " && " + _workingFolder +"/waitForFinish.sh interface"; + " && " + _workingFolder +"/waitForFinish.sh interface\n"; script.write(commandLine.toStdString().c_str()); From c28167f7973ae3de14e8f24f2d8faabb3777b3b7 Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Fri, 28 Dec 2018 14:24:19 -0800 Subject: [PATCH 31/72] Initial creation of class - has COPY of getOperatingSystemType(). --- .../PlatformInfoScriptingInterface.cpp | 23 ++++++++++++++ .../PlatformInfoScriptingInterface.h | 30 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 interface/src/scripting/PlatformInfoScriptingInterface.cpp create mode 100644 interface/src/scripting/PlatformInfoScriptingInterface.h diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp new file mode 100644 index 0000000000..e2fea92ed3 --- /dev/null +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -0,0 +1,23 @@ +// +// Created by Nissim Hadar on 2018/12/28 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "PlatformInfoScriptingInterface.h" + +PlatformInfoScriptingInterface* PlatformInfoScriptingInterface::getInstance() { + static PlatformInfoScriptingInterface sharedInstance; + return &sharedInstance; +} + +QString PlatformInfoScriptingInterface::getOperatingSystemType() { +#ifdef Q_OS_WIN + return "WINDOWS"; +#elif defined Q_OS_MAC + return "MACOS"; +#else + return "UNKNOWN"; +#endif +} diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.h b/interface/src/scripting/PlatformInfoScriptingInterface.h new file mode 100644 index 0000000000..b30af12071 --- /dev/null +++ b/interface/src/scripting/PlatformInfoScriptingInterface.h @@ -0,0 +1,30 @@ +// +// Created by Nissim Hadar on 2018/12/28 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_PlatformInfoScriptingInterface_h +#define hifi_PlatformInfoScriptingInterface_h + +#include <QtCore/QObject> + +class QScriptValue; + +class PlatformInfoScriptingInterface : public QObject { + Q_OBJECT + +public slots: + static PlatformInfoScriptingInterface* getInstance(); + + /**jsdoc + * Returns the Operating Sytem type + * @function Test.getOperatingSystemType + * @returns {string} "WINDOWS", "MACOS" or "UNKNOWN" + */ + QString getOperatingSystemType(); +}; + +#endif // hifi_PlatformInfoScriptingInterface_h From 246c4dab9474a94dae6a14d27f7717fcfd251aa4 Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Fri, 28 Dec 2018 14:41:55 -0800 Subject: [PATCH 32/72] Created the PlatformInfo scripting object. --- interface/src/Application.cpp | 2 ++ interface/src/scripting/PlatformInfoScriptingInterface.h | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5e41530d93..56130160c4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -170,6 +170,7 @@ #include "scripting/Audio.h" #include "networking/CloseEventSender.h" #include "scripting/TestScriptingInterface.h" +#include "scripting/PlatformInfoScriptingInterface.h" #include "scripting/AssetMappingsScriptingInterface.h" #include "scripting/ClipboardScriptingInterface.h" #include "scripting/DesktopScriptingInterface.h" @@ -6994,6 +6995,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance()); } + scriptEngine->registerGlobalObject("PlatformInfo", PlatformInfoScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); // hook our avatar and avatar hash map object into this script engine diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.h b/interface/src/scripting/PlatformInfoScriptingInterface.h index b30af12071..dc88e1963f 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.h +++ b/interface/src/scripting/PlatformInfoScriptingInterface.h @@ -25,6 +25,10 @@ public slots: * @returns {string} "WINDOWS", "MACOS" or "UNKNOWN" */ QString getOperatingSystemType(); + + /**jsdoc + * Returns the CPU brand + */ }; #endif // hifi_PlatformInfoScriptingInterface_h From cb33a91a34366f04ac286cb342ea63cff4b3d2ca Mon Sep 17 00:00:00 2001 From: Thijs Wenker <me@thoys.nl> Date: Sat, 29 Dec 2018 03:31:56 +0100 Subject: [PATCH 33/72] - rename functionality - avatar packager works in tablet now --- .../resources/qml/hifi/AvatarPackager.qml | 276 ----------------- .../qml/hifi/AvatarPackagerWindow.qml | 32 ++ .../hifi/avatarPackager/AvatarPackagerApp.qml | 288 ++++++++++++++++++ .../avatarPackager/AvatarPackagerHeader.qml | 102 ++++++- .../avatarPackager/AvatarPackagerState.qml | 1 + .../qml/hifi/avatarPackager/AvatarProject.qml | 2 +- .../hifi/avatarPackager/AvatarProjectCard.qml | 7 + .../avatarPackager/AvatarProjectUpload.qml | 10 +- .../avatarPackager/AvatarUploadStatusItem.qml | 8 +- .../avatarPackager/CreateAvatarProject.qml | 3 +- .../avatarPackager/ProjectInputControl.qml | 2 +- .../qml/hifi/tablet/AvatarPackager.qml | 15 + interface/src/avatar/AvatarPackager.cpp | 88 ++++-- interface/src/avatar/AvatarPackager.h | 33 +- interface/src/avatar/AvatarProject.cpp | 25 +- interface/src/avatar/AvatarProject.h | 15 +- libraries/fbx/src/FST.cpp | 4 +- libraries/fbx/src/FST.h | 4 +- 18 files changed, 543 insertions(+), 372 deletions(-) delete mode 100644 interface/resources/qml/hifi/AvatarPackager.qml create mode 100644 interface/resources/qml/hifi/AvatarPackagerWindow.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml create mode 100644 interface/resources/qml/hifi/tablet/AvatarPackager.qml diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml deleted file mode 100644 index 2492746627..0000000000 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ /dev/null @@ -1,276 +0,0 @@ -import QtQuick 2.6 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQml.Models 2.1 -import QtGraphicalEffects 1.0 -import "../controlsUit" 1.0 as HifiControls -import "../stylesUit" 1.0 -import "../windows" as Windows -import "../dialogs" -import "./avatarPackager" 1.0 -import "avatarapp" 1.0 as AvatarApp - -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 } - - Item { - id: windowContent - height: pane.height - width: pane.width - - 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 - } - } - } - } - } - - 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"); faqEnabled: 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: "project-upload" - PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; backButtonEnabled: false } - PropertyChanges { target: avatarUploader; visible: true } - PropertyChanges { target: avatarPackagerFooter; visible: false } - } - ] - - property alias showModalOverlay: modalOverlay.visible - - function openProject(path) { - let project = AvatarPackagerCore.openAvatarProject(path); - if (project) { - avatarProject.reset(); - avatarPackager.state = "project"; - } - return project; - } - - AvatarPackagerHeader { - id: avatarPackagerHeader - onBackButtonClicked: { - avatarPackager.state = "main" - } - } - - Item { - height: pane.height - avatarPackagerHeader.height - avatarPackagerFooter.height - width: pane.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 = desktop.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); - let currentAvatarProject = avatarPackager.openProject(fstFilePath); - if (currentAvatarProject) { - avatarPackager.showModalOverlay = false; - } - }); - } - } - } - - Flow { - visible: AvatarPackagerCore.recentProjects.length === 0 - anchors { - fill: parent - topMargin: 18 - leftMargin: 16 - rightMargin: 16 - } - RalewayRegular { - size: 20 - color: "white" - text: qsTr("Use a custom avatar to express your identity") - } - RalewayRegular { - size: 20 - color: "white" - text: qsTr("To learn more about using this tool, visit our docs") - } - } - - Column { - visible: AvatarPackagerCore.recentProjects.length > 0 - anchors { - fill: parent - topMargin: 18 - leftMargin: 16 - rightMargin: 16 - } - spacing: 10 - - Repeater { - model: AvatarPackagerCore.recentProjects - AvatarProjectCard { - title: modelData.name - path: modelData.path - onOpen: avatarPackager.openProject(modelData.path) - } - } - } - } - } - AvatarPackagerFooter { - id: avatarPackagerFooter - } - } - } -} diff --git a/interface/resources/qml/hifi/AvatarPackagerWindow.qml b/interface/resources/qml/hifi/AvatarPackagerWindow.qml new file mode 100644 index 0000000000..9d434ef97c --- /dev/null +++ b/interface/resources/qml/hifi/AvatarPackagerWindow.qml @@ -0,0 +1,32 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtQml.Models 2.1 +import QtGraphicalEffects 1.0 +import "../controlsUit" 1.0 as HifiControls +import "../stylesUit" 1.0 +import "../windows" as Windows +import "../controls" 1.0 +import "../dialogs" +import "avatarPackager" 1.0 +import "avatarapp" 1.0 as AvatarApp + +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..2b21b4d938 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -0,0 +1,288 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtQml.Models 2.1 +import QtGraphicalEffects 1.0 +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 +import "../../windows" as Windows +import "../../controls" 1.0 +import "../../dialogs" +import "../avatarapp" 1.0 as AvatarApp + +Item { + HifiConstants { id: hifi } + + property alias desktopObject: avatarPackager.desktopObject + + id: windowContent + // height: pane ? pane.height : parent.width + // width: pane ? pane.width : parent.width + + + 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 + } + } + } + } + } + + 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 project = AvatarPackagerCore.openAvatarProject(path); + if (project) { + avatarProject.reset(); + avatarPackager.state = AvatarPackagerState.project; + } + return project; + } + + function openDocs() { + Qt.openUrlExternally("https://docs.highfidelity.com/create/avatars/create-avatars#how-to-package-your-avatar"); + } + + AvatarPackagerHeader { + 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); + let currentAvatarProject = avatarPackager.openProject(fstFilePath); + if (currentAvatarProject) { + avatarPackager.showModalOverlay = false; + } + }); + } + } + } + + Flow { + visible: AvatarPackagerCore.recentProjects.length === 0 + anchors { + fill: parent + topMargin: 18 + leftMargin: 16 + rightMargin: 16 + } + RalewayRegular { + size: 20 + color: "white" + text: qsTr("Use a custom avatar to express your identity") + } + RalewayRegular { + size: 20 + color: "white" + text: qsTr("To learn more about using this tool, visit our docs") + } + } + + Column { + visible: AvatarPackagerCore.recentProjects.length > 0 + anchors { + fill: parent + topMargin: 18 + 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/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 5b01005edd..7037aa9d92 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -11,11 +11,18 @@ Rectangle { color: "#252525" property alias title: title.text - property alias faqEnabled: faq.visible + 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 bool canRename: false + property int colorScheme + + property color textColor: "white" + property color hoverTextColor: "gray" + property color pressedTextColor: "#6A6A6A" + signal backButtonClicked + signal docsButtonClicked RalewaySemiBold { id: back @@ -27,7 +34,7 @@ Rectangle { anchors.leftMargin: 16 anchors.verticalCenter: back.verticalCenter text: "◀" - color: "white" + color: textColor MouseArea { anchors.fill: parent onClicked: root.backButtonClicked() @@ -37,37 +44,102 @@ Rectangle { states: [ State { name: "hovering" - PropertyChanges { - target: back - color: "gray" - } + PropertyChanges { target: back; color: hoverTextColor } } ] } } - - RalewaySemiBold { - id: title - size: 28 + 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.verticalCenter: title.verticalCenter - text: qsTr("Avatar Packager") - color: "white" + anchors.right: docs.left + states: [ + State { + name: "renaming" + PropertyChanges { target: title; visible: false } + PropertyChanges { target: titleInputArea; visible: true } + } + ] + + RalewaySemiBold { + id: title + size: 28 + anchors.fill: parent + text: qsTr("Avatar Packager") + color: "white" + + MouseArea { + anchors.fill: parent + 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" && !titleArea.focus) { + //titleArea.state = ""; + accepted(); + } + } + Keys.onPressed: { + if (event.key === Qt.Key_Escape) { + titleArea.state = ""; + } + } + onAccepted: { + if (acceptableInput) { + //AvatarPackagerCore.renameProject(text); + console.warn(text); + AvatarPackagerCore.currentAvatarProject.name = text; + console.warn(AvatarPackagerCore.currentAvatarProject.name); + + } + titleArea.state = ""; + } + } + } } RalewaySemiBold { - id: faq + id: docs visible: false size: 28 anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right anchors.rightMargin: 16 - anchors.verticalCenter: faq.verticalCenter + anchors.verticalCenter: docs.verticalCenter text: qsTr("Docs") color: "white" + MouseArea { + anchors.fill: parent + onClicked: { + docsButtonClicked(); + } + } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml index f12edf4952..c81173a080 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml @@ -6,4 +6,5 @@ Item { 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 index c94c9570ae..a5a2263346 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -178,7 +178,7 @@ Item { } }); root.uploader.send(); - avatarPackager.state = "project-upload"; + avatarPackager.state = AvatarPackagerState.projectUpload; } function showConfirmUploadPopup() { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml index 736de2019c..5496711d44 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -53,11 +53,14 @@ Item { RalewayBold { id: title + elide: "ElideRight" anchors { top: parent.top topMargin: 13 left: parent.left leftMargin: 16 + right: parent.right + rightMargin: 16 } text: "<title missing>" size: 16 @@ -69,7 +72,11 @@ Item { top: title.bottom left: parent.left leftMargin: 32 + right: background.right + rightMargin: 16 } + elide: "ElideLeft" + horizontalAlignment: Text.AlignRight text: "<path missing>" size: 20 } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index 2031392ca2..c1d1a98158 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -21,8 +21,8 @@ Item { running: false repeat: false onTriggered: { - if (avatarPackager.state =="project-upload") { - avatarPackager.state = "project" + if (avatarPackager.state === AvatarPackagerState.projectUpload) { + avatarPackager.state = AvatarPackagerState.project; } } } @@ -130,7 +130,7 @@ Item { AvatarUploadStatusItem { id: statusCategories uploader: root.uploader - text: "Retreiving categories" + text: "Retrieving categories" uploaderState: 1 } @@ -189,8 +189,8 @@ Item { colorScheme: root.colorScheme width: 133 height: 40 - onClicked: function() { - avatarPackager.state = "project" + onClicked: { + avatarPackager.state = AvatarPackagerState.project; } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml index ca01e453e9..4749e912c6 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml @@ -14,9 +14,9 @@ Item { property int uploaderState; property var uploader; - state: root.uploader.state > uploaderState - ? "success" - : (root.uploader.error !== 0 ? "fail" : (root.uploader.state === uploaderState ? "running" : "")) + state: root.uploader === undefined ? "" : + (root.uploader.state > uploaderState ? "success" + : (root.uploader.error !== 0 ? "fail" : (root.uploader.state === uploaderState ? "running" : ""))) states: [ State { @@ -90,4 +90,4 @@ Item { size: 28 color: "#777777" } -} \ No newline at end of file +} diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml index a5d335feba..d6f530a196 100644 --- a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -119,7 +119,7 @@ Item { ProjectInputControl { id: textureFolder - label: "Specify Texture Folder <i> - optional</i>" + label: "Specify Texture Folder - <i>Optional</i>" colorScheme: root.colorScheme browseEnabled: true browseFolder: true @@ -128,5 +128,4 @@ Item { onTextChanged: checkErrors() } } - } diff --git a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml index 2ac4a37d02..f0a3aac8a7 100644 --- a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml +++ b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml @@ -57,7 +57,7 @@ Column { colorScheme: root.colorScheme onClicked: { avatarPackager.showModalOverlay = true; - let browser = desktop.fileDialog({ + let browser = avatarPackager.desktopObject.fileDialog({ selectDirectory: browseFolder, dir: browseDir, filter: browseFilter, 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/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index d8aadeb4e0..eef574a8b5 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -11,6 +11,8 @@ #include "AvatarPackager.h" +#include "Application.h" + #include <QQmlContext> #include <QQmlEngine> #include <QUrl> @@ -19,8 +21,9 @@ #include "ModelSelector.h" #include <avatar/MarketplaceItemUploader.h> -#include <thread> #include <mutex> +#include "scripting/HMDScriptingInterface.h" +#include "ui/TabletScriptingInterface.h" std::once_flag setupQMLTypesFlag; AvatarPackager::AvatarPackager() { @@ -38,31 +41,32 @@ AvatarPackager::AvatarPackager() { } bool AvatarPackager::open() { - static const QUrl url{ "hifi/AvatarPackager.qml" }; + static const QUrl url{ "hifi/AvatarPackagerWindow.qml" }; const auto packageModelDialogCreated = [=](QQmlContext* context, QObject* newObject) { context->setContextProperty("AvatarPackagerCore", this); }; - DependencyManager::get<OffscreenUi>()->show(url, "AvatarPackager", packageModelDialogCreated); + + static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; + auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>(); + auto tablet = dynamic_cast<TabletProxy*>(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + auto hmd = DependencyManager::get<HMDScriptingInterface>(); + + if (tablet->getToolbarMode()) { + DependencyManager::get<OffscreenUi>()->show(url, "AvatarPackager", packageModelDialogCreated); + } else { + static const QUrl url("hifi/tablet/AvatarPackager.qml"); + if (!tablet->isPathLoaded(url)) { + tablet->getTabletSurface()->getSurfaceContext()->setContextProperty("AvatarPackagerCore", this); + tablet->pushOntoStack(url); + } + } return true; } -AvatarProject* AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) { - if (_currentAvatarProject) { - _currentAvatarProject->deleteLater(); - } - _currentAvatarProject = AvatarProject::openAvatarProject(avatarProjectFSTPath); - if (_currentAvatarProject) { - addRecentProject(avatarProjectFSTPath, _currentAvatarProject->getProjectName()); - } - qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; - QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); - emit avatarProjectChanged(); - return _currentAvatarProject; -} - -void AvatarPackager::addRecentProject(QString fstPath, QString projectName) { +void AvatarPackager::addCurrentProjectToRecentProjects() { const int MAX_RECENT_PROJECTS = 5; + const QString& fstPath = _currentAvatarProject->getFSTPath(); auto removeProjects = QVector<RecentAvatarProject>(); for (auto project : _recentProjects) { if (project.getProjectFSTPath() == fstPath) { @@ -73,27 +77,61 @@ void AvatarPackager::addRecentProject(QString fstPath, QString projectName) { _recentProjects.removeOne(removeProject); } - RecentAvatarProject newRecentProject = RecentAvatarProject(projectName, fstPath); + const auto newRecentProject = RecentAvatarProject(_currentAvatarProject->getProjectName(), fstPath); _recentProjects.prepend(newRecentProject); while (_recentProjects.size() > MAX_RECENT_PROJECTS) { _recentProjects.pop_back(); } - _recentProjectsSetting.set(recentProjectsToVariantList()); + _recentProjectsSetting.set(recentProjectsToVariantList(false)); emit recentProjectsChanged(); } +QVariantList AvatarPackager::recentProjectsToVariantList(bool includeProjectPaths) { + 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())); + } +} + +AvatarProject* AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) { + setAvatarProject(AvatarProject::openAvatarProject(avatarProjectFSTPath)); + return _currentAvatarProject; +} + AvatarProject* AvatarPackager::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder) { + setAvatarProject(AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder)); + return _currentAvatarProject; +} + +void AvatarPackager::setAvatarProject(AvatarProject* avatarProject) { + if (avatarProject == _currentAvatarProject) { + return; + } if (_currentAvatarProject) { _currentAvatarProject->deleteLater(); } - _currentAvatarProject = AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder); + _currentAvatarProject = avatarProject; if (_currentAvatarProject) { - addRecentProject(_currentAvatarProject->getFSTPath(), _currentAvatarProject->getProjectName()); + addCurrentProjectToRecentProjects(); + connect(_currentAvatarProject, &AvatarProject::nameChanged, this, &AvatarPackager::addCurrentProjectToRecentProjects); + QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); } - qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; - QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); emit avatarProjectChanged(); - return _currentAvatarProject; } diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index 57cbf046a7..343176497f 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -35,12 +35,14 @@ public: _projectFSTPath = other._projectFSTPath; } - ~RecentAvatarProject() = default; - 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; } @@ -72,31 +74,18 @@ signals: private: Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; Q_INVOKABLE QString getAvatarProjectsPath() const { return AvatarProject::getDefaultProjectsPath(); } - Q_INVOKABLE QVariantList getRecentProjects() { return recentProjectsToVariantList(); } + Q_INVOKABLE QVariantList getRecentProjects() { return recentProjectsToVariantList(true); } - void addRecentProject(QString fstPath, QString projectName); + void setAvatarProject(AvatarProject* avatarProject); + + void addCurrentProjectToRecentProjects(); AvatarProject* _currentAvatarProject{ nullptr }; QVector<RecentAvatarProject> _recentProjects; - QVariantList recentProjectsToVariantList() { - QVariantList result; - for (const auto& project : _recentProjects) { - QVariantMap projectVariant; - projectVariant.insert("name", project.getProjectName()); - projectVariant.insert("path", project.getProjectFSTPath()); - result.append(projectVariant); - } - - return result; - } - void 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())); - } - } + QVariantList recentProjectsToVariantList(bool includeProjectPaths); + + void recentProjectsFromVariantList(QVariantList projectsVariant); Setting::Handle<QVariantList> _recentProjectsSetting{ "io.highfidelity.avatarPackager.recentProjects", QVariantList() }; diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 09d60163b6..0c29bcf906 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -81,18 +81,18 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, } }; - foreach(const HFMMaterial mat, hfmModel->materials) { - addTextureToList(mat.normalTexture); - addTextureToList(mat.albedoTexture); - addTextureToList(mat.opacityTexture); - addTextureToList(mat.glossTexture); - addTextureToList(mat.roughnessTexture); - addTextureToList(mat.specularTexture); - addTextureToList(mat.metallicTexture); - addTextureToList(mat.emissiveTexture); - addTextureToList(mat.occlusionTexture); - addTextureToList(mat.scatteringTexture); - addTextureToList(mat.lightmapTexture); + 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); @@ -152,7 +152,6 @@ AvatarProject::AvatarProject(FST* fst) { _fst->setScriptPaths(getScriptPaths(QDir(_directory.path() + "/scripts"))); _fst->write(); - //_projectFiles = _directory.entryList(); refreshProjectFiles(); _projectPath = fileInfo.absoluteDir().absolutePath(); diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index e950fd7379..469324004d 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -28,10 +28,10 @@ class AvatarProject : public QObject { Q_PROPERTY(QStringList projectFiles READ getProjectFiles NOTIFY projectFilesChanged) - Q_PROPERTY(QString projectFolderPath READ getProjectPath) - Q_PROPERTY(QString projectFSTPath READ getFSTPath) - Q_PROPERTY(QString projectFBXPath READ getFBXPath) - Q_PROPERTY(QString name READ getProjectName NOTIFY nameChanged) + 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); @@ -39,6 +39,13 @@ public: 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 _fst->getModelPath(); } diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index 5d3737319f..9510ca5962 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -15,7 +15,7 @@ #include <QFileInfo> #include <hfm/HFM.h> -FST::FST(const QString& fstPath, QVariantHash data) : _fstPath(fstPath) { +FST::FST(QString fstPath, QVariantHash data) : _fstPath(std::move(fstPath)) { auto setValueFromFSTData = [&data] (const QString& propertyID, auto &targetProperty) mutable { if (data.contains(propertyID)) { @@ -38,7 +38,7 @@ FST::FST(const QString& fstPath, QVariantHash data) : _fstPath(fstPath) { _other = data; } -FST* FST::createFSTFromModel(QString fstPath, QString modelFilePath, const hfm::Model& hfmModel) { +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 diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h index 6fd654987e..6104130512 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -26,9 +26,9 @@ class FST : public QObject { Q_PROPERTY(QUuid marketplaceID READ getMarketplaceID) Q_PROPERTY(bool hasMarketplaceID READ getHasMarketplaceID NOTIFY marketplaceIDChanged) public: - FST(const QString& fstPath, QVariantHash data); + FST(QString fstPath, QVariantHash data); - static FST* createFSTFromModel(QString fstPath, QString modelFilePath, const hfm::Model& hfmModel); + static FST* createFSTFromModel(const QString& fstPath, const QString& modelFilePath, const hfm::Model& hfmModel); QString absoluteModelPath() const; From 63ea3009217957aff86d0a70593662003658381e Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Sat, 29 Dec 2018 17:24:44 -0800 Subject: [PATCH 34/72] Can get CPU brand. --- .../PlatformInfoScriptingInterface.cpp | 34 +++++++++++++++++++ .../PlatformInfoScriptingInterface.h | 3 ++ 2 files changed, 37 insertions(+) diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp index e2fea92ed3..a6ef660b27 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.cpp +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -21,3 +21,37 @@ QString PlatformInfoScriptingInterface::getOperatingSystemType() { return "UNKNOWN"; #endif } + +QString PlatformInfoScriptingInterface::getCPUBrand() { + int CPUInfo[4] = { -1 }; + unsigned nExIds, i = 0; + char CPUBrandString[0x40]; + // Get the information associated with each extended ID. + __cpuid(CPUInfo, 0x80000000); + nExIds = CPUInfo[0]; + for (i = 0x80000000; i <= nExIds; ++i) { + __cpuid(CPUInfo, i); + // Interpret CPU brand string + if (i == 0x80000002) { + memcpy(CPUBrandString, CPUInfo, sizeof(CPUInfo)); + } else if (i == 0x80000003) { + memcpy(CPUBrandString + 16, CPUInfo, sizeof(CPUInfo)); + } else if (i == 0x80000004) { + memcpy(CPUBrandString + 32, CPUInfo, sizeof(CPUInfo)); + } + } + + return CPUBrandString; + //////string includes manufacturer, model and clockspeed + ////cout << "CPU Type: " << CPUBrandString << endl; + + + ////SYSTEM_INFO sysInfo; + ////GetSystemInfo(&sysInfo); + ////cout << "Number of Cores: " << sysInfo.dwNumberOfProcessors << endl; + + ////MEMORYSTATUSEX statex; + ////statex.dwLength = sizeof (statex); + ////GlobalMemoryStatusEx(&statex); + ////cout << "Total System Memory: " << (statex.ullTotalPhys / 1024) / 1024 << "MB" << endl; +} \ No newline at end of file diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.h b/interface/src/scripting/PlatformInfoScriptingInterface.h index dc88e1963f..d8cba43884 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.h +++ b/interface/src/scripting/PlatformInfoScriptingInterface.h @@ -28,7 +28,10 @@ public slots: /**jsdoc * Returns the CPU brand + *function PlatformInfo.getCPUBrand() + * @returns {string} brand of CPU */ + QString getCPUBrand(); }; #endif // hifi_PlatformInfoScriptingInterface_h From 3a9214747a99f379341b35beb92a71c56309880f Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Sat, 29 Dec 2018 20:49:54 -0800 Subject: [PATCH 35/72] Can read number of logical processors --- .../PlatformInfoScriptingInterface.cpp | 26 +++++++++++++------ .../PlatformInfoScriptingInterface.h | 7 +++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp index a6ef660b27..647262c547 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.cpp +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -7,6 +7,10 @@ // #include "PlatformInfoScriptingInterface.h" +#ifdef Q_OS_WIN +#include <Windows.h> +#endif + PlatformInfoScriptingInterface* PlatformInfoScriptingInterface::getInstance() { static PlatformInfoScriptingInterface sharedInstance; return &sharedInstance; @@ -23,6 +27,7 @@ QString PlatformInfoScriptingInterface::getOperatingSystemType() { } QString PlatformInfoScriptingInterface::getCPUBrand() { +#ifdef Q_OS_WIN int CPUInfo[4] = { -1 }; unsigned nExIds, i = 0; char CPUBrandString[0x40]; @@ -42,16 +47,21 @@ QString PlatformInfoScriptingInterface::getCPUBrand() { } return CPUBrandString; - //////string includes manufacturer, model and clockspeed - ////cout << "CPU Type: " << CPUBrandString << endl; - - - ////SYSTEM_INFO sysInfo; - ////GetSystemInfo(&sysInfo); - ////cout << "Number of Cores: " << sysInfo.dwNumberOfProcessors << endl; - +#else + return "NOT IMPLEMENTED"; +#endif +} ////MEMORYSTATUSEX statex; ////statex.dwLength = sizeof (statex); ////GlobalMemoryStatusEx(&statex); ////cout << "Total System Memory: " << (statex.ullTotalPhys / 1024) / 1024 << "MB" << endl; + +int PlatformInfoScriptingInterface::getNumCores() { +#ifdef Q_OS_WIN + SYSTEM_INFO sysInfo; + GetSystemInfo(&sysInfo); + return sysInfo.dwNumberOfProcessors; +#else + return -1; +#endif } \ No newline at end of file diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.h b/interface/src/scripting/PlatformInfoScriptingInterface.h index d8cba43884..47c8401455 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.h +++ b/interface/src/scripting/PlatformInfoScriptingInterface.h @@ -32,6 +32,13 @@ public slots: * @returns {string} brand of CPU */ QString getCPUBrand(); + + /**jsdoc + * Returns the number of CPU cores + *function PlatformInfo.getNumCores() + * @returns {int} number of CPU cores + */ + int getNumCores(); }; #endif // hifi_PlatformInfoScriptingInterface_h From 6debd996c5a7d02c8e19bec3e28146661e064955 Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Sat, 29 Dec 2018 21:59:18 -0800 Subject: [PATCH 36/72] Added method to get total system memory in MB. --- .../PlatformInfoScriptingInterface.cpp | 21 ++++++++++++------- .../PlatformInfoScriptingInterface.h | 15 +++++++++---- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp index 647262c547..3ffb00e4b8 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.cpp +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -7,6 +7,8 @@ // #include "PlatformInfoScriptingInterface.h" +# include <thread> + #ifdef Q_OS_WIN #include <Windows.h> #endif @@ -34,6 +36,7 @@ QString PlatformInfoScriptingInterface::getCPUBrand() { // Get the information associated with each extended ID. __cpuid(CPUInfo, 0x80000000); nExIds = CPUInfo[0]; + for (i = 0x80000000; i <= nExIds; ++i) { __cpuid(CPUInfo, i); // Interpret CPU brand string @@ -51,16 +54,18 @@ QString PlatformInfoScriptingInterface::getCPUBrand() { return "NOT IMPLEMENTED"; #endif } - ////MEMORYSTATUSEX statex; - ////statex.dwLength = sizeof (statex); - ////GlobalMemoryStatusEx(&statex); - ////cout << "Total System Memory: " << (statex.ullTotalPhys / 1024) / 1024 << "MB" << endl; -int PlatformInfoScriptingInterface::getNumCores() { +unsigned int PlatformInfoScriptingInterface::getNumLogicalCores() { + + return std::thread::hardware_concurrency(); +} + +int PlatformInfoScriptingInterface::getTotalSystemMemoryMB() { #ifdef Q_OS_WIN - SYSTEM_INFO sysInfo; - GetSystemInfo(&sysInfo); - return sysInfo.dwNumberOfProcessors; + MEMORYSTATUSEX statex; + statex.dwLength = sizeof (statex); + GlobalMemoryStatusEx(&statex); + return statex.ullTotalPhys / 1024 / 1024; #else return -1; #endif diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.h b/interface/src/scripting/PlatformInfoScriptingInterface.h index 47c8401455..f0b8122fa5 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.h +++ b/interface/src/scripting/PlatformInfoScriptingInterface.h @@ -34,11 +34,18 @@ public slots: QString getCPUBrand(); /**jsdoc - * Returns the number of CPU cores - *function PlatformInfo.getNumCores() - * @returns {int} number of CPU cores + * Returns the number of logical CPU cores + *function PlatformInfo.getNumLogicalCores() + * @returns {int} number of logical CPU cores */ - int getNumCores(); + unsigned int getNumLogicalCores(); + + /**jsdoc + * Returns the total system memory in megabyte + *function PlatformInfo.getTotalSystemMemory() + * @returns {int} size of memory in megabytes + */ + int getTotalSystemMemoryMB(); }; #endif // hifi_PlatformInfoScriptingInterface_h From 76ce63da64be5360fa25401789b14a6cceacb0e8 Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Sun, 30 Dec 2018 20:24:00 -0800 Subject: [PATCH 37/72] Can read graphics card type. --- interface/src/Application.cpp | 4 ++++ interface/src/Application.h | 3 ++- interface/src/scripting/PlatformInfoScriptingInterface.cpp | 7 ++++++- interface/src/scripting/PlatformInfoScriptingInterface.h | 7 +++++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 56130160c4..503e974f24 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -8931,6 +8931,10 @@ void Application::copyToClipboard(const QString& text) { QApplication::clipboard()->setText(text); } +QString Application::getGraphicsCardType() { + return GPUIdent::getInstance()->getName(); +} + #if defined(Q_OS_ANDROID) void Application::beforeEnterBackground() { auto nodeList = DependencyManager::get<NodeList>(); diff --git a/interface/src/Application.h b/interface/src/Application.h index fd45a594b5..66667518fc 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -459,6 +459,8 @@ public slots: void changeViewAsNeeded(float boomLength); + QString Application::getGraphicsCardType(); + private slots: void onDesktopRootItemCreated(QQuickItem* qmlContext); void onDesktopRootContextCreated(QQmlContext* qmlContext); @@ -787,6 +789,5 @@ private: bool _showTrackedObjects { false }; bool _prevShowTrackedObjects { false }; - }; #endif // hifi_Application_h diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp index 3ffb00e4b8..7f5ee3e08a 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.cpp +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -6,8 +6,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "PlatformInfoScriptingInterface.h" +#include "Application.h" -# include <thread> +#include <thread> #ifdef Q_OS_WIN #include <Windows.h> @@ -69,4 +70,8 @@ int PlatformInfoScriptingInterface::getTotalSystemMemoryMB() { #else return -1; #endif +} + +QString PlatformInfoScriptingInterface::getGraphicsCardType() { + return qApp->getGraphicsCardType(); } \ No newline at end of file diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.h b/interface/src/scripting/PlatformInfoScriptingInterface.h index f0b8122fa5..903658cc20 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.h +++ b/interface/src/scripting/PlatformInfoScriptingInterface.h @@ -46,6 +46,13 @@ public slots: * @returns {int} size of memory in megabytes */ int getTotalSystemMemoryMB(); + + /**jsdoc + * Returns the graphics card type + * @function Test.getGraphicsCardType + * @returns {string} graphics card type + */ + QString getGraphicsCardType(); }; #endif // hifi_PlatformInfoScriptingInterface_h From bb54353644d5d99f4b2415113d26ca8f7884d72b Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Sun, 30 Dec 2018 22:36:15 -0800 Subject: [PATCH 38/72] Compilation error. --- interface/src/Application.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.h b/interface/src/Application.h index a41c9f39f2..f8f78cc46a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -459,7 +459,7 @@ public slots: void changeViewAsNeeded(float boomLength); - QString Application::getGraphicsCardType(); + QString getGraphicsCardType(); private slots: void onDesktopRootItemCreated(QQuickItem* qmlContext); From 9c00bdcddbccb174bb279591b9f916f2eb2ce722 Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Mon, 31 Dec 2018 11:00:29 -0800 Subject: [PATCH 39/72] getCPUBrand works on Mac --- .../PlatformInfoScriptingInterface.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp index 7f5ee3e08a..33b349e032 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.cpp +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -12,6 +12,8 @@ #ifdef Q_OS_WIN #include <Windows.h> +#elif defined Q_OS_MAC +#include <sstream> #endif PlatformInfoScriptingInterface* PlatformInfoScriptingInterface::getInstance() { @@ -51,8 +53,17 @@ QString PlatformInfoScriptingInterface::getCPUBrand() { } return CPUBrandString; -#else - return "NOT IMPLEMENTED"; +#elif defined Q_OS_MAC + FILE* stream = popen("sysctl -n machdep.cpu.brand_string", "r"); + + std::ostringstream hostStream; + while (!feof(stream) && !ferror(stream)) { + char buf[128]; + int bytesRead = fread(buf, 1, 128, stream); + hostStream.write(buf, bytesRead); + } + + return QString::fromStdString(hostStream.str()); #endif } @@ -74,4 +85,4 @@ int PlatformInfoScriptingInterface::getTotalSystemMemoryMB() { QString PlatformInfoScriptingInterface::getGraphicsCardType() { return qApp->getGraphicsCardType(); -} \ No newline at end of file +} From a3dde612d1a4417cafa7b16bc06a1b63b886e676 Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Mon, 31 Dec 2018 11:56:27 -0800 Subject: [PATCH 40/72] Can get total memory on Mac --- .../scripting/PlatformInfoScriptingInterface.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp index 33b349e032..bcf4e79d76 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.cpp +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -78,8 +78,19 @@ int PlatformInfoScriptingInterface::getTotalSystemMemoryMB() { statex.dwLength = sizeof (statex); GlobalMemoryStatusEx(&statex); return statex.ullTotalPhys / 1024 / 1024; -#else - return -1; +#elif defined Q_OS_MAC + FILE* stream = popen("sysctl -a | grep hw.memsize", "r"); + + std::ostringstream hostStream; + while (!feof(stream) && !ferror(stream)) { + char buf[128]; + int bytesRead = fread(buf, 1, 128, stream); + hostStream.write(buf, bytesRead); + } + + QString result = QString::fromStdString(hostStream.str()); + QStringList parts = result.split(' '); + return (int)(parts[1].toDouble() / 1024 / 1024); #endif } From c0a784fa9474ab00e678cb8278ecf3aa05e0dcfb Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Tue, 1 Jan 2019 12:44:09 -0800 Subject: [PATCH 41/72] Get graphics card type works on Mac laptop --- .../PlatformInfoScriptingInterface.cpp | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp index bcf4e79d76..b965eb4331 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.cpp +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -95,5 +95,27 @@ int PlatformInfoScriptingInterface::getTotalSystemMemoryMB() { } QString PlatformInfoScriptingInterface::getGraphicsCardType() { +#ifdef Q_OS_WIN return qApp->getGraphicsCardType(); +#elif defined Q_OS_MAC + FILE* stream = popen("system_profiler SPDisplaysDataType | grep Chipset", "r"); + + std::ostringstream hostStream; + while (!feof(stream) && !ferror(stream)) { + char buf[128]; + int bytesRead = fread(buf, 1, 128, stream); + hostStream.write(buf, bytesRead); + } + + QString result = QString::fromStdString(hostStream.str()); + QStringList parts = result.split('\n'); + for (int i = 0; i < parts.size(); ++i) { + if (parts[i].toLower().contains("radeon") || parts[i].toLower().contains("nvidia")) { + return parts[i]; + } + } + + return "UNKNOWN"; +#endif + } From 56ee91dc606235d7f8230e8f44ca2718d611586f Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Wed, 2 Jan 2019 13:23:20 -0800 Subject: [PATCH 42/72] Fixed Ubuntu warnings. --- .../src/scripting/PlatformInfoScriptingInterface.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp index b965eb4331..bcbf87689e 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.cpp +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -64,6 +64,8 @@ QString PlatformInfoScriptingInterface::getCPUBrand() { } return QString::fromStdString(hostStream.str()); +#else + return QString("NO IMPLEMENTED"); #endif } @@ -91,6 +93,8 @@ int PlatformInfoScriptingInterface::getTotalSystemMemoryMB() { QString result = QString::fromStdString(hostStream.str()); QStringList parts = result.split(' '); return (int)(parts[1].toDouble() / 1024 / 1024); +#else + return -1; #endif } @@ -115,7 +119,9 @@ QString PlatformInfoScriptingInterface::getGraphicsCardType() { } } + // unkown graphics card return "UNKNOWN"; +#else + return QString("NO IMPLEMENTED"); #endif - } From d29233872e147c47983c737b996b781caa425568 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Wed, 2 Jan 2019 14:59:47 -0800 Subject: [PATCH 43/72] Remove avatar packager from Android builds --- interface/src/Menu.cpp | 2 ++ interface/src/avatar/MarketplaceItemUploader.cpp | 14 +++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 8b43753cad..87b1542648 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -150,10 +150,12 @@ Menu::Menu() { qApp, SLOT(packageModel())); // Edit > Avatar Packager +#ifndef Q_OS_ANDROID action = addActionToQMenuAndActionHash(editMenu, MenuOption::AvatarPackager); connect(action, &QAction::triggered, [] { DependencyManager::get<AvatarPackager>()->open(); }); +#endif // Edit > Reload All Content addActionToQMenuAndActionHash(editMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 45a13f04c4..00c8299c62 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -14,11 +14,14 @@ #include <AccountManager.h> #include <DependencyManager.h> -#include <QBuffer> +#ifndef Q_OS_ANDROID #include <quazip5/quazip.h> #include <quazip5/quazipfile.h> +#endif + +#include <QTimer> +#include <QBuffer> -#include <qtimer.h> #include <QFile> #include <QFileInfo> @@ -125,8 +128,12 @@ void MarketplaceItemUploader::doGetCategories() { } void MarketplaceItemUploader::doUploadAvatar() { +#ifdef Q_OS_ANDROID + qWarning() << "Marketplace uploading is not supported on Android"; + setError(Error::Unknown); + return; +#else QBuffer buffer{ &_fileData }; - //buffer.open(QIODevice::WriteOnly); QuaZip zip{ &buffer }; if (!zip.open(QuaZip::Mode::mdAdd)) { qWarning() << "Failed to open zip"; @@ -227,6 +234,7 @@ void MarketplaceItemUploader::doUploadAvatar() { }); setState(State::UploadingAvatar); +#endif } void MarketplaceItemUploader::doWaitForInventory() { From 41effbf8624a1d7ea0d13b18ee0e638b443aa34e Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Wed, 2 Jan 2019 15:01:00 -0800 Subject: [PATCH 44/72] Remove unused variable in MarketplaceItemUploader.cpp --- interface/src/avatar/MarketplaceItemUploader.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 00c8299c62..8b97358ba4 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -285,7 +285,6 @@ void MarketplaceItemUploader::doWaitForInventory() { auto version = assetObject["version"]; auto valid = assetObject["valid"]; if (version.isDouble() && valid.isBool()) { - int versionInt = version.toDouble(); if ((int)version.toDouble() >= _itemVersion && valid.toBool()) { return true; } From dc4be42b207c83fa38983d5c4500ff434e6e7467 Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Wed, 2 Jan 2019 16:14:05 -0800 Subject: [PATCH 45/72] Corrected AWS update for text results. --- tools/nitpick/src/AWSInterface.cpp | 87 ++++++++++++++++++------------ tools/nitpick/src/ui/Nitpick.cpp | 2 +- 2 files changed, 53 insertions(+), 36 deletions(-) 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<void (QProcess::*)(int, QProcess::ExitStatus)>(&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() { From 61ac9d005045c560b324c1c954d1fee711270c37 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Wed, 2 Jan 2019 16:47:09 -0800 Subject: [PATCH 46/72] Update various styling in Avatar Packager --- .../hifi/avatarPackager/AvatarPackagerApp.qml | 59 ++++++++++++++----- .../avatarPackager/AvatarPackagerHeader.qml | 35 ++++------- .../hifi/avatarPackager/AvatarProjectCard.qml | 13 +++- 3 files changed, 69 insertions(+), 38 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index 2b21b4d938..0a678dafee 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -146,6 +146,8 @@ Item { } AvatarPackagerHeader { + z: 100 + id: avatarPackagerHeader colorScheme: root.colorScheme onBackButtonClicked: { @@ -260,22 +262,49 @@ Item { } } - Column { - visible: AvatarPackagerCore.recentProjects.length > 0 - anchors { - fill: parent - topMargin: 18 - leftMargin: 16 - rightMargin: 16 - } - spacing: 10 + Item { + anchors.fill: parent - Repeater { - model: AvatarPackagerCore.recentProjects - AvatarProjectCard { - title: modelData.name - path: modelData.projectPath - onOpen: avatarPackager.openProject(modelData.path) + visible: AvatarPackagerCore.recentProjects.length > 0 + + RalewayRegular { + id: recentProjectsText + + color: 'white' + + visible: AvatarPackagerCore.currentAvatarProject !== null + + 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) + } } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 7037aa9d92..d0b06ea15f 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -2,8 +2,9 @@ import QtQuick 2.6 import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 +import "../avatarapp" 1.0 -Rectangle { +ShadowRectangle { id: root width: parent.width @@ -24,30 +25,21 @@ Rectangle { signal backButtonClicked signal docsButtonClicked - RalewaySemiBold { + RalewayButton { id: back + visible: backButtonEnabled && backButtonVisible + size: 28 anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left anchors.leftMargin: 16 anchors.verticalCenter: back.verticalCenter + text: "◀" - color: textColor - MouseArea { - anchors.fill: parent - onClicked: root.backButtonClicked() - hoverEnabled: true - onEntered: { state = "hovering" } - onExited: { state = "" } - states: [ - State { - name: "hovering" - PropertyChanges { target: back; color: hoverTextColor } - } - ] - } + + onClicked: root.backButtonClicked() } Item { id: titleArea @@ -124,7 +116,7 @@ Rectangle { } } - RalewaySemiBold { + RalewayButton { id: docs visible: false size: 28 @@ -133,13 +125,12 @@ Rectangle { anchors.right: parent.right anchors.rightMargin: 16 anchors.verticalCenter: docs.verticalCenter + text: qsTr("Docs") color: "white" - MouseArea { - anchors.fill: parent - onClicked: { - docsButtonClicked(); - } + + onClicked: { + docsButtonClicked(); } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml index 5496711d44..a758d3936a 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -1,4 +1,5 @@ import QtQuick 2.0 +import QtGraphicalEffects 1.0 import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 @@ -63,7 +64,7 @@ Item { rightMargin: 16 } text: "<title missing>" - size: 16 + size: 24 } RalewayRegular { @@ -88,4 +89,14 @@ Item { onClicked: open() } } + + DropShadow { + id: shadow + anchors.fill: background + radius: 4 + horizontalOffset: 0 + verticalOffset: 4 + color: Qt.rgba(0, 0, 0, 0.25) + source: background + } } From 65d975fe5bf47b65dafdc4f98104dd0448dd0748 Mon Sep 17 00:00:00 2001 From: NissimHadar <nissim.hadar@gmail.com> Date: Wed, 2 Jan 2019 16:53:38 -0800 Subject: [PATCH 47/72] Added detection of Rift and Vive controllers. --- interface/src/Application.cpp | 8 ++++++++ interface/src/Application.h | 4 ++++ .../src/scripting/PlatformInfoScriptingInterface.cpp | 8 ++++++++ .../src/scripting/PlatformInfoScriptingInterface.h | 12 ++++++++++++ 4 files changed, 32 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 06362e8885..0dc7253ba6 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -8711,6 +8711,14 @@ void Application::updateLoginDialogOverlayPosition() { } } +bool Application::hasRiftControllers() { + return PluginUtils::isOculusTouchControllerAvailable(); +} + +bool Application::hasViveControllers() { + return PluginUtils::isViveControllerAvailable(); +} + void Application::onDismissedLoginDialog() { _loginDialogPoppedUp = false; loginDialogPoppedUp.set(false); diff --git a/interface/src/Application.h b/interface/src/Application.h index b70d5e72b9..dc30c3c22c 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -326,6 +326,10 @@ public: void createLoginDialogOverlay(); void updateLoginDialogOverlayPosition(); + // Check if a headset is connected + bool hasRiftControllers(); + bool hasViveControllers(); + #if defined(Q_OS_ANDROID) void beforeEnterBackground(); void enterBackground(); diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp index bcbf87689e..b6e4df0d40 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.cpp +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -125,3 +125,11 @@ QString PlatformInfoScriptingInterface::getGraphicsCardType() { return QString("NO IMPLEMENTED"); #endif } + +bool PlatformInfoScriptingInterface::hasRiftControllers() { + return qApp->hasRiftControllers(); +} + +bool PlatformInfoScriptingInterface::hasViveControllers() { + return qApp->hasViveControllers(); +} diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.h b/interface/src/scripting/PlatformInfoScriptingInterface.h index 903658cc20..3ed57965c9 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.h +++ b/interface/src/scripting/PlatformInfoScriptingInterface.h @@ -53,6 +53,18 @@ public slots: * @returns {string} graphics card type */ QString getGraphicsCardType(); + + /**jsdoc + * Returns true if Oculus Rift is connected (looks for hand controllers) + * @function Window.hasRift + * @returns {boolean} <code>true</code> if running on Windows, otherwise <code>false</code>.*/ + bool hasRiftControllers(); + + /**jsdoc + * Returns true if HTC Vive is connected (looks for hand controllers) + * @function Window.hasRift + * @returns {boolean} <code>true</code> if running on Windows, otherwise <code>false</code>.*/ + bool hasViveControllers(); }; #endif // hifi_PlatformInfoScriptingInterface_h From 0e89e37c8edb94ca2131cfd71c2914da13278b23 Mon Sep 17 00:00:00 2001 From: David Back <davidback@highfidelity.io> Date: Wed, 2 Jan 2019 17:40:00 -0800 Subject: [PATCH 48/72] fix thumb rotations, fix text height, adjust readme --- .gitignore | 1 + .../unity-avatar-exporter/Assets/Editor.meta | 2 +- .../Assets/Editor/AvatarExporter.cs | 14 ++++++++------ .../Assets/Editor/AvatarExporter.cs.meta | 2 +- tools/unity-avatar-exporter/Assets/README.txt | 14 +++++++++----- .../Assets/README.txt.meta | 2 +- .../avatarExporter.unitypackage | Bin 8711 -> 8794 bytes tools/unity-avatar-exporter/packager.bat | 2 +- 8 files changed, 22 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index bbb79ad6a9..f5605d7090 100644 --- a/.gitignore +++ b/.gitignore @@ -98,6 +98,7 @@ tools/jsdoc/package-lock.json # ignore unneeded unity project files for avatar exporter tools/unity-avatar-exporter/Library +tools/unity-avatar-exporter/Logs tools/unity-avatar-exporter/Packages tools/unity-avatar-exporter/ProjectSettings tools/unity-avatar-exporter/Temp diff --git a/tools/unity-avatar-exporter/Assets/Editor.meta b/tools/unity-avatar-exporter/Assets/Editor.meta index aac82b4258..cf7dcf12dd 100644 --- a/tools/unity-avatar-exporter/Assets/Editor.meta +++ b/tools/unity-avatar-exporter/Assets/Editor.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 51b3237a2992bd449a58ade16e52d0e0 +guid: 02111c50e71dd664da8ad5c6a6eca767 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs index 18916267f0..b6470a7551 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs @@ -85,9 +85,9 @@ class AvatarExporter : MonoBehaviour { {"LeftHandRing3", new Quaternion(-0.4936301f, 0.5097645f, -0.5061787f, -0.4901562f)}, {"LeftHandRing2", new Quaternion(-0.5089865f, 0.4943658f, -0.4909532f, -0.5054707f)}, {"LeftHandRing1", new Quaternion(-0.5020972f, 0.5005084f, -0.4979034f, -0.4994819f)}, - {"LeftHandThumb3", new Quaternion(-0.7228092f, 0.2988393f, -0.4472938f, -0.4337862f)}, - {"LeftHandThumb2", new Quaternion(-0.7554525f, 0.2018595f, -0.3871402f, -0.4885356f)}, - {"LeftHandThumb1", new Quaternion(-0.7276843f, 0.2878546f, -0.439926f, -0.4405459f)}, + {"LeftHandThumb3", new Quaternion(-0.6617184f, 0.2884935f, -0.3604706f, -0.5907297f)}, + {"LeftHandThumb2", new Quaternion(-0.6935627f, 0.1995147f, -0.2805665f, -0.6328092f)}, + {"LeftHandThumb1", new Quaternion(-0.6663674f, 0.278572f, -0.3507071f, -0.5961183f)}, {"LeftEye", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)}, {"LeftFoot", new Quaternion(0.009215056f, 0.3612514f, 0.9323555f, -0.01121602f)}, {"LeftHand", new Quaternion(-0.4797408f, 0.5195366f, -0.5279632f, -0.4703038f)}, @@ -110,9 +110,9 @@ class AvatarExporter : MonoBehaviour { {"RightHandRing3", new Quaternion(0.4933217f, 0.5102056f, -0.5064691f, 0.4897075f)}, {"RightHandRing2", new Quaternion(0.5085972f, 0.494844f, -0.4913519f, 0.505007f)}, {"RightHandRing1", new Quaternion(0.502959f, 0.4996676f, -0.4970418f, 0.5003144f)}, - {"RightHandThumb3", new Quaternion(0.7221864f, 0.3001843f, -0.4482129f, 0.4329457f)}, - {"RightHandThumb2", new Quaternion(0.755621f, 0.20102f, -0.386691f, 0.4889769f)}, - {"RightHandThumb1", new Quaternion(0.7277303f, 0.2876409f, -0.4398623f, 0.4406733f)}, + {"RightHandThumb3", new Quaternion(0.6611374f, 0.2896575f, -0.3616535f, 0.5900872f)}, + {"RightHandThumb2", new Quaternion(0.6937408f, 0.1986776f, -0.279922f, 0.6331626f)}, + {"RightHandThumb1", new Quaternion(0.6664271f, 0.2783172f, -0.3505667f, 0.596253f)}, {"RightEye", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)}, {"RightFoot", new Quaternion(-0.009482829f, 0.3612484f, 0.9323512f, 0.01144584f)}, {"RightHand", new Quaternion(0.4797273f, 0.5195542f, -0.5279628f, 0.4702987f)}, @@ -482,6 +482,7 @@ class ExportProjectWindow : EditorWindow { const int BUTTON_FONT_SIZE = 16; const int LABEL_FONT_SIZE = 16; const int TEXT_FIELD_FONT_SIZE = 14; + const int TEXT_FIELD_HEIGHT = 20; const int ERROR_FONT_SIZE = 12; string projectName = ""; @@ -508,6 +509,7 @@ class ExportProjectWindow : EditorWindow { labelStyle.fontSize = LABEL_FONT_SIZE; GUIStyle textStyle = new GUIStyle(GUI.skin.textField); textStyle.fontSize = TEXT_FIELD_FONT_SIZE; + textStyle.fixedHeight = TEXT_FIELD_HEIGHT; GUIStyle errorStyle = new GUIStyle(GUI.skin.label); errorStyle.fontSize = ERROR_FONT_SIZE; errorStyle.normal.textColor = Color.red; diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs.meta b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs.meta index c71e4c396d..373aecc6a8 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs.meta +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: c7a34be82b3ae554ea097963914b083f +guid: 00403fdc52187214c8418bc0a7f387e2 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index 034ec23982..3ca4dbb1ee 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,15 +1,19 @@ +Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter. + To create a new avatar project: -1. Import your .fbx avatar model into Unity Assets (drag and drop file into Assets window or use Assets menu > Import New Assets). -2. Select the .fbx avatar that you imported in the Assets window, and in the Rig section of the Inspector window set the Animation Type to Humanoid and choose Apply. -3. With the .fbx avatar still selected, select High Fidelity menu > Export New Avatar. +1. Import your .fbx avatar model into your Unity project's Assets by either dragging and dropping the file into the Assets window or by using Assets menu > Import New Assets. +2. Select the .fbx avatar that you imported in step 1 in the Assets window, and in the Rig section of the Inspector window set the Animation Type to Humanoid and choose Apply. +3. With the .fbx avatar still selected in the Assets window, choose High Fidelity menu > Export New Avatar. 4. Select a name for your avatar project (this will be used to create a directory with that name), as well as the target location for your project folder. 5. Once it is exported, your project directory will open in File Explorer. To update an existing avatar project: 1. Select the existing .fbx avatar in the Assets window that you would like to re-export. -2. Select High Fidelity menu > Update Existing Avatar and choose the .fst file you would like to update. -3. If the .fbx file in your Unity Assets folder is newer than the existing .fbx file in your avatar project or vice-versa, you will be prompted if you wish to replace the older file with the newer file. +2. Choose High Fidelity menu > Update Existing Avatar and browse to the .fst file you would like to update. +3. If the .fbx file in your Unity Assets folder is newer than the existing .fbx file in your selected avatar project or vice-versa, you will be prompted if you wish to replace the older file with the newer file before performing the update. 4. Once it is updated, your project directory will open in File Explorer. * WARNING * If you are using any external textures as part of your .fbx model, be sure they are copied into the textures folder that is created in the project folder after exporting a new avatar. + +For further details including troubleshooting tips, see the full documentation at https://docs.highfidelity.com/create-and-explore/avatars/create-avatars/unity-extension diff --git a/tools/unity-avatar-exporter/Assets/README.txt.meta b/tools/unity-avatar-exporter/Assets/README.txt.meta index d8bc5b9b66..148fd21fdd 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt.meta +++ b/tools/unity-avatar-exporter/Assets/README.txt.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 71e72751b2810fc4993ff53291c430b6 +guid: 30b2b6221fd08234eb07c4d6d525d32e TextScriptImporter: externalObjects: {} userData: diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index f333aecb1299cfa8553a0d4a8d9d657e9ef36bec..28052efea5971fbd5c100a1b48b25c69d5ba1ef0 100644 GIT binary patch literal 8794 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 Q&%fmP6Y8m0TmZ5F066O3H2?qr literal 8711 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 diff --git a/tools/unity-avatar-exporter/packager.bat b/tools/unity-avatar-exporter/packager.bat index 55b59a9db6..66629783af 100644 --- a/tools/unity-avatar-exporter/packager.bat +++ b/tools/unity-avatar-exporter/packager.bat @@ -1 +1 @@ -"C:\Program Files\Unity\Editor\Unity.exe" -quit -batchmode -projectPath %CD% -exportPackage "Assets" "avatarExporter.unitypackage" +Unity -quit -batchmode -projectPath %CD% -exportPackage "Assets" "avatarExporter.unitypackage" From 9c96f7bd01107a13c879bd5b88d6357668f3adfe Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 3 Jan 2019 08:21:52 -0800 Subject: [PATCH 49/72] Add RalewayButton --- .../qml/hifi/avatarPackager/RalewayButton.qml | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 interface/resources/qml/hifi/avatarPackager/RalewayButton.qml diff --git a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml new file mode 100644 index 0000000000..e7134b6934 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml @@ -0,0 +1,68 @@ +import QtQuick 2.6 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +import TabletScriptingInterface 1.0 + +RalewaySemiBold { + id: root + + text: "no text" + + signal clicked() + + color: "white" + + property var hoverColor: "#AFAFAF" + property var pressedColor: "#575757" + + MouseArea { + id: mouseArea + + anchors.fill: parent + + hoverEnabled: true + + onClicked: { + 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 + PropertyChanges { target: root; color: pressedColor } + StateChangeScript { + script: { + mouseArea.lastState = mouseArea.state + } + } + }, + State { + name: "hovering" + when: mouseArea.containsMouse + PropertyChanges { target: root; color: hoverColor } + StateChangeScript { + script: { + if (mouseArea.lastState == "") { + Tablet.playSound(TabletEnums.ButtonHover); + } + mouseArea.lastState = mouseArea.state + } + } + } + ] + } +} \ No newline at end of file From c13badcbc8f32eec08fb5456187c44e58a215933 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 3 Jan 2019 10:20:13 -0800 Subject: [PATCH 50/72] Fix project rename focus and create project not initializing --- .../hifi/avatarPackager/AvatarPackagerApp.qml | 3 +- .../avatarPackager/AvatarPackagerHeader.qml | 8 +-- .../qml/hifi/avatarPackager/ClickableArea.qml | 63 +++++++++++++++++++ .../avatarPackager/CreateAvatarProject.qml | 1 + .../qml/hifi/avatarPackager/InfoBox.qml | 6 ++ .../qml/hifi/avatarPackager/RalewayButton.qml | 56 +++-------------- 6 files changed, 80 insertions(+), 57 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/ClickableArea.qml diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index 0a678dafee..6e2e352b51 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -22,6 +22,7 @@ Item { MouseArea { anchors.fill: parent + onClicked: { unfocusser.forceActiveFocus(); } @@ -272,8 +273,6 @@ Item { color: 'white' - visible: AvatarPackagerCore.currentAvatarProject !== null - anchors.top: parent.top anchors.left: parent.left anchors.topMargin: 16 diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index d0b06ea15f..b40e9cc264 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -92,8 +92,7 @@ ShadowRectangle { font.pixelSize: 28 z: 200 onFocusChanged: { - if (titleArea.state === "renaming" && !titleArea.focus) { - //titleArea.state = ""; + if (titleArea.state === "renaming" && !focus) { accepted(); } } @@ -104,11 +103,7 @@ ShadowRectangle { } onAccepted: { if (acceptableInput) { - //AvatarPackagerCore.renameProject(text); - console.warn(text); AvatarPackagerCore.currentAvatarProject.name = text; - console.warn(AvatarPackagerCore.currentAvatarProject.name); - } titleArea.state = ""; } @@ -127,7 +122,6 @@ ShadowRectangle { anchors.verticalCenter: docs.verticalCenter text: qsTr("Docs") - color: "white" onClicked: { docsButtonClicked(); diff --git a/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml b/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml new file mode 100644 index 0000000000..f49c98bc6a --- /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 + } + } + } + ] + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml index d6f530a196..b72cbd42df 100644 --- a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -28,6 +28,7 @@ Item { Window.alert('Failed to create project'); return; } + avatarProject.reset(); avatarPackager.state = AvatarPackagerState.project; } } diff --git a/interface/resources/qml/hifi/avatarPackager/InfoBox.qml b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml index 89f5d5c7f8..4c7718c2ac 100644 --- a/interface/resources/qml/hifi/avatarPackager/InfoBox.qml +++ b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml @@ -17,6 +17,12 @@ Rectangle { property bool closeOnClickOutside: false; + onVisibleChanged: { + if (visible) { + focus = true; + } + } + function open() { visible = true; } diff --git a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml index e7134b6934..edbc31b24f 100644 --- a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml +++ b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml @@ -8,61 +8,21 @@ import TabletScriptingInterface 1.0 RalewaySemiBold { id: root - text: "no text" - - signal clicked() - - color: "white" + anchors.fill: textItem + property var idleColor: "white" property var hoverColor: "#AFAFAF" property var pressedColor: "#575757" - MouseArea { - id: mouseArea + color: clickable.hovered ? root.hoverColor : (clickable.pressed ? root.pressedColor : root.idleColor); - anchors.fill: parent + signal clicked() - hoverEnabled: true + ClickableArea { + id: clickable - onClicked: { - Tablet.playSound(TabletEnums.ButtonClick); - root.clicked() - } + anchors.fill: root - property string lastState: "" - - states: [ - State { - name: "" - StateChangeScript { - script: { - mouseArea.lastState = mouseArea.state - } - } - }, - State { - name: "pressed" - when: mouseArea.containsMouse && mouseArea.pressed - PropertyChanges { target: root; color: pressedColor } - StateChangeScript { - script: { - mouseArea.lastState = mouseArea.state - } - } - }, - State { - name: "hovering" - when: mouseArea.containsMouse - PropertyChanges { target: root; color: hoverColor } - StateChangeScript { - script: { - if (mouseArea.lastState == "") { - Tablet.playSound(TabletEnums.ButtonHover); - } - mouseArea.lastState = mouseArea.state - } - } - } - ] + onClicked: root.clicked() } } \ No newline at end of file From 7953507cb8dbb1b06abfd471557124dcfe3cd7ad Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 3 Jan 2019 11:59:16 -0800 Subject: [PATCH 51/72] Update avatar project rename to have audio and highlighting --- .../hifi/avatarPackager/AvatarPackagerApp.qml | 7 ++--- .../avatarPackager/AvatarPackagerHeader.qml | 29 +++++++++++++++---- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index 6e2e352b51..ee78f4f934 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -11,15 +11,12 @@ import "../../dialogs" import "../avatarapp" 1.0 as AvatarApp Item { + id: windowContent + HifiConstants { id: hifi } property alias desktopObject: avatarPackager.desktopObject - id: windowContent - // height: pane ? pane.height : parent.width - // width: pane ? pane.width : parent.width - - MouseArea { anchors.fill: parent diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index b40e9cc264..845fdeb99f 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -11,7 +11,7 @@ ShadowRectangle { height: 74 color: "#252525" - property alias title: title.text + 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 @@ -43,6 +43,7 @@ ShadowRectangle { } Item { id: titleArea + anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: root.backButtonVisible ? back.right : parent.left @@ -57,15 +58,31 @@ ShadowRectangle { } ] - RalewaySemiBold { + Item { id: title - size: 28 anchors.fill: parent - text: qsTr("Avatar Packager") - color: "white" - MouseArea { + 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; From ecc578c2dd4979a97cc4505a9e407178e4971693 Mon Sep 17 00:00:00 2001 From: Thijs Wenker <me@thoys.nl> Date: Thu, 3 Jan 2019 22:00:22 +0100 Subject: [PATCH 52/72] - error messages - style changes --- .../qml/hifi/AvatarPackagerWindow.qml | 8 -- .../hifi/avatarPackager/AvatarPackagerApp.qml | 114 +++++++++++++++--- .../avatarPackager/AvatarPackagerFooter.qml | 3 +- .../avatarPackager/AvatarPackagerHeader.qml | 5 - .../qml/hifi/avatarPackager/AvatarProject.qml | 6 +- .../avatarPackager/AvatarProjectUpload.qml | 5 +- .../avatarPackager/CreateAvatarProject.qml | 7 +- .../qml/hifi/avatarPackager/InfoBox.qml | 18 +-- interface/src/avatar/AvatarPackager.cpp | 54 +++++---- interface/src/avatar/AvatarPackager.h | 8 +- interface/src/avatar/AvatarProject.cpp | 65 ++++++++-- interface/src/avatar/AvatarProject.h | 32 ++++- 12 files changed, 242 insertions(+), 83 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackagerWindow.qml b/interface/resources/qml/hifi/AvatarPackagerWindow.qml index 9d434ef97c..82bcd3fa40 100644 --- a/interface/resources/qml/hifi/AvatarPackagerWindow.qml +++ b/interface/resources/qml/hifi/AvatarPackagerWindow.qml @@ -1,15 +1,7 @@ import QtQuick 2.6 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQml.Models 2.1 -import QtGraphicalEffects 1.0 -import "../controlsUit" 1.0 as HifiControls import "../stylesUit" 1.0 import "../windows" as Windows -import "../controls" 1.0 -import "../dialogs" import "avatarPackager" 1.0 -import "avatarapp" 1.0 as AvatarApp Windows.ScrollingWindow { id: root diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index 0a678dafee..fb7987ca76 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -3,9 +3,9 @@ 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 "../../windows" as Windows import "../../controls" 1.0 import "../../dialogs" import "../avatarapp" 1.0 as AvatarApp @@ -16,9 +16,6 @@ Item { property alias desktopObject: avatarPackager.desktopObject id: windowContent - // height: pane ? pane.height : parent.width - // width: pane ? pane.width : parent.width - MouseArea { anchors.fill: parent @@ -75,6 +72,37 @@ Item { } } + 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 @@ -85,8 +113,8 @@ Item { // This mouse area captures the cursor events while the modalOverlay is active MouseArea { anchors.fill: parent - propagateComposedEvents: false; - hoverEnabled: true; + propagateComposedEvents: false + hoverEnabled: true } } @@ -133,12 +161,58 @@ Item { property var desktopObject: desktop function openProject(path) { - let project = AvatarPackagerCore.openAvatarProject(path); - if (project) { - avatarProject.reset(); - avatarPackager.state = AvatarPackagerState.project; + let status = AvatarPackagerCore.openAvatarProject(path); + if (status !== AvatarProjectStatus.SUCCESS) { + displayErrorMessage(status); + return status; } - return project; + 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 during the creation of the Avatar Project directories. Please select a project location with write permissions."); + 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 if it exist 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. Please verify if you have read permissions at the specified location."); + 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 verify if the model file is supported by High Fidelity."); + 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 (avatar.fst) in the folder. Please locate it and move to the project folder."); + break; + case AvatarProjectStatus.ERROR_OPEN_OPEN_FST: + errorPopup.show("File Read Error", "We cannot read the project file (avatar.fst). Please make sure that it is not in use by another program."); + break; + case AvatarProjectStatus.ERROR_OPEN_FIND_MODEL: + errorPopup.show("File Missing", "We cannot find the avatar model file (.fbx) in the folder. Please locate it and move to the project folder."); + break; + default: + errorPopup.show("Error Message Missing", "Error message missing for status " + status); + } + } function openDocs() { @@ -233,10 +307,8 @@ Item { browser.selectedFile.connect(function(fileUrl) { let fstFilePath = fileDialogHelper.urlToPath(fileUrl); - let currentAvatarProject = avatarPackager.openProject(fstFilePath); - if (currentAvatarProject) { - avatarPackager.showModalOverlay = false; - } + avatarPackager.showModalOverlay = false; + avatarPackager.openProject(fstFilePath); }); } } @@ -253,12 +325,20 @@ Item { RalewayRegular { size: 20 color: "white" - text: qsTr("Use a custom avatar to express your identity") + text: "Use a custom avatar of your choice." + width: parent.width + wrapMode: Text.WordWrap } RalewayRegular { size: 20 color: "white" - text: qsTr("To learn more about using this tool, visit our docs") + text: "<a href='javascript:void'>Visit our docs</a> to learn more about using the packager." + linkColor: "#00B4EF" + width: parent.width + wrapMode: Text.WordWrap + onLinkActivated: { + avatarPackager.openDocs(); + } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml index e1d9396e04..31e05672d2 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml @@ -38,5 +38,4 @@ Rectangle { width: parent.width } } - -} \ No newline at end of file +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index d0b06ea15f..cd6fdb72fe 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -93,7 +93,6 @@ ShadowRectangle { z: 200 onFocusChanged: { if (titleArea.state === "renaming" && !titleArea.focus) { - //titleArea.state = ""; accepted(); } } @@ -104,11 +103,7 @@ ShadowRectangle { } onAccepted: { if (acceptableInput) { - //AvatarPackagerCore.renameProject(text); - console.warn(text); AvatarPackagerCore.currentAvatarProject.name = text; - console.warn(AvatarPackagerCore.currentAvatarProject.name); - } titleArea.state = ""; } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index a5a2263346..8f7a4be481 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -52,7 +52,7 @@ Item { colorScheme: root.colorScheme width: 133 height: 40 - onClicked: function() { + onClicked: { uploadNew(); } } @@ -70,7 +70,7 @@ Item { colorScheme: root.colorScheme width: 134 height: 40 - onClicked: function() { + onClicked: { showConfirmUploadPopup(uploadNew, uploadUpdate); } } @@ -90,7 +90,7 @@ Item { colorScheme: root.colorScheme width: 134 height: 40 - onClicked: function() { + onClicked: { showConfirmUploadPopup(uploadNew, uploadUpdate); } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index c1d1a98158..68f465f514 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -29,10 +29,11 @@ Item { function stateChangedCallback(newState) { if (newState >= 4) { - root.uploader.stateChanged.disconnect(stateChangedCallback) + root.uploader.stateChanged.disconnect(stateChangedCallback); backToProjectTimer.start(); } } + onVisibleChanged: { if (visible) { root.uploader.stateChanged.connect(stateChangedCallback); @@ -120,6 +121,7 @@ Item { source: "../../../icons/checkmark-stroke.svg" } } + Item { id: statusRows @@ -169,6 +171,7 @@ Item { color: "white" text: "We couldn't upload your avatar at this time. Please try again later." } + AvatarPackagerFooter { id: errorFooter diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml index d6f530a196..1e9a3d9e84 100644 --- a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -2,6 +2,8 @@ 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 @@ -24,8 +26,9 @@ Item { text: qsTr("Create") enabled: false onClicked: { - if (!AvatarPackagerCore.createAvatarProject(projectLocation.text, name.text, avatarModel.text, textureFolder.text)) { - Window.alert('Failed to create project'); + let status = AvatarPackagerCore.createAvatarProject(projectLocation.text, name.text, avatarModel.text, textureFolder.text); + if (status !== AvatarProjectStatus.SUCCESS) { + avatarPackager.displayErrorMessage(status); return; } avatarPackager.state = AvatarPackagerState.project; diff --git a/interface/resources/qml/hifi/avatarPackager/InfoBox.qml b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml index 89f5d5c7f8..301386acfa 100644 --- a/interface/resources/qml/hifi/avatarPackager/InfoBox.qml +++ b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml @@ -5,9 +5,9 @@ import controlsUit 1.0 as HifiControlsUit import "../../controls" as HifiControls Rectangle { - id: root; - visible: false; - color: Qt.rgba(.34, .34, .34, 0.6); + id: root + visible: false + color: Qt.rgba(.34, .34, .34, 0.6) z: 999; anchors.fill: parent @@ -17,6 +17,9 @@ Rectangle { property bool closeOnClickOutside: false; + property alias boxWidth: mainContainer.width + property alias boxHeight: mainContainer.height + function open() { visible = true; } @@ -44,15 +47,15 @@ Rectangle { } Rectangle { - id: mainContainer; + id: mainContainer width: Math.max(parent.width * 0.8, 400) height: parent.height * 0.6 MouseArea { - anchors.fill: parent; - propagateComposedEvents: false; - hoverEnabled: true; + anchors.fill: parent + propagateComposedEvents: false + hoverEnabled: true onClicked: function(ev) { ev.accepted = true; } @@ -107,6 +110,5 @@ Rectangle { colorScheme: hifi.colorSchemes.dark; } } - } } diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index eef574a8b5..941aff6943 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -22,7 +22,6 @@ #include <avatar/MarketplaceItemUploader.h> #include <mutex> -#include "scripting/HMDScriptingInterface.h" #include "ui/TabletScriptingInterface.h" std::once_flag setupQMLTypesFlag; @@ -32,6 +31,14 @@ AvatarPackager::AvatarPackager() { qmlRegisterType<MarketplaceItemUploader>(); qRegisterMetaType<AvatarPackager*>(); qRegisterMetaType<AvatarProject*>(); + qRegisterMetaType<AvatarProjectStatus::AvatarProjectStatus>(); + qmlRegisterUncreatableMetaObject( + AvatarProjectStatus::staticMetaObject, + "Hifi.AvatarPackager.AvatarProjectStatus", + 1, 0, + "AvatarProjectStatus", + "Error: only enums" + ); }); recentProjectsFromVariantList(_recentProjectsSetting.get()); @@ -41,39 +48,39 @@ AvatarPackager::AvatarPackager() { } bool AvatarPackager::open() { - static const QUrl url{ "hifi/AvatarPackagerWindow.qml" }; - const auto packageModelDialogCreated = [=](QQmlContext* context, QObject* newObject) { context->setContextProperty("AvatarPackagerCore", this); }; static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; - auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>(); - auto tablet = dynamic_cast<TabletProxy*>(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - auto hmd = DependencyManager::get<HMDScriptingInterface>(); + auto tablet = dynamic_cast<TabletProxy*>(DependencyManager::get<TabletScriptingInterface>()->getTablet(SYSTEM_TABLET)); if (tablet->getToolbarMode()) { + static const QUrl url{ "hifi/AvatarPackagerWindow.qml" }; DependencyManager::get<OffscreenUi>()->show(url, "AvatarPackager", packageModelDialogCreated); - } else { - static const QUrl url("hifi/tablet/AvatarPackager.qml"); - if (!tablet->isPathLoaded(url)) { - tablet->getTabletSurface()->getSurfaceContext()->setContextProperty("AvatarPackagerCore", this); - tablet->pushOntoStack(url); - } + return true; } - 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<RecentAvatarProject>(); - for (auto project : _recentProjects) { + for (const auto& project : _recentProjects) { if (project.getProjectFSTPath() == fstPath) { removeProjects.append(project); } } - for (const auto removeProject : removeProjects) { + for (const auto& removeProject : removeProjects) { _recentProjects.removeOne(removeProject); } @@ -110,14 +117,19 @@ void AvatarPackager::recentProjectsFromVariantList(QVariantList projectsVariant) } } -AvatarProject* AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) { - setAvatarProject(AvatarProject::openAvatarProject(avatarProjectFSTPath)); - return _currentAvatarProject; +AvatarProjectStatus::AvatarProjectStatus AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) { + AvatarProjectStatus::AvatarProjectStatus status; + setAvatarProject(AvatarProject::openAvatarProject(avatarProjectFSTPath, status)); + return status; } -AvatarProject* AvatarPackager::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder) { - setAvatarProject(AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder)); - return _currentAvatarProject; +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) { diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index 343176497f..c9c5b9d312 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -63,8 +63,12 @@ public: AvatarPackager(); bool open(); - Q_INVOKABLE AvatarProject* createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder); - Q_INVOKABLE AvatarProject* openAvatarProject(const QString& avatarProjectFSTPath); + 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) { return AvatarProject::isValidNewProjectName(projectPath, projectName); } signals: diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 0c29bcf906..e049dc0ba3 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -22,55 +22,98 @@ #include <ui/TabletScriptingInterface.h> #include "scripting/HMDScriptingInterface.h" -AvatarProject* AvatarProject::openAvatarProject(const QString& path) { +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; } - QFile file{ path }; + + 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) { +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); - QFile fbx(fbxInfo.filePath()); - if (!fbxInfo.exists() || !fbxInfo.isFile() || !fbx.open(QIODevice::ReadOnly)) { - // TODO: Can't open model FBX (throw error here) + 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<hfm::Model> hfmModel; try { - qDebug() << "Reading FBX file : " << fbxInfo.filePath(); const QByteArray fbxContents = fbx.readAll(); hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath()); } catch (const QString& error) { - qDebug() << "Error reading: " << error; + Q_UNUSED(error) + status = AvatarProjectStatus::ERROR_CREATE_READ_MODEL; return nullptr; } QStringList textures{}; @@ -111,9 +154,11 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, fst->setName(avatarProjectName); if (!fst->write()) { + status = AvatarProjectStatus::ERROR_CREATE_WRITE_FST; return nullptr; } + status = AvatarProjectStatus::SUCCESS; return new AvatarProject(fst); } @@ -157,7 +202,7 @@ AvatarProject::AvatarProject(FST* fst) { _projectPath = fileInfo.absoluteDir().absolutePath(); } -void AvatarProject::appendDirectory(QString prefix, QDir dir) { +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()) { diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 469324004d..c422c14d62 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -22,6 +22,27 @@ #include <QVariantHash> #include <QStandardPaths> +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) @@ -48,16 +69,19 @@ public: } Q_INVOKABLE QString getProjectPath() const { return _projectPath; } Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); } - Q_INVOKABLE QString getFBXPath() const { return _fst->getModelPath(); } + 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); + static AvatarProject* openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status); static AvatarProject* createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, - const QString& textureFolder); + const QString& textureFolder, + AvatarProjectStatus::AvatarProjectStatus& status); static bool isValidNewProjectName(const QString& projectPath, const QString& projectName); @@ -78,7 +102,7 @@ private: FST* getFST() { return _fst; } void refreshProjectFiles(); - void appendDirectory(QString prefix, QDir dir); + void appendDirectory(const QString& prefix, const QDir& dir); QStringList getScriptPaths(const QDir& scriptsDir); FST* _fst; From 7ca7e0f2345744c86af72805dece85de57744935 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 3 Jan 2019 13:45:00 -0800 Subject: [PATCH 53/72] Revert unintended changes in avatar packager --- interface/src/Application.cpp | 2 +- interface/src/avatar/AvatarManager.cpp | 4 ++-- scripts/system/html/js/entityProperties.js | 9 --------- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b8324874b1..a0edc7f5ff 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -466,7 +466,7 @@ public: // Don't actually crash in debug builds, in case this apparent deadlock is simply from // the developer actively debugging code #ifdef NDEBUG - //deadlockDetectionCrash(); + deadlockDetectionCrash(); #endif } } diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 1a6b510ea1..7ca18ca258 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -536,7 +536,6 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents // my avatar. (Other user machines will make a similar analysis and inject sound for their collisions.) if (collision.idA.isNull() || collision.idB.isNull()) { auto myAvatar = getMyAvatar(); - myAvatar->collisionWithEntity(collision); auto collisionSound = myAvatar->getCollisionSound(); if (collisionSound) { const auto characterController = myAvatar->getCharacterController(); @@ -572,8 +571,9 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents auto injector = AudioInjector::playSoundAndDelete(collisionSound, options); _collisionInjectors.emplace_back(injector); } + myAvatar->collisionWithEntity(collision); + return; } - return; } } } diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index d4b60f1814..78ef8ac313 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -112,15 +112,6 @@ const GROUPS = [ type: "color", propertyID: "color", }, - { - label: "Alpha", - type: "", - type: "number", - min: 0, - max: 1, - step: 0.001, - propertyID: "alpha", - }, ] }, { From 847496048fda4ecc623028efda5490bb4dd88d75 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 3 Jan 2019 13:51:45 -0800 Subject: [PATCH 54/72] Fix waiting-for-inventory sticking around on avatar uploader --- .../qml/hifi/avatarPackager/AvatarProject.qml | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index a5a2263346..7a0f7b6750 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -17,7 +17,7 @@ Item { Style { id: style } property int colorScheme; - property var uploader: undefined; + property var uploader: null; property bool hasSuccessfullyUploaded: true; @@ -25,7 +25,10 @@ Item { anchors.fill: parent anchors.margins: 10 - function reset() { hasSuccessfullyUploaded = false } + function reset() { + hasSuccessfullyUploaded = false; + uploader = null; + } property var footer: Item { anchors.fill: parent @@ -151,32 +154,22 @@ Item { } function uploadNew() { - console.log("Uploading new"); upload(false); } function uploadUpdate() { - console.log("Uploading update"); 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.uploadProgress.connect(function(uploaded, total) { - console.log("Uploader progress: " + uploaded + " / " + total); - }); - root.uploader.completed.connect(function() { - root.hasSuccessfullyUploaded = true; - }); - root.uploader.finishedChanged.connect(function() { - try { - var response = JSON.parse(root.uploader.responseData); - console.log("Uploader complete! " + response); - uploadStatus.text = response.status; - } catch (e) { - console.log("Error parsing JSON: " + root.uploader.reponseData); - } - }); root.uploader.send(); avatarPackager.state = AvatarPackagerState.projectUpload; } From 0e6eecc64b3232aa7961a52f0b54906a951a9c17 Mon Sep 17 00:00:00 2001 From: Sam Gondelman <samuel_gondelman@alumni.brown.edu> Date: Thu, 3 Jan 2019 15:20:55 -0800 Subject: [PATCH 55/72] Update PacketHeaders.h --- libraries/networking/src/udt/PacketHeaders.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 765d320490..1b031e80dc 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -252,7 +252,7 @@ enum class EntityVersion : PacketVersion { GridEntities, MissingTextProperties, GrabTraits, - MorePropertiesCleanup + MorePropertiesCleanup, UpdatedPolyLines }; From f27ee1767cca4b790734473246239ce9b8b44d81 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 08:43:43 -0800 Subject: [PATCH 56/72] Fix rename not working correctly in AvatarPackager --- .../resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 04a4d2e41a..845fdeb99f 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -109,7 +109,7 @@ ShadowRectangle { font.pixelSize: 28 z: 200 onFocusChanged: { - if (titleArea.state === "renaming" && !titleArea.focus) { + if (titleArea.state === "renaming" && !focus) { accepted(); } } From c2ceeb3d7696a4d982ecf3b7327da8b56114b691 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 09:41:31 -0800 Subject: [PATCH 57/72] Apply AvatarPackager code review cleanup --- .../avatarPackager/AvatarUploadStatusItem.qml | 17 +++++++++++++---- .../qml/hifi/avatarPackager/ClickableArea.qml | 2 +- .../qml/hifi/avatarPackager/RalewayButton.qml | 2 +- interface/src/avatar/AvatarProject.cpp | 8 ++++---- interface/src/avatar/AvatarProject.h | 2 +- .../src/avatar/MarketplaceItemUploader.cpp | 11 ++++++++--- interface/src/avatar/MarketplaceItemUploader.h | 17 +++++++---------- libraries/fbx/src/FST.cpp | 6 ++++-- libraries/fbx/src/FST.h | 4 ++-- libraries/networking/src/AccountManager.h | 7 +++---- 10 files changed, 44 insertions(+), 32 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml index 4749e912c6..1e48264b3a 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml @@ -14,25 +14,34 @@ Item { property int uploaderState; property var uploader; +/* state: root.uploader === undefined ? "" : (root.uploader.state > uploaderState ? "success" : (root.uploader.error !== 0 ? "fail" : (root.uploader.state === uploaderState ? "running" : ""))) + */ states: [ State { - name: "running" + name: "" + when: root.uploader === null + }, + State { + name: "success" + when: root.uploader.state > uploaderState PropertyChanges { target: stepText; color: "white" } - PropertyChanges { target: runningImage; visible: true; playing: true } + 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: "success" + name: "running" + when: root.uploader.state === uploaderState PropertyChanges { target: stepText; color: "white" } - PropertyChanges { target: successGlyph; visible: true } + PropertyChanges { target: runningImage; visible: true; playing: true } } ] diff --git a/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml b/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml index ebe38364ec..0f7b201f72 100644 --- a/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml +++ b/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml @@ -60,4 +60,4 @@ Item { } ] } -} \ No newline at end of file +} diff --git a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml index edbc31b24f..18cce8138f 100644 --- a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml +++ b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml @@ -25,4 +25,4 @@ RalewaySemiBold { onClicked: root.clicked() } -} \ No newline at end of file +} diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index e049dc0ba3..34da80587b 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -162,7 +162,7 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, return new AvatarProject(fst); } -QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) { +QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) const { QStringList result{}; constexpr auto flags = QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; if (!scriptsDir.exists()) { @@ -244,6 +244,8 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { } void AvatarProject::openInInventory() { + constexpr int TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS { 1000 }; + auto tablet = dynamic_cast<TabletProxy*>( DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system")); tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml"); @@ -251,9 +253,7 @@ void AvatarProject::openInInventory() { auto name = getProjectName(); // I'm not a fan of this, but it's the only current option. - QTimer::singleShot(1000, [name]() { - auto tablet = dynamic_cast<TabletProxy*>( - DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system")); + 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 index c422c14d62..2a655409e1 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -103,7 +103,7 @@ private: void refreshProjectFiles(); void appendDirectory(const QString& prefix, const QDir& dir); - QStringList getScriptPaths(const QDir& scriptsDir); + QStringList getScriptPaths(const QDir& scriptsDir) const; FST* _fst; diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 8b97358ba4..543617fc56 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -36,7 +36,10 @@ MarketplaceItemUploader::MarketplaceItemUploader(QString title, QUuid marketplaceID, QList<ProjectFilePath> filePaths) : _title(title), - _description(description), _rootFilename(rootFilename), _marketplaceID(marketplaceID), _filePaths(filePaths) { + _description(description), + _rootFilename(rootFilename), + _marketplaceID(marketplaceID), + _filePaths(filePaths) { } void MarketplaceItemUploader::setState(State newState) { @@ -299,11 +302,13 @@ void MarketplaceItemUploader::doWaitForInventory() { if (success) { setState(State::Complete); } else { + constexpr int MAX_INVENTORY_REQUESTS { 8 }; + constexpr int TIME_BETWEEN_INVENTORY_REQUESTS_MS { 5000 }; qDebug() << "Failed to find item in inventory"; - if (_numRequestsForInventory > 8) { + if (_numRequestsForInventory > MAX_INVENTORY_REQUESTS) { setError(Error::Unknown); } else { - QTimer::singleShot(5000, [this]() { doWaitForInventory(); }); + QTimer::singleShot(TIME_BETWEEN_INVENTORY_REQUESTS_MS, [this]() { doWaitForInventory(); }); } } }); diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index 4fb1713b7d..998413da88 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -30,21 +30,19 @@ class MarketplaceItemUploader : public QObject { Q_PROPERTY(Error error READ getError NOTIFY errorChanged) Q_PROPERTY(QString responseData READ getResponseData) public: - enum class Error - { + enum class Error { None, - Unknown + Unknown, }; Q_ENUM(Error); - enum class State - { + enum class State { Idle, GettingCategories, UploadingAvatar, WaitingForUploadResponse, WaitingForInventory, - Complete + Complete, }; Q_ENUM(State); @@ -63,7 +61,6 @@ public: State getState() const { return _state; } bool getComplete() const { return _state == State::Complete; } - QUuid getMarketplaceID() const { return _marketplaceID; } Error getError() const { return _error; } @@ -86,8 +83,8 @@ private: QNetworkReply* _reply; - State _state{ State::Idle }; - Error _error{ Error::None }; + State _state { State::Idle }; + Error _error { Error::None }; QString _title; QString _description; @@ -98,7 +95,7 @@ private: QString _responseData; - int _numRequestsForInventory{ 0 }; + int _numRequestsForInventory { 0 }; QString _rootFilePath; QList<ProjectFilePath> _filePaths; diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index 9510ca5962..7828037c74 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -15,6 +15,8 @@ #include <QFileInfo> #include <hfm/HFM.h> +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 { @@ -55,7 +57,7 @@ FST* FST::createFSTFromModel(const QString& fstPath, const QString& modelFilePat mapping.insert(TEXDIR_FIELD, "textures"); // mixamo/autodesk defaults - mapping.insert(SCALE_FIELD, 1.0); + 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")); @@ -161,7 +163,7 @@ void FST::setModelPath(const QString& modelPath) { emit modelPathChanged(modelPath); } -QVariantHash FST::getMapping() { +QVariantHash FST::getMapping() const { QVariantHash mapping; mapping.unite(_other); mapping.insert(NAME_FIELD, _name); diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h index 6104130512..0f4c1ecd3a 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -45,9 +45,9 @@ public: QStringList getScriptPaths() const { return _scriptPaths; } void setScriptPaths(QStringList scriptPaths) { _scriptPaths = scriptPaths; } - QString getPath() { return _fstPath; } + QString getPath() const { return _fstPath; } - QVariantHash getMapping(); + QVariantHash getMapping() const; bool write(); diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 77f20472fa..b8c3bf0cc4 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -40,11 +40,10 @@ public: }; namespace AccountManagerAuth { -enum Type -{ +enum Type { None, Required, - Optional + Optional, }; } @@ -157,7 +156,7 @@ private: bool _isWaitingForTokenRefresh{ false }; bool _isAgent{ false }; - bool _isWaitingForKeypairResponse{ false }; + bool _isWaitingForKeypairResponse { false }; QByteArray _pendingPrivateKey; QUuid _sessionID{ QUuid::createUuid() }; From f38a469e659db7a96a9864a1e2052549c4b92891 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 09:55:21 -0800 Subject: [PATCH 58/72] Update avatar packager to force focus on inventory when opened --- interface/src/avatar/AvatarProject.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 34da80587b..20556ce5ed 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -250,10 +250,12 @@ void AvatarProject::openInInventory() { DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system")); tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml"); DependencyManager::get<HMDScriptingInterface>()->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 } })); + }); } From b680e9c52ae175497ad7892884dfb331853a37d8 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 09:59:17 -0800 Subject: [PATCH 59/72] Update avatar packager with CR feedback --- interface/src/avatar/MarketplaceItemUploader.cpp | 2 +- libraries/avatars/src/ProjectFile.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 543617fc56..938485353c 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -173,7 +173,7 @@ void MarketplaceItemUploader::doUploadAvatar() { qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB"; - QString path = "/api/v1/marketplace/items"; + static const QString path = "/api/v1/marketplace/items"; bool creating = true; if (!_marketplaceID.isNull()) { creating = false; diff --git a/libraries/avatars/src/ProjectFile.h b/libraries/avatars/src/ProjectFile.h index 82930a3464..4040eb1ce5 100644 --- a/libraries/avatars/src/ProjectFile.h +++ b/libraries/avatars/src/ProjectFile.h @@ -10,4 +10,4 @@ public: QString relativePath; }; -#endif // hifi_AvatarProjectFile_h \ No newline at end of file +#endif // hifi_AvatarProjectFile_h From 6d5b9a88e416360b192886207c70a721b2ea43f2 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 11:29:07 -0800 Subject: [PATCH 60/72] Remove dead code in AvatarUploadStatusItem --- .../qml/hifi/avatarPackager/AvatarUploadStatusItem.qml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml index 1e48264b3a..70a0ea0672 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml @@ -14,12 +14,6 @@ Item { property int uploaderState; property var uploader; -/* - state: root.uploader === undefined ? "" : - (root.uploader.state > uploaderState ? "success" - : (root.uploader.error !== 0 ? "fail" : (root.uploader.state === uploaderState ? "running" : ""))) - */ - states: [ State { name: "" From 51ce19e026fd0ec9677aedfe46ef8b867145c6d1 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 12:08:23 -0800 Subject: [PATCH 61/72] Fix modified const var in MarketplaceItemUploader --- interface/src/avatar/MarketplaceItemUploader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 938485353c..543617fc56 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -173,7 +173,7 @@ void MarketplaceItemUploader::doUploadAvatar() { qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB"; - static const QString path = "/api/v1/marketplace/items"; + QString path = "/api/v1/marketplace/items"; bool creating = true; if (!_marketplaceID.isNull()) { creating = false; From 63172ec87ba3116c574e70e0b2fa3458525059c1 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 14:21:10 -0800 Subject: [PATCH 62/72] Fix marketplace item update failing on submitted items --- interface/src/avatar/AvatarProject.cpp | 1 - interface/src/avatar/MarketplaceItemUploader.cpp | 10 ++++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 20556ce5ed..a38bdcd693 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -256,6 +256,5 @@ void AvatarProject::openInInventory() { // 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/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 543617fc56..53b37eba4f 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -190,7 +190,12 @@ void MarketplaceItemUploader::doUploadAvatar() { QString jsonString = "{\"marketplace_item\":{"; jsonString += "\"title\":\"" + escapeJson(_title) + "\""; - jsonString += ",\"description\":\"" + escapeJson(_description) + "\""; + + // 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"; @@ -300,12 +305,13 @@ void MarketplaceItemUploader::doWaitForInventory() { 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 }; - qDebug() << "Failed to find item in inventory"; 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(); }); From 5d40e1e4801e2a4e4f2eca034b5f7d0590aa7543 Mon Sep 17 00:00:00 2001 From: Thijs Wenker <me@thoys.nl> Date: Fri, 4 Jan 2019 23:39:04 +0100 Subject: [PATCH 63/72] - CR/style fixes - Moved the old Avatar Packager tool to Developer -> Avatar -> .. --- .../hifi/avatarPackager/AvatarPackagerApp.qml | 16 +++++++------- .../avatarPackager/AvatarPackagerHeader.qml | 3 --- .../qml/hifi/avatarPackager/AvatarProject.qml | 22 +++++++++---------- .../hifi/avatarPackager/AvatarProjectCard.qml | 2 +- .../qml/hifi/avatarPackager/RalewayButton.qml | 10 ++++----- interface/src/Menu.cpp | 6 ++--- interface/src/avatar/AvatarPackager.cpp | 2 +- interface/src/avatar/AvatarPackager.h | 12 +++++----- interface/src/avatar/AvatarProject.cpp | 7 +++--- interface/src/avatar/AvatarProject.h | 2 +- 10 files changed, 38 insertions(+), 44 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index 9d3a347a11..b4293d5eee 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -76,7 +76,7 @@ Item { InfoBox { id: errorPopup - property string errorMessage; + property string errorMessage boxWidth: 380 boxHeight: 293 @@ -181,16 +181,16 @@ Item { 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 during the creation of the Avatar Project directories. Please select a project location with write permissions."); + 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 if it exist at the specified location."); + 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. Please verify if you have read permissions at the specified location."); + 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 verify if the model file is supported by High Fidelity."); + 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."); @@ -202,13 +202,13 @@ Item { 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 (avatar.fst) in the folder. Please locate it and move to the project folder."); + 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 (avatar.fst). Please make sure that it is not in use by another program."); + 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 folder. Please locate it and move to the project folder."); + 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); diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 845fdeb99f..25201bf81e 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -35,7 +35,6 @@ ShadowRectangle { anchors.bottom: parent.bottom anchors.left: parent.left anchors.leftMargin: 16 - anchors.verticalCenter: back.verticalCenter text: "◀" @@ -48,7 +47,6 @@ ShadowRectangle { anchors.bottom: parent.bottom anchors.left: root.backButtonVisible ? back.right : parent.left anchors.leftMargin: root.backButtonVisible ? 11 : 21 - anchors.verticalCenter: title.verticalCenter anchors.right: docs.left states: [ State { @@ -136,7 +134,6 @@ ShadowRectangle { anchors.bottom: parent.bottom anchors.right: parent.right anchors.rightMargin: 16 - anchors.verticalCenter: docs.verticalCenter text: qsTr("Docs") diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 59dd1ac5c9..85ef821a4a 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -16,10 +16,10 @@ Item { Style { id: style } - property int colorScheme; - property var uploader: null; + property int colorScheme + property var uploader: null - property bool hasSuccessfullyUploaded: true; + property bool hasSuccessfullyUploaded: true visible: false anchors.fill: parent @@ -44,7 +44,7 @@ Item { HifiControls.Button { id: uploadButton - visible: !AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded + visible: AvatarPackagerCore.currentAvatarProject && !AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded enabled: Account.loggedIn anchors.verticalCenter: parent.verticalCenter @@ -62,7 +62,7 @@ Item { HifiControls.Button { id: updateButton - visible: AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded + visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded enabled: Account.loggedIn anchors.verticalCenter: parent.verticalCenter @@ -175,9 +175,9 @@ Item { } function showConfirmUploadPopup() { - popup.titleText = 'Overwrite Avatar' + 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.' + ' This will overwrite that avatar and you won’t be able to access the older version.'; popup.button1text = 'CREATE NEW'; popup.button2text = 'OVERWRITE'; @@ -185,7 +185,7 @@ Item { popup.onButton2Clicked = function() { popup.close(); uploadUpdate(); - } + }; popup.onButton1Clicked = function() { popup.close(); showConfirmCreateNewPopup(); @@ -195,9 +195,9 @@ Item { } function showConfirmCreateNewPopup(confirmCallback) { - popup.titleText = 'Create New' + 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?' + ' You will lose the ability to update the previously uploaded avatar. Are you sure you want to continue?'; popup.button1text = 'CANCEL'; popup.button2text = 'CONFIRM'; @@ -277,7 +277,7 @@ Item { size: 20 - text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>See list</a>" + text: AvatarPackagerCore.currentAvatarProject ? AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>See list</a>" : "" onLinkActivated: fileListPopup.open() } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml index a758d3936a..25222c814c 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -21,7 +21,7 @@ Item { property color hoverBackgroundColor: "#E3E3E3" property color pressedBackgroundColor: "#6A6A6A" - signal open; + signal open state: mouseArea.pressed ? "pressed" : (mouseArea.containsMouse ? "hover" : "normal") states: [ diff --git a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml index 18cce8138f..86742ddccd 100644 --- a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml +++ b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml @@ -8,13 +8,11 @@ import TabletScriptingInterface 1.0 RalewaySemiBold { id: root - anchors.fill: textItem + property color idleColor: "white" + property color hoverColor: "#AFAFAF" + property color pressedColor: "#575757" - property var idleColor: "white" - property var hoverColor: "#AFAFAF" - property var pressedColor: "#575757" - - color: clickable.hovered ? root.hoverColor : (clickable.pressed ? root.pressedColor : root.idleColor); + color: clickable.hovered ? root.hoverColor : (clickable.pressed ? root.pressedColor : root.idleColor) signal clicked() diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 87b1542648..810e21daf5 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -145,10 +145,6 @@ 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); @@ -654,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/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index 941aff6943..fa70eee374 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -95,7 +95,7 @@ void AvatarPackager::addCurrentProjectToRecentProjects() { emit recentProjectsChanged(); } -QVariantList AvatarPackager::recentProjectsToVariantList(bool includeProjectPaths) { +QVariantList AvatarPackager::recentProjectsToVariantList(bool includeProjectPaths) const { QVariantList result; for (const auto& project : _recentProjects) { QVariantMap projectVariant; diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index c9c5b9d312..4416ec5806 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -69,7 +69,9 @@ public: const QString& textureFolder); Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus openAvatarProject(const QString& avatarProjectFSTPath); - Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) { return AvatarProject::isValidNewProjectName(projectPath, projectName); } + Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) { + return AvatarProject::isValidNewProjectName(projectPath, projectName); + } signals: void avatarProjectChanged(); @@ -78,21 +80,21 @@ signals: private: Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; Q_INVOKABLE QString getAvatarProjectsPath() const { return AvatarProject::getDefaultProjectsPath(); } - Q_INVOKABLE QVariantList getRecentProjects() { return recentProjectsToVariantList(true); } + Q_INVOKABLE QVariantList getRecentProjects() const { return recentProjectsToVariantList(true); } void setAvatarProject(AvatarProject* avatarProject); void addCurrentProjectToRecentProjects(); - AvatarProject* _currentAvatarProject{ nullptr }; + AvatarProject* _currentAvatarProject { nullptr }; QVector<RecentAvatarProject> _recentProjects; - QVariantList recentProjectsToVariantList(bool includeProjectPaths); + QVariantList recentProjectsToVariantList(bool includeProjectPaths) const; void recentProjectsFromVariantList(QVariantList projectsVariant); - Setting::Handle<QVariantList> _recentProjectsSetting{ "io.highfidelity.avatarPackager.recentProjects", QVariantList() }; + Setting::Handle<QVariantList> _recentProjectsSetting { "io.highfidelity.avatarPackager.recentProjects", QVariantList() }; }; #endif // hifi_AvatarPackager_h diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 20556ce5ed..728917e673 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -169,8 +169,8 @@ QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) const { return result; } - for (auto& script : scriptsDir.entryInfoList({}, flags)) { - if (script.fileName().endsWith(".js")) { + for (const auto& script : scriptsDir.entryInfoList({}, flags)) { + if (script.fileName().toLower().endsWith(".js")) { result.push_back("scripts/" + script.fileName()); } } @@ -243,7 +243,7 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { return uploader; } -void AvatarProject::openInInventory() { +void AvatarProject::openInInventory() const { constexpr int TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS { 1000 }; auto tablet = dynamic_cast<TabletProxy*>( @@ -256,6 +256,5 @@ void AvatarProject::openInInventory() { // 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 index 2a655409e1..1710282a3e 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -56,7 +56,7 @@ class AvatarProject : public QObject { public: Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); - Q_INVOKABLE void openInInventory(); + Q_INVOKABLE void openInInventory() const; Q_INVOKABLE QStringList getProjectFiles() const; Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } From cfbb4e29d1b0c0eab163b27e44bc998909253f5e Mon Sep 17 00:00:00 2001 From: Clement <clement.brisset@gmail.com> Date: Fri, 4 Jan 2019 13:44:38 -0800 Subject: [PATCH 64/72] Only keep aggregated thread stats --- assignment-client/src/avatars/AvatarMixer.cpp | 45 ++----------------- libraries/networking/src/LimitedNodeList.cpp | 29 ------------ libraries/networking/src/LimitedNodeList.h | 8 ---- libraries/networking/src/PacketReceiver.cpp | 6 --- libraries/networking/src/PacketReceiver.h | 8 +--- .../networking/src/ThreadedAssignment.cpp | 12 ++--- 6 files changed, 8 insertions(+), 100 deletions(-) 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/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<NLPacket> 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<NLPacketList> packetList, for (std::unique_ptr<udt::Packet>& packet : packetList->_packets) { NLPacket* nlPacket = static_cast<NLPacket*>(packet.get()); - collectPacketStats(*nlPacket); fillPacketHeader(*nlPacket); } @@ -505,7 +494,6 @@ qint64 LimitedNodeList::sendPacketList(std::unique_ptr<NLPacketList> packetList, for (std::unique_ptr<udt::Packet>& packet : packetList->_packets) { NLPacket* nlPacket = static_cast<NLPacket*>(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<SharedNodePointer> 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<NLPacket> packet, const NodeSet& destinationNodeTypes); SharedNodePointer soloNodeOfType(NodeType_t nodeType); - void getPacketStats(float& packetsInPerSecond, float& bytesInPerSecond, float& packetsOutPerSecond, float& bytesOutPerSecond); - void resetPacketStats(); - std::unique_ptr<NLPacket> constructPingPacket(const QUuid& nodeId, PingType_t pingType = PingType::Agnostic); std::unique_ptr<NLPacket> constructPingReplyPacket(ReceivedMessage& message); @@ -377,7 +374,6 @@ protected: qint64 sendPacket(std::unique_ptr<NLPacket> 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<int> _numCollectedPackets { 0 }; - std::atomic<int> _numCollectedBytes { 0 }; - - QElapsedTimer _packetStatTimer; NodePermissions _permissions; QPointer<QTimer> _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<udt::Packet> packet) { auto nlPacket = NLPacket::fromBase(std::move(packet)); auto receivedMessage = QSharedPointer<ReceivedMessage>::create(*nlPacket); - _inPacketCount += 1; - _inByteCount += nlPacket->size(); - handleVerifiedMessage(receivedMessage, true); } void PacketReceiver::handleVerifiedMessagePacket(std::unique_ptr<udt::Packet> packet) { auto nlPacket = NLPacket::fromBase(std::move(packet)); - _inPacketCount += 1; - _inByteCount += nlPacket->size(); - auto key = std::pair<HifiSockAddr, udt::Packet::MessageNumber>(nlPacket->getSenderSockAddr(), nlPacket->getMessageNumber()); auto it = _pendingMessages.find(key); QSharedPointer<ReceivedMessage> 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<PacketType, Listener> _messageListenerMap; - int _inPacketCount = 0; - int _inByteCount = 0; + bool _shouldDropPackets = false; QMutex _directConnectSetMutex; QSet<QObject*> _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<NodeList>(); - 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; From 94146ab997daba74c94b09392e914fbfc484454b Mon Sep 17 00:00:00 2001 From: Thijs Wenker <me@thoys.nl> Date: Fri, 4 Jan 2019 23:59:22 +0100 Subject: [PATCH 65/72] CR fixes --- interface/src/avatar/AvatarPackager.h | 2 +- libraries/networking/src/AccountManager.h | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index 4416ec5806..ec954a60d7 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -69,7 +69,7 @@ public: const QString& textureFolder); Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus openAvatarProject(const QString& avatarProjectFSTPath); - Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) { + Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) const { return AvatarProject::isValidNewProjectName(projectPath, projectName); } diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index b8c3bf0cc4..ca2b826c98 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -153,15 +153,15 @@ private: QUrl _authURL; DataServerAccountInfo _accountInfo; - bool _isWaitingForTokenRefresh{ false }; - bool _isAgent{ false }; + bool _isWaitingForTokenRefresh { false }; + bool _isAgent { false }; bool _isWaitingForKeypairResponse { false }; QByteArray _pendingPrivateKey; - QUuid _sessionID{ QUuid::createUuid() }; + QUuid _sessionID { QUuid::createUuid() }; - bool _limitedCommerce{ false }; + bool _limitedCommerce { false }; }; #endif // hifi_AccountManager_h From d630fe9dbdeb476356b73c8094ba1732d6bcfd92 Mon Sep 17 00:00:00 2001 From: luiscuenca <luiscuenca@outboxcode.com> Date: Fri, 4 Jan 2019 16:38:48 -0700 Subject: [PATCH 66/72] Add menu checkbox to toggle collisions with other avatars --- interface/resources/qml/hifi/AvatarApp.qml | 3 +- .../resources/qml/hifi/avatarapp/Settings.qml | 73 ++++++++----------- interface/src/avatar/AvatarManager.cpp | 1 + interface/src/avatar/AvatarMotionState.cpp | 5 +- interface/src/avatar/AvatarMotionState.h | 5 +- interface/src/avatar/MyAvatar.cpp | 15 +++- interface/src/avatar/MyAvatar.h | 21 ++++++ interface/src/avatar/OtherAvatar.cpp | 16 +++- interface/src/avatar/OtherAvatar.h | 2 + .../src/avatars-renderer/Avatar.h | 1 + libraries/avatars/src/AvatarData.cpp | 11 ++- libraries/avatars/src/AvatarData.h | 2 + .../networking/src/udt/PacketHeaders.cpp | 1 + libraries/networking/src/udt/PacketHeaders.h | 3 +- scripts/system/avatarapp.js | 13 +++- 15 files changed, 120 insertions(+), 52 deletions(-) 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/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/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 21e59c06d8..1508581e42 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -268,6 +268,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..9d9a6fba2f 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 }; - + uint32_t _collisionGroup; uint32_t _dirtyFlags; }; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 1f63a904bb..70970e245f 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -3326,7 +3326,6 @@ void MyAvatar::setCollisionsEnabled(bool enabled) { QMetaObject::invokeMethod(this, "setCollisionsEnabled", Q_ARG(bool, enabled)); return; } - _characterController.setCollisionless(!enabled); emit collisionsEnabledChanged(enabled); } @@ -3337,6 +3336,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 92fe6d4f4a..845f6398a3 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) @@ -1062,6 +1063,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} @@ -1489,6 +1502,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 a54a74fb93..dffaadf862 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -552,6 +552,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, LaterDeleter> _clientTraitsHandler; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 2cc5804d10..736bf6ae8e 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<PacketVersion>(EntityQueryPacketVersion::ConicalFrustums); case PacketType::AvatarIdentity: case PacketType::AvatarData: + return static_cast<PacketVersion>(AvatarMixerPacketVersion::CollisionFlag); case PacketType::BulkAvatarData: case PacketType::KillAvatar: return static_cast<PacketVersion>(AvatarMixerPacketVersion::GrabTraits); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 62549a7562..2ca8f0240a 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -305,7 +305,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); From 48dc7ee938eaaca0eacc273a314d58045b90fefc Mon Sep 17 00:00:00 2001 From: Thijs Wenker <me@thoys.nl> Date: Sat, 5 Jan 2019 01:56:28 +0100 Subject: [PATCH 67/72] New color/text entity properties --- scripts/system/html/css/edit-style.css | 12 +- scripts/system/html/js/entityProperties.js | 129 ++++++++++++++++++++- 2 files changed, 132 insertions(+), 9 deletions(-) diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index c5979c41e2..6a0e7c8343 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -956,12 +956,12 @@ div.refresh input[type="button"] { } .draggable-number .left-arrow { top: 3px; - left: 0px; + left: 0; transform: rotate(180deg); } .draggable-number .right-arrow { top: 3px; - right: 0px; + right: 0; } .draggable-number input[type=number] { position: absolute; @@ -995,6 +995,10 @@ div.refresh input[type="button"] { font-size: 15px; } +.rect .rect-row { + margin-bottom: 8px; +} + .row .property { width: auto; display: inline-block; @@ -1602,10 +1606,10 @@ input.rename-entity { margin-left: 4px; margin-right: 10px; } -.fstuple label.red, .fstuple label.x { +.fstuple label.red, .fstuple label.x, .fstuple label.w { color: #C62147; } -.fstuple label.green, .fstuple label.y { +.fstuple label.green, .fstuple label.y, .fstuple label.h { color: #359D85; } .fstuple label.blue, .fstuple label.z { diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 78ef8ac313..66dcecc83e 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -140,12 +140,14 @@ const GROUPS = [ step: 0.005, decimals: 4, unit: "m", - propertyID: "lineHeight" + propertyID: "lineHeight", }, { - label: "Face Camera", - type: "bool", - propertyID: "faceCamera" + label: "Billboard Mode", + type: "dropdown", + options: { none: "None", yaw: "Yaw", full: "Full"}, + propertyID: "textBillboardMode", + propertyName: "billboardMode", // actual entity property name }, ] }, @@ -478,6 +480,37 @@ const GROUPS = [ placeholder: "URL", propertyID: "imageURL", }, + { + label: "Color", + type: "color", + propertyID: "imageColor", + propertyName: "color", // actual entity property name + }, + { + label: "Emissive", + type: "bool", + propertyID: "emissive", + }, + { + label: "Sub Image", + type: "rect", + min: 0, + step: 1, + subLabels: [ "x", "y", "w", "h" ], + propertyID: "subImage", + }, + { + label: "Billboard Mode", + type: "dropdown", + options: { none: "None", yaw: "Yaw", full: "Full"}, + propertyID: "imageBillboardMode", + propertyName: "billboardMode", // actual entity property name + }, + { + label: "Keep Aspect Ratio", + type: "bool", + propertyID: "keepAspectRatio", + }, ] }, { @@ -1424,6 +1457,13 @@ const PROPERTY_NAME_DIVISION = { SUBPROPERTY: 2, }; +const RECT_ELEMENTS = { + X_NUMBER: 0, + Y_NUMBER: 1, + WIDTH_NUMBER: 2, + HEIGHT_NUMBER: 3, +}; + const VECTOR_ELEMENTS = { X_NUMBER: 0, Y_NUMBER: 1, @@ -1475,6 +1515,13 @@ function getPropertyInputElement(propertyID) { return property.elInput; case 'number-draggable': return property.elNumber.elInput; + case 'rect': + return { + x: property.elNumberX.elInput, + y: property.elNumberY.elInput, + width: property.elNumberWidth.elInput, + height: property.elNumberHeight.elInput + }; case 'vec3': case 'vec2': return { x: property.elNumberX.elInput, y: property.elNumberY.elInput, z: property.elNumberZ.elInput }; @@ -1564,6 +1611,13 @@ function resetProperties() { } break; } + case 'rect': { + property.elNumberX.setValue(""); + property.elNumberY.setValue(""); + property.elNumberWidth.setValue(""); + property.elNumberHeight.setValue(""); + break; + } case 'vec3': case 'vec2': { property.elNumberX.setValue(""); @@ -1748,7 +1802,7 @@ function createDragStartFunction(property) { function createDragEndFunction(property) { return function() { property.dragging = false; - // send an additonal update post-dragging to consider whole property change from dragStart to dragEnd to be 1 action + // send an additional update post-dragging to consider whole property change from dragStart to dragEnd to be 1 action this.valueChangeFunction(); }; } @@ -1793,6 +1847,18 @@ function createEmitVec3PropertyUpdateFunction(property) { }; } +function createEmitRectPropertyUpdateFunction(property) { + return function() { + let newValue = { + x: property.elNumberX.elInput.value, + y: property.elNumberY.elInput.value, + width: property.elNumberWidth.elInput.value, + height: property.elNumberHeight.elInput.value, + }; + updateProperty(property.name, newValue, property.isParticleProperty); + }; +} + function createEmitColorPropertyUpdateFunction(property) { return function() { emitColorPropertyUpdate(property.name, property.elNumberR.elInput.value, property.elNumberG.elInput.value, @@ -1951,6 +2017,44 @@ function createNumberDraggableProperty(property, elProperty) { return elDraggableNumber; } +function createRectProperty(property, elProperty) { + let propertyData = property.data; + + elProperty.className = "rect"; + + let elXYRow = document.createElement('div'); + elXYRow.className = "rect-row fstuple"; + elProperty.appendChild(elXYRow); + + let elWidthHeightRow = document.createElement('div'); + elWidthHeightRow.className = "rect-row fstuple"; + elProperty.appendChild(elWidthHeightRow); + + + let elNumberX = createTupleNumberInput(property, propertyData.subLabels[RECT_ELEMENTS.X_NUMBER]); + let elNumberY = createTupleNumberInput(property, propertyData.subLabels[RECT_ELEMENTS.Y_NUMBER]); + let elNumberWidth = createTupleNumberInput(property, propertyData.subLabels[RECT_ELEMENTS.WIDTH_NUMBER]); + let elNumberHeight = createTupleNumberInput(property, propertyData.subLabels[RECT_ELEMENTS.HEIGHT_NUMBER]); + + elXYRow.appendChild(elNumberX.elDiv); + elXYRow.appendChild(elNumberY.elDiv); + elWidthHeightRow.appendChild(elNumberWidth.elDiv); + elWidthHeightRow.appendChild(elNumberHeight.elDiv); + + let valueChangeFunction = createEmitRectPropertyUpdateFunction(property); + elNumberX.setValueChangeFunction(valueChangeFunction); + elNumberY.setValueChangeFunction(valueChangeFunction); + elNumberWidth.setValueChangeFunction(valueChangeFunction); + elNumberHeight.setValueChangeFunction(valueChangeFunction); + + let elResult = []; + elResult[RECT_ELEMENTS.X_NUMBER] = elNumberX; + elResult[RECT_ELEMENTS.Y_NUMBER] = elNumberY; + elResult[RECT_ELEMENTS.WIDTH_NUMBER] = elNumberWidth; + elResult[RECT_ELEMENTS.HEIGHT_NUMBER] = elNumberHeight; + return elResult; +} + function createVec3Property(property, elProperty) { let propertyData = property.data; @@ -2273,6 +2377,14 @@ function createProperty(propertyData, propertyElementID, propertyName, propertyI property.elNumber = createNumberDraggableProperty(property, elProperty); break; } + case 'rect': { + let elRect = createRectProperty(property, elProperty); + property.elNumberX = elRect[RECT_ELEMENTS.X_NUMBER]; + property.elNumberY = elRect[RECT_ELEMENTS.Y_NUMBER]; + property.elNumberWidth = elRect[RECT_ELEMENTS.WIDTH_NUMBER]; + property.elNumberHeight = elRect[RECT_ELEMENTS.HEIGHT_NUMBER]; + break; + } case 'vec3': { let elVec3 = createVec3Property(property, elProperty); property.elNumberX = elVec3[VECTOR_ELEMENTS.X_NUMBER]; @@ -3160,6 +3272,7 @@ function loaded() { case 'number-draggable': isPropertyNotNumber = isNaN(propertyValue) || propertyValue === null; break; + case 'rect': case 'vec3': case 'vec2': isPropertyNotNumber = isNaN(propertyValue.x) || propertyValue.x === null; @@ -3202,6 +3315,12 @@ function loaded() { property.elNumber.setValue(value); break; } + case 'rect': + property.elNumberX.setValue(propertyValue.x); + property.elNumberY.setValue(propertyValue.y); + property.elNumberWidth.setValue(propertyValue.width); + property.elNumberHeight.setValue(propertyValue.height); + break; case 'vec3': case 'vec2': { let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; From 2a367509cd897f9463083754e1a742ecbad03752 Mon Sep 17 00:00:00 2001 From: luiscuenca <luiscuenca@outboxcode.com> Date: Fri, 4 Jan 2019 18:02:18 -0700 Subject: [PATCH 68/72] Fix warnings --- interface/src/avatar/AvatarMotionState.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarMotionState.h b/interface/src/avatar/AvatarMotionState.h index 9d9a6fba2f..3103341622 100644 --- a/interface/src/avatar/AvatarMotionState.h +++ b/interface/src/avatar/AvatarMotionState.h @@ -90,7 +90,7 @@ protected: OtherAvatarPointer _avatar; float _diameter { 0.0f }; - uint32_t _collisionGroup; + int32_t _collisionGroup; uint32_t _dirtyFlags; }; From 26f46e87a3f917477aab8639cbf1ec49012fa35d Mon Sep 17 00:00:00 2001 From: Thijs Wenker <me@thoys.nl> Date: Sat, 5 Jan 2019 02:16:12 +0100 Subject: [PATCH 69/72] new property tooltips --- .../system/assets/data/createAppTooltips.json | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/scripts/system/assets/data/createAppTooltips.json b/scripts/system/assets/data/createAppTooltips.json index 9f77a86dc9..56341acc90 100644 --- a/scripts/system/assets/data/createAppTooltips.json +++ b/scripts/system/assets/data/createAppTooltips.json @@ -20,6 +20,10 @@ "faceCamera": { "tooltip": "If enabled, the entity follows the camera of each user, creating a billboard effect." }, + "textBillboardMode": { + "tooltip": "If enabled, determines how the entity will face the camera.", + "jsPropertyName": "billboardMode" + }, "flyingAllowed": { "tooltip": "If enabled, users can fly in the zone." }, @@ -149,9 +153,25 @@ "originalTextures": { "tooltip": "A JSON string containing the original texture used on the model." }, - "image": { - "tooltip": "The URL for the image source.", - "jsPropertyName": "textures" + "imageURL": { + "tooltip": "The URL for the image source." + }, + "imageColor": { + "tooltip": "The tint to be applied to the image.", + "jsPropertyName": "color" + }, + "emissive": { + "tooltip": "If enabled, the image will display at full brightness." + }, + "subImage": { + "tooltip": "The area of the image that is displayed." + }, + "imageBillboardMode": { + "tooltip": "If enabled, determines how the entity will face the camera.", + "jsPropertyName": "billboardMode" + }, + "keepAspectRatio": { + "tooltip": "If enabled, the image will maintain its original aspect ratio." }, "sourceUrl": { "tooltip": "The URL for the web page source." From 0dc5e7743f2c6fed5f8c71178b7f8832105a916d Mon Sep 17 00:00:00 2001 From: Dante Ruiz <danteruiz102@gmail.com> Date: Fri, 4 Jan 2019 17:49:38 -0800 Subject: [PATCH 70/72] allow mouse pointer to pick against collidable entities --- interface/src/Application.cpp | 3 ++- scripts/system/controllers/controllerDispatcher.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b6b4e8e2a1..a447b369e0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2284,7 +2284,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Setup the mouse ray pick and related operators { - auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_INCLUDE_NONCOLLIDABLE()), 0.0f, true); + auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_INCLUDE_NONCOLLIDABLE() | + PickScriptingInterface::PICK_INCLUDE_COLLIDABLE()), 0.0f, true); mouseRayPick->parentTransform = std::make_shared<MouseTransformNode>(); mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE); auto mouseRayPickID = DependencyManager::get<PickManager>()->addPick(PickQuery::Ray, mouseRayPick); diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 2a5cf5a727..edf11647a2 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -455,7 +455,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.leftPointer = this.pointerManager.createPointer(false, PickType.Ray, { joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", - filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE, + filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE | Picks.PICK_INCLUDE_COLLIDABLE, triggers: [{action: Controller.Standard.LTClick, button: "Focus"}, {action: Controller.Standard.LTClick, button: "Primary"}], posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand, true), hover: true, @@ -466,7 +466,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Keyboard.setLeftHandLaser(this.leftPointer); this.rightPointer = this.pointerManager.createPointer(false, PickType.Ray, { joint: "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND", - filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE, + filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE | Picks.PICK_INCLUDE_COLLIDABLE, triggers: [{action: Controller.Standard.RTClick, button: "Focus"}, {action: Controller.Standard.RTClick, button: "Primary"}], posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand, true), hover: true, From 4b99a795dd78b1d3d4b7daae6d13574768cb5761 Mon Sep 17 00:00:00 2001 From: Dante Ruiz <danteruiz102@gmail.com> Date: Mon, 7 Jan 2019 08:36:23 -0800 Subject: [PATCH 71/72] code review changes --- interface/src/Application.cpp | 3 +-- scripts/system/controllers/controllerDispatcher.js | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a447b369e0..4dd8595dab 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2284,8 +2284,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Setup the mouse ray pick and related operators { - auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_INCLUDE_NONCOLLIDABLE() | - PickScriptingInterface::PICK_INCLUDE_COLLIDABLE()), 0.0f, true); + auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES()), 0.0f, true); mouseRayPick->parentTransform = std::make_shared<MouseTransformNode>(); mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE); auto mouseRayPickID = DependencyManager::get<PickManager>()->addPick(PickQuery::Ray, mouseRayPick); diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index edf11647a2..2a5cf5a727 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -455,7 +455,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.leftPointer = this.pointerManager.createPointer(false, PickType.Ray, { joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", - filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE | Picks.PICK_INCLUDE_COLLIDABLE, + filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE, triggers: [{action: Controller.Standard.LTClick, button: "Focus"}, {action: Controller.Standard.LTClick, button: "Primary"}], posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand, true), hover: true, @@ -466,7 +466,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Keyboard.setLeftHandLaser(this.leftPointer); this.rightPointer = this.pointerManager.createPointer(false, PickType.Ray, { joint: "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND", - filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE | Picks.PICK_INCLUDE_COLLIDABLE, + filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE, triggers: [{action: Controller.Standard.RTClick, button: "Focus"}, {action: Controller.Standard.RTClick, button: "Primary"}], posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand, true), hover: true, From 5cded6938b1af548798783ea856d063ec4e3d2b5 Mon Sep 17 00:00:00 2001 From: SamGondelman <samuel_gondelman@alumni.brown.edu> Date: Mon, 7 Jan 2019 11:55:34 -0800 Subject: [PATCH 72/72] possible fix for texture path --- .../entities-renderer/src/RenderablePolyLineEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index de224103ce..d4a10e551d 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -26,7 +26,7 @@ using namespace render::entities; gpu::PipelinePointer PolyLineEntityRenderer::_pipeline = nullptr; -static const QUrl DEFAULT_POLYLINE_TEXTURE = QUrl(PathUtils::resourcesPath() + "images/paintStroke.png"); +static const QUrl DEFAULT_POLYLINE_TEXTURE = PathUtils::resourcesUrl("images/paintStroke.png"); PolyLineEntityRenderer::PolyLineEntityRenderer(const EntityItemPointer& entity) : Parent(entity) { _texture = DependencyManager::get<TextureCache>()->getTexture(DEFAULT_POLYLINE_TEXTURE);