From cb74313de8210710e6666836b838e1456786f487 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 19 Dec 2018 19:23:24 +0100 Subject: [PATCH] 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);