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