diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml new file mode 100644 index 0000000000..d329b903bd --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml @@ -0,0 +1,125 @@ +import QtQuick 2.0 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Item { + id: diagnosingScreen + + visible: false + + property var avatarDoctor: null + property var errors: [] + + signal doneDiagnosing + + onVisibleChanged: { + if (!diagnosingScreen.visible) { + //if (debugDelay.running) { + // debugDelay.stop(); + //} + return; + } + //debugDelay.start(); + avatarDoctor = AvatarPackagerCore.currentAvatarProject.diagnose(); + avatarDoctor.complete.connect(function(errors) { + console.warn("avatarDoctor.complete " + JSON.stringify(errors)); + diagnosingScreen.errors = errors; + AvatarPackagerCore.currentAvatarProject.hasErrors = errors.length > 0; + AvatarPackagerCore.addCurrentProjectToRecentProjects(); + + // FIXME: can't seem to change state here so do it with a timer instead + doneTimer.start(); + }); + avatarDoctor.startDiagnosing(); + } + + Timer { + id: doneTimer + interval: 1 + repeat: false + running: false + onTriggered: { + doneDiagnosing(); + } + } + +/* + Timer { + id: debugDelay + interval: 5000 + repeat: false + running: false + onTriggered: { + if (Math.random() > 0.5) { + // ERROR + avatarPackager.state = AvatarPackagerState.avatarDoctorErrorReport; + } else { + // SUCCESS + avatarPackager.state = AvatarPackagerState.project; + } + } + } +*/ + + property var footer: Item { + anchors.fill: parent + anchors.rightMargin: 17 + HifiControls.Button { + id: cancelButton + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + height: 30 + width: 133 + text: qsTr("Cancel") + onClicked: { + avatarPackager.state = AvatarPackagerState.main; + } + } + } + + LoadingCircle { + id: loadingCircle + anchors { + top: parent.top + topMargin: 46 + horizontalCenter: parent.horizontalCenter + } + width: 163 + height: 163 + } + + RalewayRegular { + id: testingPackageTitle + + anchors { + horizontalCenter: parent.horizontalCenter + top: loadingCircle.bottom + topMargin: 5 + } + + text: "Testing package for errors" + size: 28 + color: "white" + } + + RalewayRegular { + id: testingPackageText + + anchors { + top: testingPackageTitle.bottom + topMargin: 26 + left: parent.left + leftMargin: 21 + right: parent.right + rightMargin: 16 + } + + text: "We are trying to find errors in your project so you can quickly understand and resolve them." + size: 21 + color: "white" + lineHeight: 33 + lineHeightMode: Text.FixedHeight + wrapMode: Text.Wrap + } +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml new file mode 100644 index 0000000000..8811ba48a3 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml @@ -0,0 +1,112 @@ +import QtQuick 2.0 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Item { + id: errorReport + + visible: false + + property alias errors: errorRepeater.model + + property var footer: Item { + anchors.fill: parent + anchors.rightMargin: 17 + HifiControls.Button { + id: tryAgainButton + anchors.verticalCenter: parent.verticalCenter + anchors.right: continueButton.left + anchors.rightMargin: 22 + height: 40 + width: 134 + text: qsTr("Try Again") + // colorScheme: root.colorScheme + onClicked: { + avatarPackager.state = AvatarPackagerState.avatarDoctorDiagnose; + } + } + + HifiControls.Button { + id: continueButton + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + height: 40 + width: 133 + text: qsTr("Continue") + color: hifi.buttons.blue + colorScheme: root.colorScheme + onClicked: { + avatarPackager.state = AvatarPackagerState.project; + } + } + } + + HiFiGlyphs { + id: errorReportIcon + text: hifi.glyphs.alert + size: 315 + color: "#EA4C5F" + anchors { + top: parent.top + //topMargin: 73 + horizontalCenter: parent.horizontalCenter + } + } + + Column { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + top: errorReportIcon.bottom + topMargin: 27 + leftMargin: 13 + rightMargin: 13 + } + spacing: 7 + + Repeater { + id: errorRepeater + /*model: [ + {message: "Rig is not Hifi compatible", url: "http://www.highfidelity.com/"}, + {message: "Bone limit exceeds 256", url: "http://www.highfidelity.com/2"}, + {message: "Unsupported Texture", url: "http://www.highfidelity.com/texture"}, + {message: "Rig is not Hifi compatible", url: "http://www.highfidelity.com/"}, + {message: "Bone limit exceeds 256", url: "http://www.highfidelity.com/2"}, + {message: "Unsupported Texture", url: "http://www.highfidelity.com/texture"} + ]*/ + + Item { + height: 37 + width: parent.width + + HiFiGlyphs { + id: errorIcon + text: hifi.glyphs.alert + size: 56 + color: "#EA4C5F" + anchors { + top: parent.top + left: parent.left + } + } + + RalewayRegular { + id: errorLink + anchors { + top: parent.top + left: errorIcon.right + right: parent.right + } + linkColor: "#00B4EF"// style.colors.blueHighlight + size: 28 + text: "" + modelData.message + "" + onLinkActivated: Qt.openUrlExternally(modelData.url) + } + } + } + } + + +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index b4293d5eee..8afc60fd90 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -143,6 +143,18 @@ Item { PropertyChanges { target: createAvatarProject; visible: true } PropertyChanges { target: avatarPackagerFooter; content: createAvatarProject.footer } }, + State { + name: AvatarPackagerState.avatarDoctorDiagnose + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } + PropertyChanges { target: avatarDoctorDiagnose; visible: true } + PropertyChanges { target: avatarPackagerFooter; content: avatarDoctorDiagnose.footer } + }, + State { + name: AvatarPackagerState.avatarDoctorErrorReport + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } + PropertyChanges { target: avatarDoctorErrorReport; visible: true } + PropertyChanges { target: avatarPackagerFooter; content: avatarDoctorErrorReport.footer } + }, State { name: AvatarPackagerState.project PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; canRename: true } @@ -168,7 +180,7 @@ Item { return status; } avatarProject.reset(); - avatarPackager.state = AvatarPackagerState.project; + avatarPackager.state = AvatarPackagerState.avatarDoctorDiagnose; return status; } @@ -242,6 +254,23 @@ Item { color: "#404040" } + AvatarDoctorDiagnose { + id: avatarDoctorDiagnose + anchors.fill: parent + onErrorsChanged: { + avatarDoctorErrorReport.errors = avatarDoctorDiagnose.errors; + } + onDoneDiagnosing: { + avatarPackager.state = avatarDoctorDiagnose.errors.length > 0 ? AvatarPackagerState.avatarDoctorErrorReport + : AvatarPackagerState.project; + } + } + + AvatarDoctorErrorReport { + id: avatarDoctorErrorReport + anchors.fill: parent + } + AvatarProject { id: avatarProject colorScheme: root.colorScheme @@ -383,6 +412,7 @@ Item { title: modelData.name path: modelData.projectPath onOpen: avatarPackager.openProject(modelData.path) + hasError: modelData.hadErrors } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml index c81173a080..4a5abbb04b 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml @@ -7,4 +7,6 @@ Item { readonly property string project: "project" readonly property string createProject: "createProject" readonly property string projectUpload: "projectUpload" + readonly property string avatarDoctorDiagnose: "avatarDoctorDiagnose" + readonly property string avatarDoctorErrorReport: "avatarDoctorErrorReport" } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml index 25222c814c..21d0683fb1 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -12,6 +12,7 @@ Item { property alias title: title.text property alias path: path.text + property alias hasError: errorIcon.visible property color textColor: "#E3E3E3" property color hoverTextColor: "#121212" @@ -54,7 +55,7 @@ Item { RalewayBold { id: title - elide: "ElideRight" + elide: Text.ElideRight anchors { top: parent.top topMargin: 13 @@ -76,12 +77,24 @@ Item { right: background.right rightMargin: 16 } - elide: "ElideLeft" + elide: Text.ElideLeft horizontalAlignment: Text.AlignRight text: "" size: 20 } + HiFiGlyphs { + id: errorIcon + visible: false + text: hifi.glyphs.alert + size: 56 + color: "#EA4C5F" + anchors { + top: parent.top + right: parent.right + } + } + MouseArea { id: mouseArea anchors.fill: parent diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp new file mode 100644 index 0000000000..d2397ed21f --- /dev/null +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -0,0 +1,101 @@ +// +// AvatarDoctor.cpp +// +// +// Created by Thijs Wenker on 2/12/2019. +// Copyright 2019 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 "AvatarDoctor.h" +#include + +AvatarDoctor::AvatarDoctor(QUrl avatarFSTFileUrl) : + _avatarFSTFileUrl(std::move(avatarFSTFileUrl)) { +} + +void AvatarDoctor::startDiagnosing() { + _errors.clear(); + const auto resource = DependencyManager::get()->getGeometryResource(_avatarFSTFileUrl); + const auto resourceLoaded = [this, resource](bool success) { + // MODEL + if (!success) { + _errors.push_back({ "Model file cannot be opened", QUrl("http://www.highfidelity.com/docs") }); + emit complete(getErrors()); + return; + } + const auto avatarModel = resource.data()->getHFMModel(); + if (!avatarModel.originalURL.endsWith(".fbx")) { + _errors.push_back({ "Unsupported avatar model format", QUrl("http://www.highfidelity.com/docs") }); + emit complete(getErrors()); + return; + } + + // RIG + if (avatarModel.joints.isEmpty()) { + _errors.push_back({ "Avatar has no rig", QUrl("http://www.highfidelity.com/docs") }); + } + else { + if (avatarModel.joints.length() > 256) { + _errors.push_back({ "Avatar has over 256 bones", QUrl("http://www.highfidelity.com/docs") }); + } + // Avatar does not have Hips bone mapped + if (!avatarModel.getJointNames().contains("Hips")) { + _errors.push_back({ "Hips are not mapped", QUrl("http://www.highfidelity.com/docs") }); + } + if (!avatarModel.getJointNames().contains("Spine")) { + _errors.push_back({ "Spine is not mapped", QUrl("http://www.highfidelity.com/docs") }); + } + if (!avatarModel.getJointNames().contains("Head")) { + _errors.push_back({ "Head is not mapped", QUrl("http://www.highfidelity.com/docs") }); + } + } + + // SCALE + const float DEFAULT_HEIGHT = 1.75f; + const float RECOMMENDED_MIN_HEIGHT = DEFAULT_HEIGHT * 0.25; + const float RECOMMENDED_MAX_HEIGHT = DEFAULT_HEIGHT * 1.5; + + float avatarHeight = avatarModel.getMeshExtents().largestDimension(); + + qWarning() << "avatarHeight" << avatarHeight; + if (avatarHeight < RECOMMENDED_MIN_HEIGHT) { + _errors.push_back({ "Avatar is possibly smaller then expected.", QUrl("http://www.highfidelity.com/docs") }); + } + else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) { + _errors.push_back({ "Avatar is possibly larger then expected.", QUrl("http://www.highfidelity.com/docs") }); + } + + // BLENDSHAPES + + // TEXTURES + //avatarModel.materials. + + + emit complete(getErrors()); + }; + + if (resource) { + if (resource->isLoaded()) { + resourceLoaded(!resource->isFailed()); + } else { + connect(resource.data(), &GeometryResource::finished, this, resourceLoaded); + } + } else { + _errors.push_back({ "Model file cannot be opened", QUrl("http://www.highfidelity.com/docs") }); + emit complete(getErrors()); + } +} + +QVariantList AvatarDoctor::getErrors() const { + QVariantList result; + for (const auto& error : _errors) { + QVariantMap errorVariant; + errorVariant.insert("message", error.message); + errorVariant.insert("url", error.url); + result.append(errorVariant); + } + return result; +} diff --git a/interface/src/avatar/AvatarDoctor.h b/interface/src/avatar/AvatarDoctor.h new file mode 100644 index 0000000000..65a184af71 --- /dev/null +++ b/interface/src/avatar/AvatarDoctor.h @@ -0,0 +1,50 @@ +// +// AvatarDoctor.h +// +// +// Created by Thijs Wenker on 02/12/2019. +// Copyright 2019 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_AvatarDoctor_h +#define hifi_AvatarDoctor_h + +#include +#include +#include +#include + +struct AvatarDiagnosticResult { + +//public: + // AvatarDiagnosticResult() {} + // AvatarDiagnosticResult(QString message, QUrl url) : _message(std::move(message)), _url(std::move(url)) { } +//private: + QString message; + QUrl url; +}; +Q_DECLARE_METATYPE(AvatarDiagnosticResult) +Q_DECLARE_METATYPE(QVector) + +class AvatarDoctor : public QObject { + Q_OBJECT +public: + AvatarDoctor(QUrl avatarFSTFileUrl); + + Q_INVOKABLE void startDiagnosing(); + + Q_INVOKABLE QVariantList getErrors() const; + +signals: + void complete(QVariantList errors); + +private: + QUrl _avatarFSTFileUrl; + QVector _errors; +}; + +#endif // hifi_AvatarDoctor_h diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index fa70eee374..24f31cac9c 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -31,6 +31,9 @@ AvatarPackager::AvatarPackager() { qmlRegisterType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType>(); qRegisterMetaType(); qmlRegisterUncreatableMetaObject( AvatarProjectStatus::staticMetaObject, @@ -84,7 +87,7 @@ void AvatarPackager::addCurrentProjectToRecentProjects() { _recentProjects.removeOne(removeProject); } - const auto newRecentProject = RecentAvatarProject(_currentAvatarProject->getProjectName(), fstPath); + const auto newRecentProject = RecentAvatarProject(_currentAvatarProject->getProjectName(), fstPath, _currentAvatarProject->getHasErrors()); _recentProjects.prepend(newRecentProject); while (_recentProjects.size() > MAX_RECENT_PROJECTS) { @@ -101,6 +104,7 @@ QVariantList AvatarPackager::recentProjectsToVariantList(bool includeProjectPath QVariantMap projectVariant; projectVariant.insert("name", project.getProjectName()); projectVariant.insert("path", project.getProjectFSTPath()); + projectVariant.insert("hadErrors", project.getHadErrors()); if (includeProjectPaths) { projectVariant.insert("projectPath", project.getProjectPath()); } @@ -113,7 +117,10 @@ 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())); + _recentProjects.append(RecentAvatarProject( + map.value("name").toString(), + map.value("path").toString(), + map.value("hadErrors", false).toBool())); } } diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index ec954a60d7..13f62cb471 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -26,19 +26,23 @@ public: RecentAvatarProject() = default; - RecentAvatarProject(QString projectName, QString projectFSTPath) { + RecentAvatarProject(QString projectName, QString projectFSTPath, bool hadErrors) { _projectName = projectName; _projectFSTPath = projectFSTPath; + _hadErrors = hadErrors; } RecentAvatarProject(const RecentAvatarProject& other) { _projectName = other._projectName; _projectFSTPath = other._projectFSTPath; + _hadErrors = other._hadErrors; } QString getProjectName() const { return _projectName; } QString getProjectFSTPath() const { return _projectFSTPath; } + bool getHadErrors() const { return _hadErrors; } + QString getProjectPath() const { return QFileInfo(_projectFSTPath).absoluteDir().absolutePath(); } @@ -50,6 +54,7 @@ public: private: QString _projectName; QString _projectFSTPath; + bool _hadErrors; }; @@ -73,6 +78,8 @@ public: return AvatarProject::isValidNewProjectName(projectPath, projectName); } + Q_INVOKABLE void addCurrentProjectToRecentProjects(); + signals: void avatarProjectChanged(); void recentProjectsChanged(); @@ -84,8 +91,6 @@ private: void setAvatarProject(AvatarProject* avatarProject); - void addCurrentProjectToRecentProjects(); - AvatarProject* _currentAvatarProject { nullptr }; QVector _recentProjects; diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 728917e673..74edabd1f5 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -243,6 +243,12 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { return uploader; } +AvatarDoctor* AvatarProject::diagnose() { + auto avatarDoctor = new AvatarDoctor(QUrl(getFSTPath())); + + return avatarDoctor; +} + void AvatarProject::openInInventory() const { constexpr int TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS { 1000 }; diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 1710282a3e..f11547bdca 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 "AvatarDoctor.h" #include "ProjectFile.h" #include "FST.h" @@ -53,11 +54,14 @@ class AvatarProject : public QObject { Q_PROPERTY(QString projectFSTPath READ getFSTPath CONSTANT) Q_PROPERTY(QString projectFBXPath READ getFBXPath CONSTANT) Q_PROPERTY(QString name READ getProjectName WRITE setProjectName NOTIFY nameChanged) + Q_PROPERTY(bool hasErrors READ getHasErrors WRITE setHasErrors NOTIFY hasErrorsChanged) public: Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); Q_INVOKABLE void openInInventory() const; Q_INVOKABLE QStringList getProjectFiles() const; + Q_INVOKABLE AvatarDoctor* diagnose(); + Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } Q_INVOKABLE void setProjectName(const QString& newProjectName) { @@ -72,6 +76,8 @@ public: Q_INVOKABLE QString getFBXPath() const { return QDir::cleanPath(QDir(_projectPath).absoluteFilePath(_fst->getModelPath())); } + Q_INVOKABLE bool getHasErrors() const { return _hasErrors; } + Q_INVOKABLE void setHasErrors(bool hasErrors) { _hasErrors = hasErrors; } /** * returns the AvatarProject or a nullptr on failure. @@ -92,6 +98,7 @@ public: signals: void nameChanged(); void projectFilesChanged(); + void hasErrorsChanged(); private: AvatarProject(const QString& fstPath, const QByteArray& data); @@ -110,6 +117,8 @@ private: QDir _directory; QList _projectFiles{}; QString _projectPath; + + bool _hasErrors { false }; }; #endif // hifi_AvatarProject_h