mirror of
https://github.com/overte-org/overte.git
synced 2025-04-19 15:43:50 +02:00
create projects / style changes
This commit is contained in:
parent
ceb3b6385d
commit
cb74313de8
12 changed files with 693 additions and 131 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue