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

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 }
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: '<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 <QQmlContext>
#include <QQmlEngine>
#include <QUrl>
#include <OffscreenUi.h>
@ -25,6 +26,8 @@ std::once_flag setupQMLTypesFlag;
AvatarPackager::AvatarPackager() {
std::call_once(setupQMLTypesFlag, []() {
qmlRegisterType<FST>();
qRegisterMetaType<AvatarPackager*>();
qRegisterMetaType<AvatarProject*>();
});
}
@ -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;
}

View file

@ -16,25 +16,28 @@
#include <QObject>
#include <DependencyManager.h>
#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 };

View file

@ -15,44 +15,93 @@
#include <QFile>
#include <QFileInfo>
#include <QUrl>
#include <QDebug>
#include <QQmlEngine>
#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<hfm::Model> 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());

View file

@ -21,6 +21,7 @@
#include <QFileInfo>
#include <QVariantHash>
#include <QUuid>
#include <QStandardPaths>
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

View file

@ -13,21 +13,125 @@
#include <QDir>
#include <QFileInfo>
#include <hfm/HFM.h>
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);
}
}
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 <QUuid>
#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);