From 3026fd625a5f0242fc8b129071e4374f59e289a8 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 14 Feb 2019 18:52:50 +0100 Subject: [PATCH 1/3] Avatar Doctor --- .../avatarPackager/AvatarDoctorDiagnose.qml | 125 ++++++++++++++++++ .../AvatarDoctorErrorReport.qml | 112 ++++++++++++++++ .../hifi/avatarPackager/AvatarPackagerApp.qml | 32 ++++- .../avatarPackager/AvatarPackagerState.qml | 2 + .../hifi/avatarPackager/AvatarProjectCard.qml | 17 ++- interface/src/avatar/AvatarDoctor.cpp | 101 ++++++++++++++ interface/src/avatar/AvatarDoctor.h | 50 +++++++ interface/src/avatar/AvatarPackager.cpp | 11 +- interface/src/avatar/AvatarPackager.h | 11 +- interface/src/avatar/AvatarProject.cpp | 6 + interface/src/avatar/AvatarProject.h | 9 ++ 11 files changed, 468 insertions(+), 8 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml create mode 100644 interface/src/avatar/AvatarDoctor.cpp create mode 100644 interface/src/avatar/AvatarDoctor.h 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 From 556a55ff160bc936ef2db2fc58edc9f9bf2b7bbc Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 15 Feb 2019 20:55:27 +0100 Subject: [PATCH 2/3] Better scale and texture checks --- .../avatarPackager/AvatarDoctorDiagnose.qml | 64 +++++---- .../avatarPackager/CreateAvatarProject.qml | 2 +- interface/src/avatar/AvatarDoctor.cpp | 134 +++++++++++++++--- interface/src/avatar/AvatarDoctor.h | 5 + interface/src/avatar/AvatarProject.cpp | 4 +- 5 files changed, 151 insertions(+), 58 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml index d329b903bd..302930dee0 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorDiagnose.qml @@ -4,39 +4,59 @@ import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 Item { - id: diagnosingScreen + id: root visible: false property var avatarDoctor: null property var errors: [] + property int minimumDiagnoseTimeMS: 1000 + signal doneDiagnosing onVisibleChanged: { - if (!diagnosingScreen.visible) { - //if (debugDelay.running) { - // debugDelay.stop(); - //} + if (root.avatarDoctor !== null) { + root.avatarDoctor.complete.disconnect(_private.avatarDoctorComplete); + root.avatarDoctor = null; + } + if (doneTimer.running) { + doneTimer.stop(); + } + + if (!root.visible) { return; } - //debugDelay.start(); - avatarDoctor = AvatarPackagerCore.currentAvatarProject.diagnose(); - avatarDoctor.complete.connect(function(errors) { + + root.avatarDoctor = AvatarPackagerCore.currentAvatarProject.diagnose(); + root.avatarDoctor.complete.connect(this, _private.avatarDoctorComplete); + _private.startTime = Date.now(); + root.avatarDoctor.startDiagnosing(); + } + + QtObject { + id: _private + property real startTime: 0 + + function avatarDoctorComplete(errors) { + if (!root.visible) { + return; + } + console.warn("avatarDoctor.complete " + JSON.stringify(errors)); - diagnosingScreen.errors = errors; + root.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 + let timeSpendDiagnosingMS = Date.now() - _private.startTime; + let timeLeftMS = root.minimumDiagnoseTimeMS - timeSpendDiagnosingMS; + doneTimer.interval = timeLeftMS < 0 ? 0 : timeLeftMS; doneTimer.start(); - }); - avatarDoctor.startDiagnosing(); + } } Timer { id: doneTimer - interval: 1 repeat: false running: false onTriggered: { @@ -44,24 +64,6 @@ Item { } } -/* - 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 diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml index c299417c27..a0149b118f 100644 --- a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -32,7 +32,7 @@ Item { return; } avatarProject.reset(); - avatarPackager.state = AvatarPackagerState.project; + avatarPackager.state = AvatarPackagerState.avatarDoctorDiagnose; } } } diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp index d2397ed21f..c8f5d52336 100644 --- a/interface/src/avatar/AvatarDoctor.cpp +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -11,6 +11,8 @@ #include "AvatarDoctor.h" #include +#include +#include AvatarDoctor::AvatarDoctor(QUrl avatarFSTFileUrl) : _avatarFSTFileUrl(std::move(avatarFSTFileUrl)) { @@ -18,63 +20,149 @@ AvatarDoctor::AvatarDoctor(QUrl avatarFSTFileUrl) : void AvatarDoctor::startDiagnosing() { _errors.clear(); + + _externalTextureCount = 0; + _checkedTextureCount = 0; + _missingTextureCount = 0; + _unsupportedTextureCount = 0; + const auto resource = DependencyManager::get()->getGeometryResource(_avatarFSTFileUrl); - const auto resourceLoaded = [this, resource](bool success) { + resource->refresh(); + const QUrl DEFAULT_URL = QUrl("https://docs.highfidelity.com/create/avatars/create-avatars.html#create-your-own-avatar"); + const auto resourceLoaded = [this, resource, DEFAULT_URL](bool success) { // MODEL if (!success) { - _errors.push_back({ "Model file cannot be opened", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Model file cannot be opened", DEFAULT_URL }); emit complete(getErrors()); return; } + const auto model = resource.data(); const auto avatarModel = resource.data()->getHFMModel(); if (!avatarModel.originalURL.endsWith(".fbx")) { - _errors.push_back({ "Unsupported avatar model format", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Unsupported avatar model format", DEFAULT_URL }); emit complete(getErrors()); return; } // RIG if (avatarModel.joints.isEmpty()) { - _errors.push_back({ "Avatar has no rig", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Avatar has no rig", DEFAULT_URL }); } else { if (avatarModel.joints.length() > 256) { - _errors.push_back({ "Avatar has over 256 bones", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Avatar has over 256 bones", DEFAULT_URL }); } // 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") }); + _errors.push_back({ "Hips are not mapped", DEFAULT_URL }); } if (!avatarModel.getJointNames().contains("Spine")) { - _errors.push_back({ "Spine is not mapped", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Spine is not mapped", DEFAULT_URL }); } if (!avatarModel.getJointNames().contains("Head")) { - _errors.push_back({ "Head is not mapped", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Head is not mapped", DEFAULT_URL }); } } // 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; + const float RECOMMENDED_MIN_HEIGHT = DEFAULT_AVATAR_HEIGHT * 0.25f; + const float RECOMMENDED_MAX_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; + + const float avatarHeight = avatarModel.bindExtents.largestDimension(); - float avatarHeight = avatarModel.getMeshExtents().largestDimension(); - - qWarning() << "avatarHeight" << avatarHeight; + qDebug() << "avatarHeight" << avatarHeight; + qDebug() << "defined Scale =" << model->getMapping()["scale"].toFloat(); if (avatarHeight < RECOMMENDED_MIN_HEIGHT) { - _errors.push_back({ "Avatar is possibly smaller then expected.", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Avatar is possibly smaller then expected.", DEFAULT_URL }); + } else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) { + _errors.push_back({ "Avatar is possibly larger then expected.", DEFAULT_URL }); } - 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. + QStringList externalTextures{}; + QSet textureNames{}; + auto addTextureToList = [&externalTextures](hfm::Texture texture) mutable { + if (!texture.filename.isEmpty() && texture.content.isEmpty() && !externalTextures.contains(texture.name)) { + externalTextures << texture.name; + } + }; + + foreach(const HFMMaterial material, avatarModel.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); + } + if (!externalTextures.empty()) { + // Check External Textures: + auto modelTexturesURLs = model->getTextures(); + _externalTextureCount = externalTextures.length(); + foreach(const QString textureKey, externalTextures) { + if (!modelTexturesURLs.contains(textureKey)) { + _missingTextureCount++; + _checkedTextureCount++; + continue; + } - emit complete(getErrors()); + const QUrl textureURL = modelTexturesURLs[textureKey].toUrl(); + + auto textureResource = DependencyManager::get()->getTexture(textureURL); + auto checkTextureLoadingComplete = [this, DEFAULT_URL] () mutable { + qDebug() << "checkTextureLoadingComplete" << _checkedTextureCount << "/" << _externalTextureCount; + + if (_checkedTextureCount == _externalTextureCount) { + if (_missingTextureCount == 1) { + _errors.push_back({ tr("Missing %n texture(s).","", _missingTextureCount), DEFAULT_URL }); + } + if (_unsupportedTextureCount > 0) { + _errors.push_back({ tr("%n unsupported texture(s) found.", "", _unsupportedTextureCount), DEFAULT_URL }); + } + emit complete(getErrors()); + } + }; + + auto textureLoaded = [this, textureResource, checkTextureLoadingComplete] (bool success) mutable { + if (!success) { + auto normalizedURL = DependencyManager::get()->normalizeURL(textureResource->getURL()); + if (normalizedURL.isLocalFile()) { + QFile textureFile(normalizedURL.toLocalFile()); + if (textureFile.exists()) { + _unsupportedTextureCount++; + } else { + _missingTextureCount++; + } + } else { + _missingTextureCount++; + } + } + _checkedTextureCount++; + checkTextureLoadingComplete(); + }; + + if (textureResource) { + textureResource->refresh(); + if (textureResource->isLoaded()) { + textureLoaded(!textureResource->isFailed()); + } else { + connect(textureResource.data(), &NetworkTexture::finished, this, textureLoaded); + } + } else { + _missingTextureCount++; + _checkedTextureCount++; + checkTextureLoadingComplete(); + } + } + } else { + emit complete(getErrors()); + } }; if (resource) { @@ -84,7 +172,7 @@ void AvatarDoctor::startDiagnosing() { connect(resource.data(), &GeometryResource::finished, this, resourceLoaded); } } else { - _errors.push_back({ "Model file cannot be opened", QUrl("http://www.highfidelity.com/docs") }); + _errors.push_back({ "Model file cannot be opened", DEFAULT_URL }); emit complete(getErrors()); } } diff --git a/interface/src/avatar/AvatarDoctor.h b/interface/src/avatar/AvatarDoctor.h index 65a184af71..f11bc7377c 100644 --- a/interface/src/avatar/AvatarDoctor.h +++ b/interface/src/avatar/AvatarDoctor.h @@ -45,6 +45,11 @@ signals: private: QUrl _avatarFSTFileUrl; QVector _errors; + + int _externalTextureCount = 0; + int _checkedTextureCount = 0; + int _missingTextureCount = 0; + int _unsupportedTextureCount = 0; }; #endif // hifi_AvatarDoctor_h diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 74edabd1f5..b020cdb627 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -244,9 +244,7 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { } AvatarDoctor* AvatarProject::diagnose() { - auto avatarDoctor = new AvatarDoctor(QUrl(getFSTPath())); - - return avatarDoctor; + return new AvatarDoctor(QUrl(getFSTPath())); } void AvatarProject::openInInventory() const { From e900d3784be72a9f9576ac9d835b4878e895fe2d Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 15 Feb 2019 23:58:42 +0100 Subject: [PATCH 3/3] fixes --- .../AvatarDoctorErrorReport.qml | 22 ++++++---------- interface/src/avatar/AvatarDoctor.cpp | 25 ++++++++++++------- interface/src/avatar/AvatarDoctor.h | 8 ++---- interface/src/avatar/AvatarPackager.cpp | 2 -- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml index 8811ba48a3..73c5e34d13 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarDoctorErrorReport.qml @@ -21,7 +21,6 @@ Item { height: 40 width: 134 text: qsTr("Try Again") - // colorScheme: root.colorScheme onClicked: { avatarPackager.state = AvatarPackagerState.avatarDoctorDiagnose; } @@ -49,7 +48,7 @@ Item { color: "#EA4C5F" anchors { top: parent.top - //topMargin: 73 + topMargin: -20 horizontalCenter: parent.horizontalCenter } } @@ -60,7 +59,7 @@ Item { right: parent.right bottom: parent.bottom top: errorReportIcon.bottom - topMargin: 27 + topMargin: -40 leftMargin: 13 rightMargin: 13 } @@ -68,15 +67,6 @@ Item { 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 @@ -89,6 +79,7 @@ Item { anchors { top: parent.top left: parent.left + leftMargin: -5 } } @@ -96,17 +87,18 @@ Item { id: errorLink anchors { top: parent.top + topMargin: 5 left: errorIcon.right right: parent.right } - linkColor: "#00B4EF"// style.colors.blueHighlight + color: "#00B4EF" + linkColor: "#00B4EF" size: 28 text: "" + modelData.message + "" onLinkActivated: Qt.openUrlExternally(modelData.url) + elide: Text.ElideRight } } } } - - } diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp index c8f5d52336..b528441be7 100644 --- a/interface/src/avatar/AvatarDoctor.cpp +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -14,11 +14,22 @@ #include #include + AvatarDoctor::AvatarDoctor(QUrl avatarFSTFileUrl) : - _avatarFSTFileUrl(std::move(avatarFSTFileUrl)) { + _avatarFSTFileUrl(avatarFSTFileUrl) { + + connect(this, &AvatarDoctor::complete, this, [this](QVariantList errors) { + _isDiagnosing = false; + }); } void AvatarDoctor::startDiagnosing() { + if (_isDiagnosing) { + // One diagnose at a time for now + return; + } + _isDiagnosing = true; + _errors.clear(); _externalTextureCount = 0; @@ -47,8 +58,7 @@ void AvatarDoctor::startDiagnosing() { // RIG if (avatarModel.joints.isEmpty()) { _errors.push_back({ "Avatar has no rig", DEFAULT_URL }); - } - else { + } else { if (avatarModel.joints.length() > 256) { _errors.push_back({ "Avatar has over 256 bones", DEFAULT_URL }); } @@ -69,13 +79,10 @@ void AvatarDoctor::startDiagnosing() { const float RECOMMENDED_MAX_HEIGHT = DEFAULT_AVATAR_HEIGHT * 1.5f; const float avatarHeight = avatarModel.bindExtents.largestDimension(); - - qDebug() << "avatarHeight" << avatarHeight; - qDebug() << "defined Scale =" << model->getMapping()["scale"].toFloat(); if (avatarHeight < RECOMMENDED_MIN_HEIGHT) { - _errors.push_back({ "Avatar is possibly smaller then expected.", DEFAULT_URL }); + _errors.push_back({ "Avatar is possibly too small.", DEFAULT_URL }); } else if (avatarHeight > RECOMMENDED_MAX_HEIGHT) { - _errors.push_back({ "Avatar is possibly larger then expected.", DEFAULT_URL }); + _errors.push_back({ "Avatar is possibly too large.", DEFAULT_URL }); } // TEXTURES @@ -119,7 +126,7 @@ void AvatarDoctor::startDiagnosing() { qDebug() << "checkTextureLoadingComplete" << _checkedTextureCount << "/" << _externalTextureCount; if (_checkedTextureCount == _externalTextureCount) { - if (_missingTextureCount == 1) { + if (_missingTextureCount > 0) { _errors.push_back({ tr("Missing %n texture(s).","", _missingTextureCount), DEFAULT_URL }); } if (_unsupportedTextureCount > 0) { diff --git a/interface/src/avatar/AvatarDoctor.h b/interface/src/avatar/AvatarDoctor.h index f11bc7377c..bebec32542 100644 --- a/interface/src/avatar/AvatarDoctor.h +++ b/interface/src/avatar/AvatarDoctor.h @@ -13,17 +13,11 @@ #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; }; @@ -50,6 +44,8 @@ private: int _checkedTextureCount = 0; int _missingTextureCount = 0; int _unsupportedTextureCount = 0; + + bool _isDiagnosing = false; }; #endif // hifi_AvatarDoctor_h diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index 24f31cac9c..90def7ad43 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -32,8 +32,6 @@ AvatarPackager::AvatarPackager() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); - qRegisterMetaType(); - qRegisterMetaType>(); qRegisterMetaType(); qmlRegisterUncreatableMetaObject( AvatarProjectStatus::staticMetaObject,