create projects / style changes

This commit is contained in:
Thijs Wenker 2018-12-19 19:23:24 +01:00
parent ceb3b6385d
commit cb74313de8
12 changed files with 693 additions and 131 deletions

View file

@ -15,92 +15,160 @@ Windows.ScrollingWindow {
width: 480 width: 480
height: 706 height: 706
title: "Avatar Packager" title: "Avatar Packager"
resizable: true resizable: false
opacity: parent.opacity opacity: parent.opacity
destroyOnHidden: true destroyOnHidden: true
implicitWidth: 384; implicitHeight: 640 implicitWidth: 384; implicitHeight: 640
minSize: Qt.vector2d(200, 300) minSize: Qt.vector2d(480, 706)
HifiConstants { id: hifi }
//HifiConstants { id: hifi }
Item { Item {
id: windowContent
height: pane.height height: pane.height
width: pane.width width: pane.width
anchors.fill: parent
AvatarProject { // FIXME: modal overlay does not show
id: avatarProject Rectangle {
colorScheme: root.colorScheme id: modalOverlay
visible: false
anchors.fill: parent anchors.fill: parent
z: 20000
color: "#aa031b33"
clip: true
visible: true
} }
Item { Column {
id: avatarPackagerMain id: avatarPackager
anchors.left: parent.left anchors.fill: parent
anchors.right: parent.right state: "main"
anchors.top: parent.top states: [
anchors.bottom: parent.bottom State {
RalewaySemiBold { name: "main"
id: avatarPackagerLabel PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); faqEnabled: true; backButtonEnabled: false }
size: 24; PropertyChanges { target: avatarPackagerMain; visible: true }
anchors.left: parent.left PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer }
anchors.top: parent.top },
anchors.topMargin: 25 State {
anchors.bottomMargin: 25 name: "createProject"
text: 'Avatar Packager' 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 { AvatarPackagerHeader {
id: createProjectButton id: avatarPackagerHeader
anchors.left: parent.left onBackButtonClicked: {
anchors.right: parent.right avatarPackager.state = "main"
anchors.top: avatarPackagerLabel.bottom
text: qsTr("Create Project")
colorScheme: root.colorScheme
height: 30
onClicked: function() {
} }
} }
HifiControls.Button {
id: openProjectButton
anchors.left: parent.left
anchors.right: parent.right
anchors.top: createProjectButton.bottom
text: qsTr("Open Avatar Project")
colorScheme: root.colorScheme
height: 30
onClicked: function() {
var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects";
console.log("path = " + avatarProjectsPath);
// TODO: make the dialog modal Item {
height: pane.height - avatarPackagerHeader.height - avatarPackagerFooter.height
width: pane.width
var browser = desktop.fileDialog({ Rectangle {
selectDirectory: false, anchors.fill: parent
dir: fileDialogHelper.pathToUrl(avatarProjectsPath), color: "#404040"
filter: "Avatar Project FST Files (*.fst)", }
title: "Open Project (.fst)"
});
browser.canceled.connect(function() { AvatarProject {
id: avatarProject
}); colorScheme: root.colorScheme
anchors.fill: parent
}
browser.selectedFile.connect(function(fileUrl) { CreateAvatarProject {
console.log("FOUND PATH " + fileUrl); id: createAvatarProject
let fstFilePath = fileDialogHelper.urlToPath(fileUrl); colorScheme: root.colorScheme
let currentAvatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); anchors.fill: parent
if (currentAvatarProject) { }
console.log("LOAD COMPLETE");
console.log("file dir = " + AvatarPackagerCore.currentAvatarProject.projectFolderPath); Item {
id: avatarPackagerMain
avatarPackagerMain.visible = false; visible: false
avatarProject.visible = true; 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
}
} }
} }
} }

View file

@ -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;
}
}

View file

@ -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"
}
}

View file

@ -11,36 +11,56 @@ Item {
HifiConstants { id: hifi } HifiConstants { id: hifi }
property int colorScheme; property int colorScheme
visible: true
visible: false
anchors.fill: parent anchors.fill: parent
anchors.margins: 10 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 { RalewaySemiBold {
id: avatarProjectLabel id: avatarFBXNameLabel
size: 24; size: 14
width: parent.width anchors.left: parent.left
anchors.top: parent.top
anchors.topMargin: 25 anchors.topMargin: 25
anchors.bottomMargin: 25 anchors.bottomMargin: 25
text: 'Avatar Project' text: qsTr("FBX file here")
color: "white"
} }
HifiControls.Button { HifiControls.Button {
id: openFolderButton id: openFolderButton
width: parent.width width: parent.width
anchors.top: avatarProjectLabel.bottom anchors.top: avatarFBXNameLabel.bottom
anchors.topMargin: 10 anchors.topMargin: 10
text: qsTr("Open Project Folder") text: qsTr("Open Project Folder")
colorScheme: root.colorScheme colorScheme: root.colorScheme
height: 30 height: 30
onClicked: function() { onClicked: {
fileDialogHelper.openDirectory(AvatarPackagerCore.currentAvatarProject.projectFolderPath); fileDialogHelper.openDirectory(fileDialogHelper.pathToUrl(AvatarPackagerCore.currentAvatarProject.projectFolderPath));
} }
} }
Rectangle { Rectangle {
color: 'white' color: "white"
visible: AvatarPackagerCore.currentAvatarProject !== null visible: AvatarPackagerCore.currentAvatarProject !== null
anchors.top: openFolderButton.bottom anchors.top: openFolderButton.bottom
anchors.left: parent.left anchors.left: parent.left
@ -56,15 +76,4 @@ Item {
delegate: Text { text: '<b>File:</b> ' + modelData } delegate: Text { text: '<b>File:</b> ' + 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() {
}
}
} }

View file

@ -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
}
}
}

View file

@ -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);
});
}
}
}
}

View file

@ -12,6 +12,7 @@
#include "AvatarPackager.h" #include "AvatarPackager.h"
#include <QQmlContext> #include <QQmlContext>
#include <QQmlEngine>
#include <QUrl> #include <QUrl>
#include <OffscreenUi.h> #include <OffscreenUi.h>
@ -25,6 +26,8 @@ std::once_flag setupQMLTypesFlag;
AvatarPackager::AvatarPackager() { AvatarPackager::AvatarPackager() {
std::call_once(setupQMLTypesFlag, []() { std::call_once(setupQMLTypesFlag, []() {
qmlRegisterType<FST>(); qmlRegisterType<FST>();
qRegisterMetaType<AvatarPackager*>();
qRegisterMetaType<AvatarProject*>();
}); });
} }
@ -38,12 +41,24 @@ bool AvatarPackager::open() {
return true; return true;
} }
QObject* AvatarPackager::openAvatarProject(QString avatarProjectFSTPath) { AvatarProject* AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) {
if (_currentAvatarProject) { if (_currentAvatarProject) {
//_currentAvatarProject->deleteLater(); _currentAvatarProject->deleteLater();
//_currentAvatarProject = nullptr;
} }
_currentAvatarProject = AvatarProject::openAvatarProject(avatarProjectFSTPath); _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(); emit avatarProjectChanged();
return _currentAvatarProject; return _currentAvatarProject;
} }

View file

@ -16,25 +16,28 @@
#include <QObject> #include <QObject>
#include <DependencyManager.h> #include <DependencyManager.h>
#include "FileDialogHelper.h"
#include "avatar/AvatarProject.h" #include "avatar/AvatarProject.h"
class AvatarPackager : public QObject, public Dependency { class AvatarPackager : public QObject, public Dependency {
Q_OBJECT Q_OBJECT
SINGLETON_DEPENDENCY SINGLETON_DEPENDENCY
Q_PROPERTY(QObject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged) Q_PROPERTY(AvatarProject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged)
Q_PROPERTY(QString AVATAR_PROJECTS_PATH READ getAvatarProjectsPath CONSTANT)
public: public:
AvatarPackager(); AvatarPackager();
bool open(); 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: signals:
void avatarProjectChanged(); void avatarProjectChanged();
private: private:
Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; };
//Q_INVOKABLE QObject* openAvatarProject(); Q_INVOKABLE QString getAvatarProjectsPath() const { return AvatarProject::getDefaultProjectsPath(); }
Q_INVOKABLE QObject* uploadItem(); Q_INVOKABLE QObject* uploadItem();
AvatarProject* _currentAvatarProject{ nullptr }; AvatarProject* _currentAvatarProject{ nullptr };

View file

@ -15,44 +15,93 @@
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QUrl>
#include <QDebug> #include <QDebug>
#include <QQmlEngine> #include <QQmlEngine>
#include "FBXSerializer.h"
AvatarProject* AvatarProject::openAvatarProject(const QString& path) { AvatarProject* AvatarProject::openAvatarProject(const QString& path) {
const auto pathToLower = path.toLower(); if (!path.toLower().endsWith(".fst")) {
if (pathToLower.endsWith(".fst")) { return nullptr;
QFile file{ path }; }
if (!file.open(QIODevice::ReadOnly)) { QFile file{ path };
return nullptr; if (!file.open(QIODevice::ReadOnly)) {
} return nullptr;
auto project = new AvatarProject(path, file.readAll()); }
QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership); const auto project = new AvatarProject(path, file.readAll());
return project; 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")) { std::shared_ptr<hfm::Model> hfmModel;
// TODO: Create FST here:
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) : 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(); //_projectFiles = _directory.entryList();
refreshProjectFiles(); refreshProjectFiles();
auto fileInfo = QFileInfo(_fstPath);
_projectPath = fileInfo.absoluteDir().absolutePath(); _projectPath = fileInfo.absoluteDir().absolutePath();
} }
void AvatarProject::appendDirectory(QString prefix, QDir dir) { void AvatarProject::appendDirectory(QString prefix, QDir dir) {
qDebug() << "Inside of " << prefix << dir.absolutePath(); 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)) { for (auto& entry : dir.entryInfoList({}, flags)) {
if (entry.isFile()) { if (entry.isFile()) {
_projectFiles.append(prefix + "/" + entry.fileName()); _projectFiles.append(prefix + "/" + entry.fileName());

View file

@ -21,6 +21,7 @@
#include <QFileInfo> #include <QFileInfo>
#include <QVariantHash> #include <QVariantHash>
#include <QUuid> #include <QUuid>
#include <QStandardPaths>
class AvatarProject : public QObject { class AvatarProject : public QObject {
Q_OBJECT Q_OBJECT
@ -31,6 +32,7 @@ class AvatarProject : public QObject {
Q_PROPERTY(QString projectFolderPath READ getProjectPath) Q_PROPERTY(QString projectFolderPath READ getProjectPath)
Q_PROPERTY(QString projectFSTPath READ getFSTPath) Q_PROPERTY(QString projectFSTPath READ getFSTPath)
Q_PROPERTY(QString projectFBXPath READ getFBXPath) Q_PROPERTY(QString projectFBXPath READ getFBXPath)
Q_PROPERTY(QString name READ getProjectName)
public: public:
Q_INVOKABLE bool write() { Q_INVOKABLE bool write() {
@ -38,38 +40,41 @@ public:
return false; 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. * returns the AvatarProject or a nullptr on failure.
*/ */
static AvatarProject* openAvatarProject(const QString& path); 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: private:
AvatarProject(const QString& fstPath, const QByteArray& data); AvatarProject(const QString& fstPath, const QByteArray& data);
AvatarProject(FST* fst);
~AvatarProject() { ~AvatarProject() {
// TODO: cleanup FST / AvatarProjectUploader etc. // TODO: cleanup FST / AvatarProjectUploader etc.
} }
Q_INVOKABLE QString getProjectName() const { return _fst->getName(); }
Q_INVOKABLE QString getProjectPath() const { return _projectPath; } Q_INVOKABLE QString getProjectPath() const { return _projectPath; }
Q_INVOKABLE QString getFSTPath() const { return _fstPath; } Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); }
Q_INVOKABLE QString getFBXPath() const { return _fst.getModelPath(); } Q_INVOKABLE QString getFBXPath() const { return _fst->getModelPath(); }
FST* getFST() { return &_fst; } FST* getFST() { return _fst; }
void refreshProjectFiles(); void refreshProjectFiles();
void appendDirectory(QString prefix, QDir dir); void appendDirectory(QString prefix, QDir dir);
FST _fst; FST* _fst;
QDir _directory; QDir _directory;
QStringList _projectFiles{}; QStringList _projectFiles{};
QString _projectPath; QString _projectPath;
QString _fstPath;
}; };
#endif // hifi_AvatarProject_h #endif // hifi_AvatarProject_h

View file

@ -13,21 +13,125 @@
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <hfm/HFM.h>
FST::FST(QString fstPath, QVariantHash data) : _fstPath(fstPath) { FST::FST(const QString& fstPath, QVariantHash data) : _fstPath(fstPath) {
if (data.contains("name")) { if (data.contains(NAME_FIELD)) {
_name = data["name"].toString(); _name = data[NAME_FIELD].toString();
data.remove("name"); data.remove(NAME_FIELD);
} }
if (data.contains("filename")) { if (data.contains(FILENAME_FIELD)) {
_modelPath = data["filename"].toString(); _modelPath = data[FILENAME_FIELD].toString();
data.remove("filename"); data.remove(FILENAME_FIELD);
} }
_other = data; _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 { QString FST::absoluteModelPath() const {
QFileInfo fileInfo{ _fstPath }; QFileInfo fileInfo{ _fstPath };
QDir dir{ fileInfo.absoluteDir() }; QDir dir{ fileInfo.absoluteDir() };
@ -42,4 +146,21 @@ void FST::setName(const QString& name) {
void FST::setModelPath(const QString& modelPath) { void FST::setModelPath(const QString& modelPath) {
_modelPath = modelPath; _modelPath = modelPath;
emit modelPathChanged(modelPath); emit modelPathChanged(modelPath);
} }
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;
}

View file

@ -13,6 +13,11 @@
#include <QVariantHash> #include <QVariantHash>
#include <QUuid> #include <QUuid>
#include "FSTReader.h"
namespace hfm {
class Model;
};
class FST : public QObject { class FST : public QObject {
Q_OBJECT Q_OBJECT
@ -20,7 +25,9 @@ class FST : public QObject {
Q_PROPERTY(QString modelPath READ getModelPath WRITE setModelPath NOTIFY modelPathChanged) Q_PROPERTY(QString modelPath READ getModelPath WRITE setModelPath NOTIFY modelPathChanged)
Q_PROPERTY(QUuid marketplaceID READ getMarketplaceID) Q_PROPERTY(QUuid marketplaceID READ getMarketplaceID)
public: 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; QString absoluteModelPath() const;
@ -32,6 +39,12 @@ public:
QUuid getMarketplaceID() const { return _marketplaceID; } QUuid getMarketplaceID() const { return _marketplaceID; }
QString getPath() { return _fstPath; }
QVariantHash getMapping();
bool write();
signals: signals:
void nameChanged(const QString& name); void nameChanged(const QString& name);
void modelPathChanged(const QString& modelPath); void modelPathChanged(const QString& modelPath);