// // AvatarProject.cpp // // // Created by Thijs Wenker on 12/7/2018 // Copyright 2018 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "AvatarProject.h" #include #include #include #include #include #include "FBXSerializer.h" #include #include "scripting/HMDScriptingInterface.h" AvatarProject* AvatarProject::openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status) { status = AvatarProjectStatus::NONE; if (!path.toLower().endsWith(".fst")) { status = AvatarProjectStatus::ERROR_OPEN_INVALID_FILE_TYPE; return nullptr; } QFileInfo fstFileInfo{ path }; if (!fstFileInfo.absoluteDir().exists()) { status = AvatarProjectStatus::ERROR_OPEN_PROJECT_FOLDER; return nullptr; } if (!fstFileInfo.exists()) { status = AvatarProjectStatus::ERROR_OPEN_FIND_FST; return nullptr; } QFile file{ fstFileInfo.filePath() }; if (!file.open(QIODevice::ReadOnly)) { status = AvatarProjectStatus::ERROR_OPEN_OPEN_FST; return nullptr; } const auto project = new AvatarProject(path, file.readAll()); QFileInfo fbxFileInfo{ project->getFBXPath() }; if (!fbxFileInfo.exists()) { project->deleteLater(); status = AvatarProjectStatus::ERROR_OPEN_FIND_MODEL; return nullptr; } QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership); status = AvatarProjectStatus::SUCCESS; return project; } AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder, AvatarProjectStatus::AvatarProjectStatus& status) { status = AvatarProjectStatus::NONE; if (!isValidNewProjectName(projectsFolder, avatarProjectName)) { status = AvatarProjectStatus::ERROR_CREATE_PROJECT_NAME; return nullptr; } QDir projectDir(projectsFolder + "/" + avatarProjectName); if (!projectDir.mkpath(".")) { status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES; return nullptr; } QDir projectTexturesDir(projectDir.path() + "/textures"); if (!projectTexturesDir.mkpath(".")) { status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES; return nullptr; } QDir projectScriptsDir(projectDir.path() + "/scripts"); if (!projectScriptsDir.mkpath(".")) { status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES; return nullptr; } const auto fileName = QFileInfo(avatarModelPath).fileName(); const auto newModelPath = projectDir.absoluteFilePath(fileName); const auto newFSTPath = projectDir.absoluteFilePath("avatar.fst"); QFile::copy(avatarModelPath, newModelPath); QFileInfo fbxInfo{ newModelPath }; if (!fbxInfo.exists() || !fbxInfo.isFile()) { status = AvatarProjectStatus::ERROR_CREATE_FIND_MODEL; return nullptr; } QFile fbx{ fbxInfo.filePath() }; if (!fbx.open(QIODevice::ReadOnly)) { status = AvatarProjectStatus::ERROR_CREATE_OPEN_MODEL; return nullptr; } std::shared_ptr hfmModel; try { const QByteArray fbxContents = fbx.readAll(); hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath()); } catch (const QString& error) { Q_UNUSED(error) status = AvatarProjectStatus::ERROR_CREATE_READ_MODEL; return nullptr; } QStringList textures{}; auto addTextureToList = [&textures](hfm::Texture texture) mutable { if (!texture.filename.isEmpty() && texture.content.isEmpty() && !textures.contains(texture.filename)) { textures << texture.filename; } }; foreach(const HFMMaterial material, hfmModel->materials) { addTextureToList(material.normalTexture); addTextureToList(material.albedoTexture); addTextureToList(material.opacityTexture); addTextureToList(material.glossTexture); addTextureToList(material.roughnessTexture); addTextureToList(material.specularTexture); addTextureToList(material.metallicTexture); addTextureToList(material.emissiveTexture); addTextureToList(material.occlusionTexture); addTextureToList(material.scatteringTexture); addTextureToList(material.lightmapTexture); } QDir textureDir(textureFolder.isEmpty() ? fbxInfo.absoluteDir() : textureFolder); for (const auto& texture : textures) { QString sourcePath = textureDir.path() + "/" + texture; QString targetPath = projectTexturesDir.path() + "/" + texture; QFileInfo sourceTexturePath(sourcePath); if (sourceTexturePath.exists()) { QFile::copy(sourcePath, targetPath); } } auto fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel); fst->setName(avatarProjectName); if (!fst->write()) { status = AvatarProjectStatus::ERROR_CREATE_WRITE_FST; return nullptr; } status = AvatarProjectStatus::SUCCESS; return new AvatarProject(fst); } QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) const { QStringList result{}; constexpr auto flags = QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; if (!scriptsDir.exists()) { return result; } for (const auto& script : scriptsDir.entryInfoList({}, flags)) { if (script.fileName().toLower().endsWith(".js")) { result.push_back("scripts/" + script.fileName()); } } return result; } bool AvatarProject::isValidNewProjectName(const QString& projectPath, const QString& projectName) { if (projectPath.trimmed().isEmpty() || projectName.trimmed().isEmpty()) { return false; } QDir dir(projectPath + "/" + projectName); return !dir.exists(); } AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : AvatarProject::AvatarProject(new FST(fstPath, FSTReader::readMapping(data))) { } AvatarProject::AvatarProject(FST* fst) { _fst = fst; auto fileInfo = QFileInfo(getFSTPath()); _directory = fileInfo.absoluteDir(); _fst->setScriptPaths(getScriptPaths(QDir(_directory.path() + "/scripts"))); _fst->write(); refreshProjectFiles(); _projectPath = fileInfo.absoluteDir().absolutePath(); } void AvatarProject::appendDirectory(const QString& prefix, const QDir& dir) { constexpr auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; for (auto& entry : dir.entryInfoList({}, flags)) { if (entry.isFile()) { _projectFiles.append({ entry.absoluteFilePath(), prefix + entry.fileName() }); } else if (entry.isDir()) { appendDirectory(prefix + entry.fileName() + "/", entry.absoluteFilePath()); } } } void AvatarProject::refreshProjectFiles() { _projectFiles.clear(); appendDirectory("", _directory); } QStringList AvatarProject::getProjectFiles() const { QStringList paths; for (auto& path : _projectFiles) { paths.append(path.relativePath); } return paths; } MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { QUuid itemID; if (updateExisting) { itemID = _fst->getMarketplaceID(); } auto uploader = new MarketplaceItemUploader(getProjectName(), "", QFileInfo(getFSTPath()).fileName(), itemID, _projectFiles); connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() { if (uploader->getError() == MarketplaceItemUploader::Error::None) { _fst->setMarketplaceID(uploader->getMarketplaceID()); _fst->write(); } }); return uploader; } void AvatarProject::openInInventory() const { constexpr int TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS { 1000 }; auto tablet = dynamic_cast( DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml"); DependencyManager::get()->openTablet(); tablet->getTabletRoot()->forceActiveFocus(); auto name = getProjectName(); // I'm not a fan of this, but it's the only current option. QTimer::singleShot(TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS, [name, tablet]() { tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } })); }); }