diff --git a/examples/editEntities.js b/examples/editEntities.js index faef875d9b..c236336266 100644 --- a/examples/editEntities.js +++ b/examples/editEntities.js @@ -95,6 +95,21 @@ var isActive = false; var placingEntityID = null; +IMPORTING_SVO_OVERLAY_WIDTH = 130; +IMPORTING_SVO_OVERLAY_HEIGHT = 30; +IMPORTING_SVO_OVERLAY_MARGIN = 6; +var importingSVOOverlay = Overlays.addOverlay("text", { + font: { size: 14 }, + text: "Importing SVO...", + x: Window.innerWidth - IMPORTING_SVO_OVERLAY_WIDTH - IMPORTING_SVO_OVERLAY_MARGIN, + y: Window.innerHeight - IMPORTING_SVO_OVERLAY_HEIGHT - IMPORTING_SVO_OVERLAY_MARGIN, + width: IMPORTING_SVO_OVERLAY_WIDTH, + height: IMPORTING_SVO_OVERLAY_HEIGHT, + backgroundColor: { red: 80, green: 80, blue: 80 }, + backgroundAlpha: 0.7, + visible: false, +}); + var toolBar = (function () { var that = {}, toolBar, @@ -753,6 +768,8 @@ Script.scriptEnding.connect(function() { tooltip.cleanup(); selectionDisplay.cleanup(); Entities.setLightsArePickable(originalLightsArePickable); + + Overlays.deleteOverlay(importingSVOOverlay); }); // Do some stuff regularly, like check for placement of various overlays @@ -816,24 +833,7 @@ function handeMenuEvent(menuItem) { } if (importURL) { - var success = Clipboard.importEntities(importURL); - - if (success) { - var distance = cameraManager.enabled ? cameraManager.zoomDistance : DEFAULT_ENTITY_DRAG_DROP_DISTANCE; - var direction = Quat.getFront(Camera.orientation); - var offset = Vec3.multiply(distance, direction); - var position = Vec3.sum(Camera.position, offset); - - position.x = Math.max(0, position.x); - position.y = Math.max(0, position.y); - position.z = Math.max(0, position.z); - - var pastedEntityIDs = Clipboard.pasteEntities(position); - - selectionManager.setSelections(pastedEntityIDs); - } else { - Window.alert("There was an error importing the entity file."); - } + importSVO(importURL); } } else if (menuItem == "Entity List...") { entityListTool.toggleVisible(); @@ -841,6 +841,34 @@ function handeMenuEvent(menuItem) { tooltip.show(false); } +function importSVO(importURL) { + Overlays.editOverlay(importingSVOOverlay, { visible: true }); + + var success = Clipboard.importEntities(importURL); + + if (success) { + var distance = cameraManager.enabled ? cameraManager.zoomDistance : DEFAULT_ENTITY_DRAG_DROP_DISTANCE; + var direction = Quat.getFront(Camera.orientation); + var offset = Vec3.multiply(distance, direction); + var position = Vec3.sum(Camera.position, offset); + + position.x = Math.max(0, position.x); + position.y = Math.max(0, position.y); + position.z = Math.max(0, position.z); + + var pastedEntityIDs = Clipboard.pasteEntities(position); + + if (isActive) { + selectionManager.setSelections(pastedEntityIDs); + } + } else { + Window.alert("There was an error importing the entity file."); + } + + Overlays.editOverlay(importingSVOOverlay, { visible: false }); +} +Window.svoImportRequested.connect(importSVO); + Menu.menuItemEvent.connect(handeMenuEvent); Controller.keyPressEvent.connect(function(event) { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1e21144546..3d246f64d0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -91,7 +91,7 @@ #include "InterfaceVersion.h" #include "LODManager.h" #include "Menu.h" -#include "ModelUploader.h" +#include "ModelPackager.h" #include "Util.h" #include "avatar/AvatarManager.h" @@ -244,6 +244,7 @@ bool setupEssentials(int& argc, char** argv) { auto bandwidthRecorder = DependencyManager::set(); auto resouceCacheSharedItems = DependencyManager::set(); auto entityScriptingInterface = DependencyManager::set(); + auto windowScriptingInterface = DependencyManager::set(); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) auto speechRecognizer = DependencyManager::set(); #endif @@ -882,9 +883,15 @@ bool Application::event(QEvent* event) { if (event->type() == QEvent::FileOpen) { QFileOpenEvent* fileEvent = static_cast(event); + + QUrl url = fileEvent->url(); - if (!fileEvent->url().isEmpty()) { - DependencyManager::get()->handleLookupString(fileEvent->url().toString()); + if (!url.isEmpty()) { + if (url.scheme() == HIFI_URL_SCHEME) { + DependencyManager::get()->handleLookupString(fileEvent->url().toString()); + } else if (url.url().toLower().endsWith(SVO_EXTENSION)) { + emit svoImportRequested(url.url()); + } } return false; @@ -1451,6 +1458,10 @@ void Application::dropEvent(QDropEvent *event) { if (url.url().toLower().endsWith(SNAPSHOT_EXTENSION)) { snapshotPath = url.toLocalFile(); break; + } else if (url.url().toLower().endsWith(SVO_EXTENSION)) { + emit svoImportRequested(url.url()); + event->acceptProposedAction(); + return; } } @@ -3528,7 +3539,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri qScriptRegisterMetaType(scriptEngine, RayToOverlayIntersectionResultToScriptValue, RayToOverlayIntersectionResultFromScriptValue); - QScriptValue windowValue = scriptEngine->registerGlobalObject("Window", WindowScriptingInterface::getInstance()); + QScriptValue windowValue = scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter, windowValue); // register `location` on the global object. @@ -3716,20 +3727,8 @@ void Application::toggleRunningScriptsWidget() { } } -void Application::uploadHead() { - ModelUploader::uploadHead(); -} - -void Application::uploadSkeleton() { - ModelUploader::uploadSkeleton(); -} - -void Application::uploadAttachment() { - ModelUploader::uploadAttachment(); -} - -void Application::uploadEntity() { - ModelUploader::uploadEntity(); +void Application::packageModel() { + ModelPackager::package(); } void Application::openUrl(const QUrl& url) { diff --git a/interface/src/Application.h b/interface/src/Application.h index d8d9132de9..bcd31fcd51 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -94,6 +94,7 @@ static const float NODE_KILLED_GREEN = 0.0f; static const float NODE_KILLED_BLUE = 0.0f; static const QString SNAPSHOT_EXTENSION = ".jpg"; +static const QString SVO_EXTENSION = ".svo"; static const float BILLBOARD_FIELD_OF_VIEW = 30.0f; // degrees static const float BILLBOARD_DISTANCE = 5.56f; // meters @@ -315,6 +316,8 @@ signals: void scriptLocationChanged(const QString& newPath); + void svoImportRequested(const QString& url); + public slots: void domainChanged(const QString& domainHostname); void updateWindowTitle(); @@ -340,11 +343,8 @@ public slots: void loadDefaultScripts(); void toggleRunningScriptsWidget(); void saveScripts(); - - void uploadHead(); - void uploadSkeleton(); - void uploadAttachment(); - void uploadEntity(); + + void packageModel(); void openUrl(const QUrl& url); diff --git a/interface/src/GLCanvas.cpp b/interface/src/GLCanvas.cpp index b72c00c779..4ece8f0857 100644 --- a/interface/src/GLCanvas.cpp +++ b/interface/src/GLCanvas.cpp @@ -170,7 +170,8 @@ void GLCanvas::wheelEvent(QWheelEvent* event) { void GLCanvas::dragEnterEvent(QDragEnterEvent* event) { const QMimeData *mimeData = event->mimeData(); foreach (QUrl url, mimeData->urls()) { - if (url.url().toLower().endsWith(SNAPSHOT_EXTENSION)) { + auto lower = url.url().toLower(); + if (lower.endsWith(SNAPSHOT_EXTENSION) || lower.endsWith(SVO_EXTENSION)) { event->acceptProposedAction(); break; } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 3a1e10ac4a..2823e8eb23 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -105,16 +105,6 @@ Menu::Menu() { addActionToQMenuAndActionHash(fileMenu, MenuOption::CopyPath, 0, addressManager.data(), SLOT(copyPath())); - addDisabledActionAndSeparator(fileMenu, "Upload Avatar Model"); - addActionToQMenuAndActionHash(fileMenu, MenuOption::UploadHead, 0, - qApp, SLOT(uploadHead())); - addActionToQMenuAndActionHash(fileMenu, MenuOption::UploadSkeleton, 0, - qApp, SLOT(uploadSkeleton())); - addActionToQMenuAndActionHash(fileMenu, MenuOption::UploadAttachment, 0, - qApp, SLOT(uploadAttachment())); - addActionToQMenuAndActionHash(fileMenu, MenuOption::UploadEntity, 0, - qApp, SLOT(uploadEntity())); - addActionToQMenuAndActionHash(fileMenu, MenuOption::Quit, Qt::CTRL | Qt::Key_Q, @@ -180,6 +170,9 @@ Menu::Menu() { Qt::Key_Apostrophe, qApp, SLOT(resetSensors())); + + addActionToQMenuAndActionHash(toolsMenu, MenuOption::PackageModel, 0, + qApp, SLOT(packageModel())); QMenu* avatarMenu = addMenu("Avatar"); QObject* avatar = DependencyManager::get()->getMyAvatar(); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index ce8102443c..fc1347fa27 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -251,10 +251,7 @@ namespace MenuOption { const QString ToolWindow = "Tool Window"; const QString TransmitterDrive = "Transmitter Drive"; const QString TurnWithHead = "Turn using Head"; - const QString UploadAttachment = "Upload Attachment Model"; - const QString UploadEntity = "Upload Entity Model"; - const QString UploadHead = "Upload Head Model"; - const QString UploadSkeleton = "Upload Skeleton Model"; + const QString PackageModel = "Package Model"; const QString UserInterface = "User Interface"; const QString Visage = "Visage"; const QString Wireframe = "Wireframe"; diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp new file mode 100644 index 0000000000..49d4ae566f --- /dev/null +++ b/interface/src/ModelPackager.cpp @@ -0,0 +1,397 @@ +// +// ModelPackager.cpp +// +// +// Created by Clement on 3/9/15. +// Copyright 2015 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 +#include +#include +#include + +#include "ModelSelector.h" +#include "ModelPropertiesDialog.h" + +#include "ModelPackager.h" + +static const int MAX_TEXTURE_SIZE = 1024; + +void copyDirectoryContent(QDir& from, QDir& to) { + for (auto entry : from.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot | + QDir::NoSymLinks | QDir::Readable)) { + if (entry.isDir()) { + to.mkdir(entry.fileName()); + from.cd(entry.fileName()); + to.cd(entry.fileName()); + copyDirectoryContent(from, to); + from.cdUp(); + to.cdUp(); + } else { // Files + QFile file(entry.absoluteFilePath()); + QString newPath = to.absolutePath() + "/" + entry.fileName(); + if (to.exists(entry.fileName())) { + QFile overridenFile(newPath); + overridenFile.remove(); + } + file.copy(newPath); + } + } +} + +bool ModelPackager::package() { + ModelPackager packager; + if (!packager.selectModel()) { + return false; + } + if (!packager.loadModel()) { + return false; + } + if (!packager.editProperties()) { + return false; + } + if (!packager.zipModel()) { + return false; + } + return true; +} + +bool ModelPackager::selectModel() { + ModelSelector selector; + if(selector.exec() == QDialog::Accepted) { + _modelFile = selector.getFileInfo(); + _modelType = selector.getModelType(); + return true; + } + return false; +} + +bool ModelPackager::loadModel() { + // First we check the FST file (if any) + if (_modelFile.completeSuffix().contains("fst")) { + QFile fst(_modelFile.filePath()); + if (!fst.open(QFile::ReadOnly | QFile::Text)) { + QMessageBox::warning(NULL, + QString("ModelPackager::loadModel()"), + QString("Could not open FST file %1").arg(_modelFile.filePath()), + QMessageBox::Ok); + qWarning() << QString("ModelPackager::loadModel(): Could not open FST file %1").arg(_modelFile.filePath()); + return false; + } + qDebug() << "Reading FST file : " << _modelFile.filePath(); + _mapping = readMapping(fst.readAll()); + fst.close(); + + _fbxInfo = QFileInfo(_modelFile.path() + "/" + _mapping.value(FILENAME_FIELD).toString()); + } else { + _fbxInfo = QFileInfo(_modelFile.filePath()); + } + + // open the fbx file + QFile fbx(_fbxInfo.filePath()); + if (!_fbxInfo.exists() || !_fbxInfo.isFile() || !fbx.open(QIODevice::ReadOnly)) { + QMessageBox::warning(NULL, + QString("ModelPackager::loadModel()"), + QString("Could not open FBX file %1").arg(_fbxInfo.filePath()), + QMessageBox::Ok); + qWarning() << QString("ModelPackager::loadModel(): Could not open FBX file %1").arg(_fbxInfo.filePath()); + return false; + } + qDebug() << "Reading FBX file : " << _fbxInfo.filePath(); + QByteArray fbxContents = fbx.readAll(); + _geometry = readFBX(fbxContents, QVariantHash()); + + // make sure we have some basic mappings + populateBasicMapping(_mapping, _fbxInfo.filePath(), _geometry); + return true; +} + +bool ModelPackager::editProperties() { + // open the dialog to configure the rest + ModelPropertiesDialog properties(_modelType, _mapping, _modelFile.path(), _geometry); + if (properties.exec() == QDialog::Rejected) { + return false; + } + _mapping = properties.getMapping(); + + // Make sure that a mapping for the root joint has been specified + QVariantHash joints = _mapping.value(JOINT_FIELD).toHash(); + if (!joints.contains("jointRoot")) { + qWarning() << QString("%1 root joint not configured for skeleton.").arg(_modelFile.fileName()); + + QString message = "Your did not configure a root joint for your skeleton model.\n\nThe upload will be canceled."; + QMessageBox msgBox; + msgBox.setWindowTitle("Model Upload"); + msgBox.setText(message); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setIcon(QMessageBox::Warning); + msgBox.exec(); + + return false; + } + + return true; +} + +bool ModelPackager::zipModel() { + QTemporaryDir dir; + dir.setAutoRemove(true); + QDir tempDir(dir.path()); + + QByteArray nameField = _mapping.value(NAME_FIELD).toByteArray(); + tempDir.mkpath(nameField + "/textures"); + QDir fbxDir(tempDir.path() + "/" + nameField); + QDir texDir(fbxDir.path() + "/textures"); + + // Copy textures + listTextures(); + if (!_textures.empty()) { + QByteArray texdirField = _mapping.value(TEXDIR_FIELD).toByteArray(); + _texDir = _modelFile.path() + "/" + texdirField; + copyTextures(_texDir, texDir); + } + + // Copy LODs + QVariantHash lodField = _mapping.value(LOD_FIELD).toHash(); + if (!lodField.empty()) { + for (auto it = lodField.constBegin(); it != lodField.constEnd(); ++it) { + QString oldPath = _modelFile.path() + "/" + it.key(); + QFile lod(oldPath); + QString newPath = fbxDir.path() + "/" + QFileInfo(lod).fileName(); + if (lod.exists()) { + lod.copy(newPath); + } + } + } + + // Copy FBX + QFile fbx(_fbxInfo.filePath()); + QByteArray filenameField = _mapping.value(FILENAME_FIELD).toByteArray(); + QString newPath = fbxDir.path() + "/" + QFileInfo(filenameField).fileName(); + fbx.copy(newPath); + + // Correct FST + _mapping[FILENAME_FIELD] = tempDir.relativeFilePath(newPath); + _mapping[TEXDIR_FIELD] = tempDir.relativeFilePath(texDir.path()); + + // Copy FST + QFile fst(tempDir.path() + "/" + nameField + ".fst"); + if (fst.open(QIODevice::WriteOnly)) { + fst.write(writeMapping(_mapping)); + fst.close(); + } else { + qDebug() << "Couldn't write FST file" << fst.fileName(); + return false; + } + + + QString saveDirPath = QFileDialog::getExistingDirectory(nullptr, "Save Model", + "", QFileDialog::ShowDirsOnly); + if (saveDirPath.isEmpty()) { + qDebug() << "Invalid directory" << saveDirPath; + return false; + } + + QDir saveDir(saveDirPath); + copyDirectoryContent(tempDir, saveDir); + return true; +} + +void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const FBXGeometry& geometry) { + if (!mapping.contains(NAME_FIELD)) { + mapping.insert(NAME_FIELD, QFileInfo(filename).baseName()); + } + + if (!mapping.contains(FILENAME_FIELD)) { + QDir root(_modelFile.path()); + mapping.insert(FILENAME_FIELD, root.relativeFilePath(filename)); + } + if (!mapping.contains(TEXDIR_FIELD)) { + mapping.insert(TEXDIR_FIELD, "."); + } + + // mixamo/autodesk defaults + if (!mapping.contains(SCALE_FIELD)) { + mapping.insert(SCALE_FIELD, 1.0); + } + QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); + if (!joints.contains("jointEyeLeft")) { + joints.insert("jointEyeLeft", geometry.jointIndices.contains("jointEyeLeft") ? "jointEyeLeft" : + (geometry.jointIndices.contains("EyeLeft") ? "EyeLeft" : "LeftEye")); + } + if (!joints.contains("jointEyeRight")) { + joints.insert("jointEyeRight", geometry.jointIndices.contains("jointEyeRight") ? "jointEyeRight" : + geometry.jointIndices.contains("EyeRight") ? "EyeRight" : "RightEye"); + } + if (!joints.contains("jointNeck")) { + joints.insert("jointNeck", geometry.jointIndices.contains("jointNeck") ? "jointNeck" : "Neck"); + } + if (!joints.contains("jointRoot")) { + joints.insert("jointRoot", "Hips"); + } + if (!joints.contains("jointLean")) { + joints.insert("jointLean", "Spine"); + } + if (!joints.contains("jointHead")) { + const char* topName = (geometry.applicationName == "mixamo.com") ? "HeadTop_End" : "HeadEnd"; + joints.insert("jointHead", geometry.jointIndices.contains(topName) ? topName : "Head"); + } + if (!joints.contains("jointLeftHand")) { + joints.insert("jointLeftHand", "LeftHand"); + } + if (!joints.contains("jointRightHand")) { + joints.insert("jointRightHand", "RightHand"); + } + mapping.insert(JOINT_FIELD, joints); + if (!mapping.contains(FREE_JOINT_FIELD)) { + mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); + } + + // mixamo blendshapes - 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 = geometry.applicationName == "mixamo.com" || + (geometry.blendshapeChannelNames.contains("BrowsDown_Right") && + geometry.blendshapeChannelNames.contains("MouthOpen") && + geometry.blendshapeChannelNames.contains("Blink_Left") && + geometry.blendshapeChannelNames.contains("Blink_Right") && + geometry.blendshapeChannelNames.contains("Squint_Right")); + + if (!mapping.contains(BLENDSHAPE_FIELD) && 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); + } +} + +void ModelPackager::listTextures() { + _textures.clear(); + foreach (FBXMesh mesh, _geometry.meshes) { + foreach (FBXMeshPart part, mesh.parts) { + if (!part.diffuseTexture.filename.isEmpty() && part.diffuseTexture.content.isEmpty() && + !_textures.contains(part.diffuseTexture.filename)) { + _textures << part.diffuseTexture.filename; + } + if (!part.normalTexture.filename.isEmpty() && part.normalTexture.content.isEmpty() && + !_textures.contains(part.normalTexture.filename)) { + + _textures << part.normalTexture.filename; + } + if (!part.specularTexture.filename.isEmpty() && part.specularTexture.content.isEmpty() && + !_textures.contains(part.specularTexture.filename)) { + _textures << part.specularTexture.filename; + } + if (!part.emissiveTexture.filename.isEmpty() && part.emissiveTexture.content.isEmpty() && + !_textures.contains(part.emissiveTexture.filename)) { + _textures << part.emissiveTexture.filename; + } + } + } +} + +bool ModelPackager::copyTextures(const QString& oldDir, const QDir& newDir) { + QString errors; + for (auto texture : _textures) { + QString oldPath = oldDir + "/" + texture; + QString newPath = newDir.path() + "/" + texture; + + // Make sure path exists + if (texture.contains("/")) { + QString dirPath = newDir.relativeFilePath(QFileInfo(newPath).path()); + newDir.mkpath(dirPath); + } + + QFile texFile(oldPath); + if (texFile.exists() && texFile.open(QIODevice::ReadOnly)) { + // Check if texture needs to be recoded + QFileInfo fileInfo(oldPath); + QString extension = fileInfo.suffix().toLower(); + bool isJpeg = (extension == "jpg"); + bool mustRecode = !(isJpeg || extension == "png"); + QImage image = QImage::fromData(texFile.readAll()); + + // Recode texture if too big + if (image.width() > MAX_TEXTURE_SIZE || image.height() > MAX_TEXTURE_SIZE) { + image = image.scaled(MAX_TEXTURE_SIZE, MAX_TEXTURE_SIZE, Qt::KeepAspectRatio); + mustRecode = true; + } + + // Copy texture + if (mustRecode) { + QFile newTexFile(newPath); + newTexFile.open(QIODevice::WriteOnly); + image.save(&newTexFile, isJpeg ? "JPG" : "PNG"); + } else { + texFile.copy(newPath); + } + } else { + errors += QString("\n%1").arg(oldPath); + } + } + + if (!errors.isEmpty()) { + QMessageBox::warning(nullptr, "ModelPackager::copyTextures()", + "Missing textures:" + errors); + qDebug() << "ModelPackager::copyTextures():" << errors; + return false; + } + + return true; +} + + diff --git a/interface/src/ModelPackager.h b/interface/src/ModelPackager.h new file mode 100644 index 0000000000..c62388f196 --- /dev/null +++ b/interface/src/ModelPackager.h @@ -0,0 +1,51 @@ +// +// ModelPackager.h +// +// +// Created by Clement on 3/9/15. +// Copyright 2015 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 +// + +#ifndef hifi_ModelPackager_h +#define hifi_ModelPackager_h + +#include +#include + +#include + +#include "ui/ModelsBrowser.h" + +class ModelPackager : public QObject { +public: + static bool package(); + +private: + bool selectModel(); + + bool loadModel(); + bool editProperties(); + bool zipModel(); + + void populateBasicMapping(QVariantHash& mapping, QString filename, const FBXGeometry& geometry); + + void listTextures(); + bool copyTextures(const QString& oldDir, const QDir& newDir); + + QFileInfo _modelFile; + QFileInfo _fbxInfo; + ModelType _modelType; + QString _texDir; + + QVariantHash _mapping; + FBXGeometry _geometry; + QStringList _textures; +}; + + + + +#endif // hifi_ModelPackager_h \ No newline at end of file diff --git a/interface/src/ModelPropertiesDialog.cpp b/interface/src/ModelPropertiesDialog.cpp new file mode 100644 index 0000000000..de98407a2a --- /dev/null +++ b/interface/src/ModelPropertiesDialog.cpp @@ -0,0 +1,244 @@ +// +// ModelPropertiesDialog.cpp +// +// +// Created by Clement on 3/10/15. +// Copyright 2015 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ModelPropertiesDialog.h" + + +ModelPropertiesDialog::ModelPropertiesDialog(ModelType modelType, const QVariantHash& originalMapping, + const QString& basePath, const FBXGeometry& geometry) : +_modelType(modelType), +_originalMapping(originalMapping), +_basePath(basePath), +_geometry(geometry) +{ + setWindowTitle("Set Model Properties"); + + QFormLayout* form = new QFormLayout(); + setLayout(form); + + form->addRow("Name:", _name = new QLineEdit()); + + form->addRow("Texture Directory:", _textureDirectory = new QPushButton()); + connect(_textureDirectory, SIGNAL(clicked(bool)), SLOT(chooseTextureDirectory())); + + form->addRow("Scale:", _scale = new QDoubleSpinBox()); + _scale->setMaximum(FLT_MAX); + _scale->setSingleStep(0.01); + + if (_modelType != ENTITY_MODEL) { + if (_modelType == ATTACHMENT_MODEL) { + QHBoxLayout* translation = new QHBoxLayout(); + form->addRow("Translation:", translation); + translation->addWidget(_translationX = createTranslationBox()); + translation->addWidget(_translationY = createTranslationBox()); + translation->addWidget(_translationZ = createTranslationBox()); + form->addRow("Pivot About Center:", _pivotAboutCenter = new QCheckBox()); + form->addRow("Pivot Joint:", _pivotJoint = createJointBox()); + connect(_pivotAboutCenter, SIGNAL(toggled(bool)), SLOT(updatePivotJoint())); + _pivotAboutCenter->setChecked(true); + + } else { + form->addRow("Left Eye Joint:", _leftEyeJoint = createJointBox()); + form->addRow("Right Eye Joint:", _rightEyeJoint = createJointBox()); + form->addRow("Neck Joint:", _neckJoint = createJointBox()); + } + if (_modelType == SKELETON_MODEL) { + form->addRow("Root Joint:", _rootJoint = createJointBox()); + form->addRow("Lean Joint:", _leanJoint = createJointBox()); + form->addRow("Head Joint:", _headJoint = createJointBox()); + form->addRow("Left Hand Joint:", _leftHandJoint = createJointBox()); + form->addRow("Right Hand Joint:", _rightHandJoint = createJointBox()); + + form->addRow("Free Joints:", _freeJoints = new QVBoxLayout()); + QPushButton* newFreeJoint = new QPushButton("New Free Joint"); + _freeJoints->addWidget(newFreeJoint); + connect(newFreeJoint, SIGNAL(clicked(bool)), SLOT(createNewFreeJoint())); + } + } + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | + QDialogButtonBox::Cancel | QDialogButtonBox::Reset); + connect(buttons, SIGNAL(accepted()), SLOT(accept())); + connect(buttons, SIGNAL(rejected()), SLOT(reject())); + connect(buttons->button(QDialogButtonBox::Reset), SIGNAL(clicked(bool)), SLOT(reset())); + + form->addRow(buttons); + + // reset to initialize the fields + reset(); +} + +QVariantHash ModelPropertiesDialog::getMapping() const { + QVariantHash mapping = _originalMapping; + mapping.insert(NAME_FIELD, _name->text()); + mapping.insert(TEXDIR_FIELD, _textureDirectory->text()); + mapping.insert(SCALE_FIELD, QString::number(_scale->value())); + + // update the joint indices + QVariantHash jointIndices; + for (int i = 0; i < _geometry.joints.size(); i++) { + jointIndices.insert(_geometry.joints.at(i).name, QString::number(i)); + } + mapping.insert(JOINT_INDEX_FIELD, jointIndices); + + if (_modelType != ENTITY_MODEL) { + QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); + if (_modelType == ATTACHMENT_MODEL) { + glm::vec3 pivot; + if (_pivotAboutCenter->isChecked()) { + pivot = (_geometry.meshExtents.minimum + _geometry.meshExtents.maximum) * 0.5f; + + } else if (_pivotJoint->currentIndex() != 0) { + pivot = extractTranslation(_geometry.joints.at(_pivotJoint->currentIndex() - 1).transform); + } + mapping.insert(TRANSLATION_X_FIELD, -pivot.x * _scale->value() + _translationX->value()); + mapping.insert(TRANSLATION_Y_FIELD, -pivot.y * _scale->value() + _translationY->value()); + mapping.insert(TRANSLATION_Z_FIELD, -pivot.z * _scale->value() + _translationZ->value()); + + } else { + insertJointMapping(joints, "jointEyeLeft", _leftEyeJoint->currentText()); + insertJointMapping(joints, "jointEyeRight", _rightEyeJoint->currentText()); + insertJointMapping(joints, "jointNeck", _neckJoint->currentText()); + } + if (_modelType == SKELETON_MODEL) { + insertJointMapping(joints, "jointRoot", _rootJoint->currentText()); + insertJointMapping(joints, "jointLean", _leanJoint->currentText()); + insertJointMapping(joints, "jointHead", _headJoint->currentText()); + insertJointMapping(joints, "jointLeftHand", _leftHandJoint->currentText()); + insertJointMapping(joints, "jointRightHand", _rightHandJoint->currentText()); + + mapping.remove(FREE_JOINT_FIELD); + for (int i = 0; i < _freeJoints->count() - 1; i++) { + QComboBox* box = static_cast(_freeJoints->itemAt(i)->widget()->layout()->itemAt(0)->widget()); + mapping.insertMulti(FREE_JOINT_FIELD, box->currentText()); + } + } + mapping.insert(JOINT_FIELD, joints); + } + + return mapping; +} + +static void setJointText(QComboBox* box, const QString& text) { + box->setCurrentIndex(qMax(box->findText(text), 0)); +} + +void ModelPropertiesDialog::reset() { + _name->setText(_originalMapping.value(NAME_FIELD).toString()); + _textureDirectory->setText(_originalMapping.value(TEXDIR_FIELD).toString()); + _scale->setValue(_originalMapping.value(SCALE_FIELD).toDouble()); + + QVariantHash jointHash = _originalMapping.value(JOINT_FIELD).toHash(); + + if (_modelType != ENTITY_MODEL) { + if (_modelType == ATTACHMENT_MODEL) { + _translationX->setValue(_originalMapping.value(TRANSLATION_X_FIELD).toDouble()); + _translationY->setValue(_originalMapping.value(TRANSLATION_Y_FIELD).toDouble()); + _translationZ->setValue(_originalMapping.value(TRANSLATION_Z_FIELD).toDouble()); + _pivotAboutCenter->setChecked(true); + _pivotJoint->setCurrentIndex(0); + + } else { + setJointText(_leftEyeJoint, jointHash.value("jointEyeLeft").toString()); + setJointText(_rightEyeJoint, jointHash.value("jointEyeRight").toString()); + setJointText(_neckJoint, jointHash.value("jointNeck").toString()); + } + if (_modelType == SKELETON_MODEL) { + setJointText(_rootJoint, jointHash.value("jointRoot").toString()); + setJointText(_leanJoint, jointHash.value("jointLean").toString()); + setJointText(_headJoint, jointHash.value("jointHead").toString()); + setJointText(_leftHandJoint, jointHash.value("jointLeftHand").toString()); + setJointText(_rightHandJoint, jointHash.value("jointRightHand").toString()); + + while (_freeJoints->count() > 1) { + delete _freeJoints->itemAt(0)->widget(); + } + foreach (const QVariant& joint, _originalMapping.values(FREE_JOINT_FIELD)) { + QString jointName = joint.toString(); + if (_geometry.jointIndices.contains(jointName)) { + createNewFreeJoint(jointName); + } + } + } + } +} + +void ModelPropertiesDialog::chooseTextureDirectory() { + QString directory = QFileDialog::getExistingDirectory(this, "Choose Texture Directory", + _basePath + "/" + _textureDirectory->text()); + if (directory.isEmpty()) { + return; + } + if (!directory.startsWith(_basePath)) { + QMessageBox::warning(NULL, "Invalid texture directory", "Texture directory must be child of base path."); + return; + } + _textureDirectory->setText(directory.length() == _basePath.length() ? "." : directory.mid(_basePath.length() + 1)); +} + +void ModelPropertiesDialog::updatePivotJoint() { + _pivotJoint->setEnabled(!_pivotAboutCenter->isChecked()); +} + +void ModelPropertiesDialog::createNewFreeJoint(const QString& joint) { + QWidget* freeJoint = new QWidget(); + QHBoxLayout* freeJointLayout = new QHBoxLayout(); + freeJointLayout->setContentsMargins(QMargins()); + freeJoint->setLayout(freeJointLayout); + QComboBox* jointBox = createJointBox(false); + jointBox->setCurrentText(joint); + freeJointLayout->addWidget(jointBox, 1); + QPushButton* deleteJoint = new QPushButton("Delete"); + freeJointLayout->addWidget(deleteJoint); + freeJoint->connect(deleteJoint, SIGNAL(clicked(bool)), SLOT(deleteLater())); + _freeJoints->insertWidget(_freeJoints->count() - 1, freeJoint); +} + +QComboBox* ModelPropertiesDialog::createJointBox(bool withNone) const { + QComboBox* box = new QComboBox(); + if (withNone) { + box->addItem("(none)"); + } + foreach (const FBXJoint& joint, _geometry.joints) { + if (joint.isSkeletonJoint || !_geometry.hasSkeletonJoints) { + box->addItem(joint.name); + } + } + return box; +} + +QDoubleSpinBox* ModelPropertiesDialog::createTranslationBox() const { + QDoubleSpinBox* box = new QDoubleSpinBox(); + const double MAX_TRANSLATION = 1000000.0; + box->setMinimum(-MAX_TRANSLATION); + box->setMaximum(MAX_TRANSLATION); + return box; +} + +void ModelPropertiesDialog::insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const { + if (_geometry.jointIndices.contains(name)) { + joints.insert(joint, name); + } else { + joints.remove(joint); + } +} diff --git a/interface/src/ModelPropertiesDialog.h b/interface/src/ModelPropertiesDialog.h new file mode 100644 index 0000000000..65c5be6c21 --- /dev/null +++ b/interface/src/ModelPropertiesDialog.h @@ -0,0 +1,83 @@ +// +// ModelPropertiesDialog.h +// +// +// Created by Clement on 3/10/15. +// Copyright 2015 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 +// + +#ifndef hifi_ModelPropertiesDialog_h +#define hifi_ModelPropertiesDialog_h + +#include + +#include + +#include "ui/ModelsBrowser.h" + +class QDoubleSpinBox; +class QComboBox; +class QCheckBox; +class QVBoxLayout; + +static const QString NAME_FIELD = "name"; +static const QString FILENAME_FIELD = "filename"; +static const QString TEXDIR_FIELD = "texdir"; +static const QString LOD_FIELD = "lod"; +static const QString JOINT_INDEX_FIELD = "jointIndex"; +static const QString SCALE_FIELD = "scale"; +static const QString TRANSLATION_X_FIELD = "tx"; +static const QString TRANSLATION_Y_FIELD = "ty"; +static const QString TRANSLATION_Z_FIELD = "tz"; +static const QString JOINT_FIELD = "joint"; +static const QString FREE_JOINT_FIELD = "freeJoint"; +static const QString BLENDSHAPE_FIELD = "bs"; + +/// A dialog that allows customization of various model properties. +class ModelPropertiesDialog : public QDialog { + Q_OBJECT + +public: + ModelPropertiesDialog(ModelType modelType, const QVariantHash& originalMapping, + const QString& basePath, const FBXGeometry& geometry); + + QVariantHash getMapping() const; + +private slots: + void reset(); + void chooseTextureDirectory(); + void updatePivotJoint(); + void createNewFreeJoint(const QString& joint = QString()); + +private: + QComboBox* createJointBox(bool withNone = true) const; + QDoubleSpinBox* createTranslationBox() const; + void insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const; + + ModelType _modelType; + QVariantHash _originalMapping; + QString _basePath; + FBXGeometry _geometry; + QLineEdit* _name = nullptr; + QPushButton* _textureDirectory = nullptr; + QDoubleSpinBox* _scale = nullptr; + QDoubleSpinBox* _translationX = nullptr; + QDoubleSpinBox* _translationY = nullptr; + QDoubleSpinBox* _translationZ = nullptr; + QCheckBox* _pivotAboutCenter = nullptr; + QComboBox* _pivotJoint = nullptr; + QComboBox* _leftEyeJoint = nullptr; + QComboBox* _rightEyeJoint = nullptr; + QComboBox* _neckJoint = nullptr; + QComboBox* _rootJoint = nullptr; + QComboBox* _leanJoint = nullptr; + QComboBox* _headJoint = nullptr; + QComboBox* _leftHandJoint = nullptr; + QComboBox* _rightHandJoint = nullptr; + QVBoxLayout* _freeJoints = nullptr; +}; + +#endif // hifi_ModelPropertiesDialog_h \ No newline at end of file diff --git a/interface/src/ModelSelector.cpp b/interface/src/ModelSelector.cpp new file mode 100644 index 0000000000..c55d77dc00 --- /dev/null +++ b/interface/src/ModelSelector.cpp @@ -0,0 +1,89 @@ +// +// ModelSelector.cpp +// +// +// Created by Clement on 3/10/15. +// Copyright 2015 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 +#include +#include +#include +#include +#include + +#include "ModelSelector.h" + +static const QString AVATAR_HEAD_STRING = "Avatar Head"; +static const QString AVATAR_BODY_STRING = "Avatar Body"; +static const QString AVATAR_ATTACHEMENT_STRING = "Avatar Attachment"; +static const QString ENTITY_MODEL_STRING = "Entity Model"; + +ModelSelector::ModelSelector() { + QFormLayout* form = new QFormLayout(this); + + setWindowTitle("Select Model"); + setLayout(form); + + _browseButton = new QPushButton("Browse", this); + connect(_browseButton, &QPushButton::clicked, this, &ModelSelector::browse); + form->addRow("Model File:", _browseButton); + + _modelType = new QComboBox(this); + _modelType->addItem(AVATAR_HEAD_STRING); + _modelType->addItem(AVATAR_BODY_STRING); + _modelType->addItem(AVATAR_ATTACHEMENT_STRING); + _modelType->addItem(ENTITY_MODEL_STRING); + form->addRow("Model Type:", _modelType); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(buttons, &QDialogButtonBox::accepted, this, &ModelSelector::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + form->addRow(buttons); +} + +QFileInfo ModelSelector::getFileInfo() const { + return _modelFile; +} + +ModelType ModelSelector::getModelType() const { + QString text = _modelType->currentText(); + + if (text == AVATAR_HEAD_STRING) { + return HEAD_MODEL; + } else if (text == AVATAR_BODY_STRING) { + return SKELETON_MODEL; + } else if (text == AVATAR_ATTACHEMENT_STRING) { + return ATTACHMENT_MODEL; + } else if (text == ENTITY_MODEL_STRING) { + return ENTITY_MODEL; + } else { + Q_UNREACHABLE(); + } +} + +void ModelSelector::accept() { + if (!_modelFile.isFile()) { + return; + } + QDialog::accept(); +} + +void ModelSelector::browse() { + static Setting::Handle lastModelBrowseLocation("LastModelBrowseLocation", + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); + QString filename = QFileDialog::getOpenFileName(NULL, "Select your model file ...", + lastModelBrowseLocation.get(), + "Model files (*.fst *.fbx)"); + QFileInfo fileInfo(filename); + + if (fileInfo.isFile() && fileInfo.completeSuffix().contains(QRegExp("fst|fbx|FST|FBX"))) { + _modelFile = fileInfo; + _browseButton->setText(fileInfo.fileName()); + lastModelBrowseLocation.set(fileInfo.path()); + } +} \ No newline at end of file diff --git a/interface/src/ModelSelector.h b/interface/src/ModelSelector.h new file mode 100644 index 0000000000..aaa35e01c3 --- /dev/null +++ b/interface/src/ModelSelector.h @@ -0,0 +1,46 @@ +// +// ModelSelector.h +// +// +// Created by Clement on 3/10/15. +// Copyright 2015 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 +// + +#ifndef hifi_ModelSelector_h +#define hifi_ModelSelector_h + +#include +#include + +#include + +#include "ui/ModelsBrowser.h" + +class QComboBox; +class QPushButton; + +class ModelSelector : public QDialog { + Q_OBJECT + +public: + ModelSelector(); + + QFileInfo getFileInfo() const; + ModelType getModelType() const; + + public slots: + virtual void accept(); + + private slots: + void browse(); + +private: + QFileInfo _modelFile; + QPushButton* _browseButton; + QComboBox* _modelType; +}; + +#endif // hifi_ModelSelector_h \ No newline at end of file diff --git a/interface/src/ModelUploader.cpp b/interface/src/ModelUploader.cpp deleted file mode 100644 index a6b51aa938..0000000000 --- a/interface/src/ModelUploader.cpp +++ /dev/null @@ -1,881 +0,0 @@ -// -// ModelUploader.cpp -// interface/src -// -// Created by Clément Brisset on 3/4/14. -// Copyright 2014 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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "ModelUploader.h" - - -static const QString NAME_FIELD = "name"; -static const QString FILENAME_FIELD = "filename"; -static const QString TEXDIR_FIELD = "texdir"; -static const QString LOD_FIELD = "lod"; -static const QString JOINT_INDEX_FIELD = "jointIndex"; -static const QString SCALE_FIELD = "scale"; -static const QString TRANSLATION_X_FIELD = "tx"; -static const QString TRANSLATION_Y_FIELD = "ty"; -static const QString TRANSLATION_Z_FIELD = "tz"; -static const QString JOINT_FIELD = "joint"; -static const QString FREE_JOINT_FIELD = "freeJoint"; -static const QString BLENDSHAPE_FIELD = "bs"; - -static const QString S3_URL = "http://public.highfidelity.io"; -static const QString MODEL_URL = "/api/v1/models"; - -static const unsigned long long MAX_SIZE = 50 * 1024 * BYTES_PER_MEGABYTES; // 50 GB (Virtually remove limit) -static const int MAX_TEXTURE_SIZE = 1024; -static const int TIMEOUT = 1000; -static const int MAX_CHECK = 30; - -static const int QCOMPRESS_HEADER_POSITION = 0; -static const int QCOMPRESS_HEADER_SIZE = 4; - -Setting::Handle ModelUploader::_lastModelUploadLocation("LastModelUploadLocation", - QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); - -void ModelUploader::uploadModel(ModelType modelType) { - ModelUploader* uploader = new ModelUploader(modelType); - QThread* thread = new QThread(); - thread->setObjectName("Model Uploader"); - thread->connect(uploader, SIGNAL(destroyed()), SLOT(quit())); - thread->connect(thread, SIGNAL(finished()), SLOT(deleteLater())); - uploader->connect(thread, SIGNAL(started()), SLOT(send())); - - thread->start(); -} - -void ModelUploader::uploadHead() { - uploadModel(HEAD_MODEL); -} - -void ModelUploader::uploadSkeleton() { - uploadModel(SKELETON_MODEL); -} - -void ModelUploader::uploadAttachment() { - uploadModel(ATTACHMENT_MODEL); -} - -void ModelUploader::uploadEntity() { - uploadModel(ENTITY_MODEL); -} - -ModelUploader::ModelUploader(ModelType modelType) : - _lodCount(-1), - _texturesCount(-1), - _totalSize(0), - _modelType(modelType), - _readyToSend(false), - _dataMultiPart(new QHttpMultiPart(QHttpMultiPart::FormDataType)), - _numberOfChecks(MAX_CHECK) -{ - connect(&_timer, SIGNAL(timeout()), SLOT(checkS3())); -} - -ModelUploader::~ModelUploader() { - delete _dataMultiPart; -} - -bool ModelUploader::zip() { - // File Dialog - QString lastLocation = _lastModelUploadLocation.get(); - - if (lastLocation.isEmpty()) { - lastLocation = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); - // Temporary fix to Qt bug: http://stackoverflow.com/questions/16194475 -#ifdef __APPLE__ - lastLocation.append("/model.fst"); -#endif - } - - - QString filename = QFileDialog::getOpenFileName(NULL, "Select your model file ...", - lastLocation, "Model files (*.fst *.fbx)"); - if (filename == "") { - // If the user canceled we return. - return false; - } - _lastModelUploadLocation.set(filename); - - // First we check the FST file (if any) - QFile* fst; - QVariantHash mapping; - QString basePath; - QString fbxFile; - if (filename.toLower().endsWith(".fst")) { - fst = new QFile(filename, this); - if (!fst->open(QFile::ReadOnly | QFile::Text)) { - QMessageBox::warning(NULL, - QString("ModelUploader::zip()"), - QString("Could not open FST file."), - QMessageBox::Ok); - qDebug() << "[Warning] " << QString("Could not open FST file."); - return false; - } - qDebug() << "Reading FST file : " << QFileInfo(*fst).filePath(); - mapping = readMapping(fst->readAll()); - basePath = QFileInfo(*fst).path(); - fbxFile = basePath + "/" + mapping.value(FILENAME_FIELD).toString(); - QFileInfo fbxInfo(fbxFile); - if (!fbxInfo.exists() || !fbxInfo.isFile()) { // Check existence - QMessageBox::warning(NULL, - QString("ModelUploader::zip()"), - QString("FBX file %1 could not be found.").arg(fbxInfo.fileName()), - QMessageBox::Ok); - qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(fbxInfo.fileName()); - return false; - } - } else { - fst = new QTemporaryFile(this); - fst->open(QFile::WriteOnly); - fbxFile = filename; - basePath = QFileInfo(filename).path(); - mapping.insert(FILENAME_FIELD, QFileInfo(filename).fileName()); - } - - // open the fbx file - QFile fbx(fbxFile); - if (!fbx.open(QIODevice::ReadOnly)) { - return false; - } - QByteArray fbxContents = fbx.readAll(); - FBXGeometry geometry = readFBX(fbxContents, QVariantHash()); - - // make sure we have some basic mappings - populateBasicMapping(mapping, filename, geometry); - - // open the dialog to configure the rest - ModelPropertiesDialog properties(_modelType, mapping, basePath, geometry); - if (properties.exec() == QDialog::Rejected) { - return false; - } - mapping = properties.getMapping(); - - // Make sure that a mapping for the root joint has been specified - QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); - if (!joints.contains("jointRoot")) { - qDebug() << QString("[Warning] %1 root joint not configured for skeleton.").arg(filename); - - QString message = "Your did not configure a root joint for your skeleton model.\n\nThe upload will be canceled."; - QMessageBox msgBox; - msgBox.setWindowTitle("Model Upload"); - msgBox.setText(message); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Warning); - msgBox.exec(); - - return false; - } - - QByteArray nameField = mapping.value(NAME_FIELD).toByteArray(); - QString urlBase; - if (!nameField.isEmpty()) { - QHttpPart textPart; - textPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"model_name\""); - textPart.setBody(nameField); - _dataMultiPart->append(textPart); - urlBase = S3_URL + "/models/" + MODEL_TYPE_NAMES[_modelType] + "/" + nameField; - _url = urlBase + ".fst"; - - } else { - QMessageBox::warning(NULL, - QString("ModelUploader::zip()"), - QString("Model name is missing in the .fst file."), - QMessageBox::Ok); - qDebug() << "[Warning] " << QString("Model name is missing in the .fst file."); - return false; - } - - QByteArray texdirField = mapping.value(TEXDIR_FIELD).toByteArray(); - QString texDir; - _textureBase = urlBase + "/textures/"; - if (!texdirField.isEmpty()) { - texDir = basePath + "/" + texdirField; - QFileInfo texInfo(texDir); - if (!texInfo.exists() || !texInfo.isDir()) { - QMessageBox::warning(NULL, - QString("ModelUploader::zip()"), - QString("Texture directory could not be found."), - QMessageBox::Ok); - qDebug() << "[Warning] " << QString("Texture directory could not be found."); - return false; - } - } - - QVariantHash lodField = mapping.value(LOD_FIELD).toHash(); - for (QVariantHash::const_iterator it = lodField.constBegin(); it != lodField.constEnd(); it++) { - QFileInfo lod(basePath + "/" + it.key()); - if (!lod.exists() || !lod.isFile()) { // Check existence - QMessageBox::warning(NULL, - QString("ModelUploader::zip()"), - QString("LOD file %1 could not be found.").arg(lod.fileName()), - QMessageBox::Ok); - qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(lod.fileName()); - } - // Compress and copy - if (!addPart(lod.filePath(), QString("lod%1").arg(++_lodCount))) { - return false; - } - } - - // Write out, compress and copy the fst - if (!addPart(*fst, writeMapping(mapping), QString("fst"))) { - return false; - } - - // Compress and copy the fbx - if (!addPart(fbx, fbxContents, "fbx")) { - return false; - } - - if (!addTextures(texDir, geometry)) { - return false; - } - - QHttpPart textPart; - textPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data;" - " name=\"model_category\""); - textPart.setBody(MODEL_TYPE_NAMES[_modelType]); - _dataMultiPart->append(textPart); - - _readyToSend = true; - return true; -} - -void ModelUploader::populateBasicMapping(QVariantHash& mapping, QString filename, const FBXGeometry& geometry) { - if (!mapping.contains(NAME_FIELD)) { - mapping.insert(NAME_FIELD, QFileInfo(filename).baseName()); - } - if (!mapping.contains(TEXDIR_FIELD)) { - mapping.insert(TEXDIR_FIELD, "."); - } - - // mixamo/autodesk defaults - if (!mapping.contains(SCALE_FIELD)) { - mapping.insert(SCALE_FIELD, 1.0); - } - QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); - if (!joints.contains("jointEyeLeft")) { - joints.insert("jointEyeLeft", geometry.jointIndices.contains("jointEyeLeft") ? "jointEyeLeft" : - (geometry.jointIndices.contains("EyeLeft") ? "EyeLeft" : "LeftEye")); - } - if (!joints.contains("jointEyeRight")) { - joints.insert("jointEyeRight", geometry.jointIndices.contains("jointEyeRight") ? "jointEyeRight" : - geometry.jointIndices.contains("EyeRight") ? "EyeRight" : "RightEye"); - } - if (!joints.contains("jointNeck")) { - joints.insert("jointNeck", geometry.jointIndices.contains("jointNeck") ? "jointNeck" : "Neck"); - } - if (!joints.contains("jointRoot")) { - joints.insert("jointRoot", "Hips"); - } - if (!joints.contains("jointLean")) { - joints.insert("jointLean", "Spine"); - } - if (!joints.contains("jointHead")) { - const char* topName = (geometry.applicationName == "mixamo.com") ? "HeadTop_End" : "HeadEnd"; - joints.insert("jointHead", geometry.jointIndices.contains(topName) ? topName : "Head"); - } - if (!joints.contains("jointLeftHand")) { - joints.insert("jointLeftHand", "LeftHand"); - } - if (!joints.contains("jointRightHand")) { - joints.insert("jointRightHand", "RightHand"); - } - mapping.insert(JOINT_FIELD, joints); - if (!mapping.contains(FREE_JOINT_FIELD)) { - mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); - mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); - } - - // mixamo blendshapes - 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 = geometry.applicationName == "mixamo.com" || - (geometry.blendshapeChannelNames.contains("BrowsDown_Right") && - geometry.blendshapeChannelNames.contains("MouthOpen") && - geometry.blendshapeChannelNames.contains("Blink_Left") && - geometry.blendshapeChannelNames.contains("Blink_Right") && - geometry.blendshapeChannelNames.contains("Squint_Right")); - - if (!mapping.contains(BLENDSHAPE_FIELD) && 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); - } -} - -void ModelUploader::send() { - if (!zip()) { - deleteLater(); - return; - } - - JSONCallbackParameters callbackParams; - callbackParams.jsonCallbackReceiver = this; - callbackParams.jsonCallbackMethod = "checkJSON"; - callbackParams.errorCallbackReceiver = this; - callbackParams.errorCallbackMethod = "uploadFailed"; - - AccountManager::getInstance().authenticatedRequest(MODEL_URL + "/" + QFileInfo(_url).baseName(), - QNetworkAccessManager::GetOperation, - callbackParams); - - qDebug() << "Sending model..."; - _progressDialog = new QDialog(); - _progressBar = new QProgressBar(_progressDialog); - _progressBar->setRange(0, 100); - _progressBar->setValue(0); - - _progressDialog->setWindowTitle("Uploading model..."); - _progressDialog->setLayout(new QGridLayout(_progressDialog)); - _progressDialog->layout()->addWidget(_progressBar); - - _progressDialog->exec(); - - delete _progressDialog; - _progressDialog = NULL; - _progressBar = NULL; -} - -void ModelUploader::checkJSON(QNetworkReply& requestReply) { - QJsonObject jsonResponse = QJsonDocument::fromJson(requestReply.readAll()).object(); - - if (jsonResponse.contains("status") && jsonResponse.value("status").toString() == "success") { - qDebug() << "status : success"; - JSONCallbackParameters callbackParams; - callbackParams.jsonCallbackReceiver = this; - callbackParams.jsonCallbackMethod = "uploadSuccess"; - callbackParams.errorCallbackReceiver = this; - callbackParams.errorCallbackMethod = "uploadFailed"; - callbackParams.updateReciever = this; - callbackParams.updateSlot = SLOT(uploadUpdate(qint64, qint64)); - - if (jsonResponse.contains("exists") && jsonResponse.value("exists").toBool()) { - qDebug() << "exists : true"; - if (jsonResponse.contains("can_update") && jsonResponse.value("can_update").toBool()) { - qDebug() << "can_update : true"; - - AccountManager::getInstance().authenticatedRequest(MODEL_URL + "/" + QFileInfo(_url).baseName(), - QNetworkAccessManager::PutOperation, - callbackParams, - QByteArray(), - _dataMultiPart); - _dataMultiPart = NULL; - } else { - qDebug() << "can_update : false"; - if (_progressDialog) { - _progressDialog->reject(); - } - QMessageBox::warning(NULL, - QString("ModelUploader::checkJSON()"), - QString("This model already exist and is own by someone else."), - QMessageBox::Ok); - deleteLater(); - } - } else { - qDebug() << "exists : false"; - AccountManager::getInstance().authenticatedRequest(MODEL_URL, - QNetworkAccessManager::PostOperation, - callbackParams, - QByteArray(), - _dataMultiPart); - _dataMultiPart = NULL; - } - } else { - qDebug() << "status : failed"; - if (_progressDialog) { - _progressDialog->reject(); - } - QMessageBox::warning(NULL, - QString("ModelUploader::checkJSON()"), - QString("Something went wrong with the data-server."), - QMessageBox::Ok); - deleteLater(); - } -} - -void ModelUploader::uploadUpdate(qint64 bytesSent, qint64 bytesTotal) { - if (_progressDialog) { - _progressBar->setRange(0, bytesTotal); - _progressBar->setValue(bytesSent); - } -} - -void ModelUploader::uploadSuccess(QNetworkReply& requestReply) { - if (_progressDialog) { - _progressDialog->accept(); - } - QMessageBox::information(NULL, - QString("ModelUploader::uploadSuccess()"), - QString("We are reading your model information."), - QMessageBox::Ok); - qDebug() << "Model sent with success"; - checkS3(); -} - -void ModelUploader::uploadFailed(QNetworkReply& errorReply) { - if (_progressDialog) { - _progressDialog->reject(); - } - qDebug() << "Model upload failed (" << errorReply.error() << "): " << errorReply.errorString(); - QMessageBox::warning(NULL, - QString("ModelUploader::uploadFailed()"), - QString("There was a problem with your upload, please try again later."), - QMessageBox::Ok); - deleteLater(); -} - -void ModelUploader::checkS3() { - qDebug() << "Checking S3 for " << _url; - QNetworkRequest request(_url); - request.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = NetworkAccessManager::getInstance().head(request); - connect(reply, SIGNAL(finished()), SLOT(processCheck())); -} - -void ModelUploader::processCheck() { - QNetworkReply* reply = static_cast(sender()); - _timer.stop(); - - switch (reply->error()) { - case QNetworkReply::NoError: { - QMessageBox::information(NULL, - QString("ModelUploader::processCheck()"), - QString("Your model is now available in the browser."), - QMessageBox::Ok); - DependencyManager::get()->refresh(_url); - auto textureCache = DependencyManager::get(); - foreach (const QByteArray& filename, _textureFilenames) { - textureCache->refresh(_textureBase + filename); - } - deleteLater(); - break; - } - case QNetworkReply::ContentNotFoundError: - if (--_numberOfChecks) { - _timer.start(TIMEOUT); - break; - } - default: - QMessageBox::warning(NULL, - QString("ModelUploader::processCheck()"), - QString("We could not verify that your model was sent sucessfully\n" - "but it may have. If you do not see it in the model browser, try to upload again."), - QMessageBox::Ok); - deleteLater(); - break; - } - - delete reply; -} - -bool ModelUploader::addTextures(const QString& texdir, const FBXGeometry& geometry) { - foreach (FBXMesh mesh, geometry.meshes) { - foreach (FBXMeshPart part, mesh.parts) { - if (!part.diffuseTexture.filename.isEmpty() && part.diffuseTexture.content.isEmpty() && - !_textureFilenames.contains(part.diffuseTexture.filename)) { - if (!addPart(texdir + "/" + part.diffuseTexture.filename, - QString("texture%1").arg(++_texturesCount), true)) { - return false; - } - _textureFilenames.insert(part.diffuseTexture.filename); - } - if (!part.normalTexture.filename.isEmpty() && part.normalTexture.content.isEmpty() && - !_textureFilenames.contains(part.normalTexture.filename)) { - if (!addPart(texdir + "/" + part.normalTexture.filename, - QString("texture%1").arg(++_texturesCount), true)) { - return false; - } - _textureFilenames.insert(part.normalTexture.filename); - } - if (!part.specularTexture.filename.isEmpty() && part.specularTexture.content.isEmpty() && - !_textureFilenames.contains(part.specularTexture.filename)) { - if (!addPart(texdir + "/" + part.specularTexture.filename, - QString("texture%1").arg(++_texturesCount), true)) { - return false; - } - _textureFilenames.insert(part.specularTexture.filename); - } - if (!part.emissiveTexture.filename.isEmpty() && part.emissiveTexture.content.isEmpty() && - !_textureFilenames.contains(part.emissiveTexture.filename)) { - if (!addPart(texdir + "/" + part.emissiveTexture.filename, - QString("texture%1").arg(++_texturesCount), true)) { - return false; - } - _textureFilenames.insert(part.emissiveTexture.filename); - } - } - } - - return true; -} - -bool ModelUploader::addPart(const QString &path, const QString& name, bool isTexture) { - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - QMessageBox::warning(NULL, - QString("ModelUploader::addPart()"), - QString("Could not open %1").arg(path), - QMessageBox::Ok); - qDebug() << "[Warning] " << QString("Could not open %1").arg(path); - return false; - } - return addPart(file, file.readAll(), name, isTexture); -} - -bool ModelUploader::addPart(const QFile& file, const QByteArray& contents, const QString& name, bool isTexture) { - QFileInfo fileInfo(file); - QByteArray recodedContents = contents; - if (isTexture) { - QString extension = fileInfo.suffix().toLower(); - bool isJpeg = (extension == "jpg"); - bool mustRecode = !(isJpeg || extension == "png"); - QImage image = QImage::fromData(contents); - if (image.width() > MAX_TEXTURE_SIZE || image.height() > MAX_TEXTURE_SIZE) { - image = image.scaled(MAX_TEXTURE_SIZE, MAX_TEXTURE_SIZE, Qt::KeepAspectRatio); - mustRecode = true; - } - if (mustRecode) { - QBuffer buffer; - buffer.open(QIODevice::WriteOnly); - image.save(&buffer, isJpeg ? "JPG" : "PNG"); - recodedContents = buffer.data(); - } - } - QByteArray buffer = qCompress(recodedContents); - - // Qt's qCompress() default compression level (-1) is the standard zLib compression. - // Here remove Qt's custom header that prevent the data server from uncompressing the files with zLib. - buffer.remove(QCOMPRESS_HEADER_POSITION, QCOMPRESS_HEADER_SIZE); - - QHttpPart part; - part.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data;" - " name=\"" + name.toUtf8() + "\";" - " filename=\"" + QFileInfo(file).fileName().toUtf8() + "\"")); - part.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); - part.setBody(buffer); - _dataMultiPart->append(part); - - - qDebug() << "File " << QFileInfo(file).fileName() << " added to model."; - _totalSize += recodedContents.size(); - if (_totalSize > MAX_SIZE) { - QMessageBox::warning(NULL, - QString("ModelUploader::zip()"), - QString("Model too big, over %1 MB.").arg(MAX_SIZE / BYTES_PER_MEGABYTES), - QMessageBox::Ok); - qDebug() << "[Warning] " << QString("Model too big, over %1 MB.").arg(MAX_SIZE / BYTES_PER_MEGABYTES); - return false; - } - qDebug() << "Current model size: " << _totalSize; - - return true; -} - -static QDoubleSpinBox* createTranslationBox() { - QDoubleSpinBox* box = new QDoubleSpinBox(); - const double MAX_TRANSLATION = 1000000.0; - box->setMinimum(-MAX_TRANSLATION); - box->setMaximum(MAX_TRANSLATION); - return box; -} - -ModelPropertiesDialog::ModelPropertiesDialog(ModelType modelType, const QVariantHash& originalMapping, - const QString& basePath, const FBXGeometry& geometry) : - _modelType(modelType), - _originalMapping(originalMapping), - _basePath(basePath), - _geometry(geometry) -{ - setWindowTitle("Set Model Properties"); - - QFormLayout* form = new QFormLayout(); - setLayout(form); - - form->addRow("Name:", _name = new QLineEdit()); - - form->addRow("Texture Directory:", _textureDirectory = new QPushButton()); - connect(_textureDirectory, SIGNAL(clicked(bool)), SLOT(chooseTextureDirectory())); - - form->addRow("Scale:", _scale = new QDoubleSpinBox()); - _scale->setMaximum(FLT_MAX); - _scale->setSingleStep(0.01); - - if (_modelType != ENTITY_MODEL) { - if (_modelType == ATTACHMENT_MODEL) { - QHBoxLayout* translation = new QHBoxLayout(); - form->addRow("Translation:", translation); - translation->addWidget(_translationX = createTranslationBox()); - translation->addWidget(_translationY = createTranslationBox()); - translation->addWidget(_translationZ = createTranslationBox()); - form->addRow("Pivot About Center:", _pivotAboutCenter = new QCheckBox()); - form->addRow("Pivot Joint:", _pivotJoint = createJointBox()); - connect(_pivotAboutCenter, SIGNAL(toggled(bool)), SLOT(updatePivotJoint())); - _pivotAboutCenter->setChecked(true); - - } else { - form->addRow("Left Eye Joint:", _leftEyeJoint = createJointBox()); - form->addRow("Right Eye Joint:", _rightEyeJoint = createJointBox()); - form->addRow("Neck Joint:", _neckJoint = createJointBox()); - } - if (_modelType == SKELETON_MODEL) { - form->addRow("Root Joint:", _rootJoint = createJointBox()); - form->addRow("Lean Joint:", _leanJoint = createJointBox()); - form->addRow("Head Joint:", _headJoint = createJointBox()); - form->addRow("Left Hand Joint:", _leftHandJoint = createJointBox()); - form->addRow("Right Hand Joint:", _rightHandJoint = createJointBox()); - - form->addRow("Free Joints:", _freeJoints = new QVBoxLayout()); - QPushButton* newFreeJoint = new QPushButton("New Free Joint"); - _freeJoints->addWidget(newFreeJoint); - connect(newFreeJoint, SIGNAL(clicked(bool)), SLOT(createNewFreeJoint())); - } - } - - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | - QDialogButtonBox::Cancel | QDialogButtonBox::Reset); - connect(buttons, SIGNAL(accepted()), SLOT(accept())); - connect(buttons, SIGNAL(rejected()), SLOT(reject())); - connect(buttons->button(QDialogButtonBox::Reset), SIGNAL(clicked(bool)), SLOT(reset())); - - form->addRow(buttons); - - // reset to initialize the fields - reset(); -} - -QVariantHash ModelPropertiesDialog::getMapping() const { - QVariantHash mapping = _originalMapping; - mapping.insert(NAME_FIELD, _name->text()); - mapping.insert(TEXDIR_FIELD, _textureDirectory->text()); - mapping.insert(SCALE_FIELD, QString::number(_scale->value())); - - // update the joint indices - QVariantHash jointIndices; - for (int i = 0; i < _geometry.joints.size(); i++) { - jointIndices.insert(_geometry.joints.at(i).name, QString::number(i)); - } - mapping.insert(JOINT_INDEX_FIELD, jointIndices); - - if (_modelType != ENTITY_MODEL) { - QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); - if (_modelType == ATTACHMENT_MODEL) { - glm::vec3 pivot; - if (_pivotAboutCenter->isChecked()) { - pivot = (_geometry.meshExtents.minimum + _geometry.meshExtents.maximum) * 0.5f; - - } else if (_pivotJoint->currentIndex() != 0) { - pivot = extractTranslation(_geometry.joints.at(_pivotJoint->currentIndex() - 1).transform); - } - mapping.insert(TRANSLATION_X_FIELD, -pivot.x * _scale->value() + _translationX->value()); - mapping.insert(TRANSLATION_Y_FIELD, -pivot.y * _scale->value() + _translationY->value()); - mapping.insert(TRANSLATION_Z_FIELD, -pivot.z * _scale->value() + _translationZ->value()); - - } else { - insertJointMapping(joints, "jointEyeLeft", _leftEyeJoint->currentText()); - insertJointMapping(joints, "jointEyeRight", _rightEyeJoint->currentText()); - insertJointMapping(joints, "jointNeck", _neckJoint->currentText()); - } - if (_modelType == SKELETON_MODEL) { - insertJointMapping(joints, "jointRoot", _rootJoint->currentText()); - insertJointMapping(joints, "jointLean", _leanJoint->currentText()); - insertJointMapping(joints, "jointHead", _headJoint->currentText()); - insertJointMapping(joints, "jointLeftHand", _leftHandJoint->currentText()); - insertJointMapping(joints, "jointRightHand", _rightHandJoint->currentText()); - - mapping.remove(FREE_JOINT_FIELD); - for (int i = 0; i < _freeJoints->count() - 1; i++) { - QComboBox* box = static_cast(_freeJoints->itemAt(i)->widget()->layout()->itemAt(0)->widget()); - mapping.insertMulti(FREE_JOINT_FIELD, box->currentText()); - } - } - mapping.insert(JOINT_FIELD, joints); - } - - return mapping; -} - -static void setJointText(QComboBox* box, const QString& text) { - box->setCurrentIndex(qMax(box->findText(text), 0)); -} - -void ModelPropertiesDialog::reset() { - _name->setText(_originalMapping.value(NAME_FIELD).toString()); - _textureDirectory->setText(_originalMapping.value(TEXDIR_FIELD).toString()); - _scale->setValue(_originalMapping.value(SCALE_FIELD).toDouble()); - - QVariantHash jointHash = _originalMapping.value(JOINT_FIELD).toHash(); - - if (_modelType != ENTITY_MODEL) { - if (_modelType == ATTACHMENT_MODEL) { - _translationX->setValue(_originalMapping.value(TRANSLATION_X_FIELD).toDouble()); - _translationY->setValue(_originalMapping.value(TRANSLATION_Y_FIELD).toDouble()); - _translationZ->setValue(_originalMapping.value(TRANSLATION_Z_FIELD).toDouble()); - _pivotAboutCenter->setChecked(true); - _pivotJoint->setCurrentIndex(0); - - } else { - setJointText(_leftEyeJoint, jointHash.value("jointEyeLeft").toString()); - setJointText(_rightEyeJoint, jointHash.value("jointEyeRight").toString()); - setJointText(_neckJoint, jointHash.value("jointNeck").toString()); - } - if (_modelType == SKELETON_MODEL) { - setJointText(_rootJoint, jointHash.value("jointRoot").toString()); - setJointText(_leanJoint, jointHash.value("jointLean").toString()); - setJointText(_headJoint, jointHash.value("jointHead").toString()); - setJointText(_leftHandJoint, jointHash.value("jointLeftHand").toString()); - setJointText(_rightHandJoint, jointHash.value("jointRightHand").toString()); - - while (_freeJoints->count() > 1) { - delete _freeJoints->itemAt(0)->widget(); - } - foreach (const QVariant& joint, _originalMapping.values(FREE_JOINT_FIELD)) { - QString jointName = joint.toString(); - if (_geometry.jointIndices.contains(jointName)) { - createNewFreeJoint(jointName); - } - } - } - } -} - -void ModelPropertiesDialog::chooseTextureDirectory() { - QString directory = QFileDialog::getExistingDirectory(this, "Choose Texture Directory", - _basePath + "/" + _textureDirectory->text()); - if (directory.isEmpty()) { - return; - } - if (!directory.startsWith(_basePath)) { - QMessageBox::warning(NULL, "Invalid texture directory", "Texture directory must be child of base path."); - return; - } - _textureDirectory->setText(directory.length() == _basePath.length() ? "." : directory.mid(_basePath.length() + 1)); -} - -void ModelPropertiesDialog::updatePivotJoint() { - _pivotJoint->setEnabled(!_pivotAboutCenter->isChecked()); -} - -void ModelPropertiesDialog::createNewFreeJoint(const QString& joint) { - QWidget* freeJoint = new QWidget(); - QHBoxLayout* freeJointLayout = new QHBoxLayout(); - freeJointLayout->setContentsMargins(QMargins()); - freeJoint->setLayout(freeJointLayout); - QComboBox* jointBox = createJointBox(false); - jointBox->setCurrentText(joint); - freeJointLayout->addWidget(jointBox, 1); - QPushButton* deleteJoint = new QPushButton("Delete"); - freeJointLayout->addWidget(deleteJoint); - freeJoint->connect(deleteJoint, SIGNAL(clicked(bool)), SLOT(deleteLater())); - _freeJoints->insertWidget(_freeJoints->count() - 1, freeJoint); -} - -QComboBox* ModelPropertiesDialog::createJointBox(bool withNone) const { - QComboBox* box = new QComboBox(); - if (withNone) { - box->addItem("(none)"); - } - foreach (const FBXJoint& joint, _geometry.joints) { - if (joint.isSkeletonJoint || !_geometry.hasSkeletonJoints) { - box->addItem(joint.name); - } - } - return box; -} - -void ModelPropertiesDialog::insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const { - if (_geometry.jointIndices.contains(name)) { - joints.insert(joint, name); - } else { - joints.remove(joint); - } -} - diff --git a/interface/src/ModelUploader.h b/interface/src/ModelUploader.h deleted file mode 100644 index 59a424b346..0000000000 --- a/interface/src/ModelUploader.h +++ /dev/null @@ -1,126 +0,0 @@ -// -// ModelUploader.h -// interface/src -// -// Created by Clément Brisset on 3/4/14. -// Copyright 2014 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 -// - -#ifndef hifi_ModelUploader_h -#define hifi_ModelUploader_h - -#include -#include - -#include -#include - -#include "ui/ModelsBrowser.h" - -class QCheckBox; -class QComboBox; -class QDoubleSpinBox; -class QFileInfo; -class QHttpMultiPart; -class QLineEdit; -class QProgressBar; -class QPushButton; -class QVBoxLayout; - -class ModelUploader : public QObject { - Q_OBJECT - -public: - static void uploadModel(ModelType modelType); - - static void uploadHead(); - static void uploadSkeleton(); - static void uploadAttachment(); - static void uploadEntity(); - -private slots: - void send(); - void checkJSON(QNetworkReply& requestReply); - void uploadUpdate(qint64 bytesSent, qint64 bytesTotal); - void uploadSuccess(QNetworkReply& requestReply); - void uploadFailed(QNetworkReply& errorReply); - void checkS3(); - void processCheck(); - -private: - ModelUploader(ModelType type); - ~ModelUploader(); - - void populateBasicMapping(QVariantHash& mapping, QString filename, const FBXGeometry& geometry); - bool zip(); - bool addTextures(const QString& texdir, const FBXGeometry& geometry); - bool addPart(const QString& path, const QString& name, bool isTexture = false); - bool addPart(const QFile& file, const QByteArray& contents, const QString& name, bool isTexture = false); - - QString _url; - QString _textureBase; - QSet _textureFilenames; - int _lodCount; - int _texturesCount; - unsigned long _totalSize; - ModelType _modelType; - bool _readyToSend; - - QHttpMultiPart* _dataMultiPart = nullptr; - - int _numberOfChecks; - QTimer _timer; - - QDialog* _progressDialog = nullptr; - QProgressBar* _progressBar = nullptr; - - static Setting::Handle _lastModelUploadLocation; -}; - -/// A dialog that allows customization of various model properties. -class ModelPropertiesDialog : public QDialog { - Q_OBJECT - -public: - ModelPropertiesDialog(ModelType modelType, const QVariantHash& originalMapping, - const QString& basePath, const FBXGeometry& geometry); - - QVariantHash getMapping() const; - -private slots: - void reset(); - void chooseTextureDirectory(); - void updatePivotJoint(); - void createNewFreeJoint(const QString& joint = QString()); - -private: - QComboBox* createJointBox(bool withNone = true) const; - void insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const; - - ModelType _modelType; - QVariantHash _originalMapping; - QString _basePath; - FBXGeometry _geometry; - QLineEdit* _name = nullptr; - QPushButton* _textureDirectory = nullptr; - QDoubleSpinBox* _scale = nullptr; - QDoubleSpinBox* _translationX = nullptr; - QDoubleSpinBox* _translationY = nullptr; - QDoubleSpinBox* _translationZ = nullptr; - QCheckBox* _pivotAboutCenter = nullptr; - QComboBox* _pivotJoint = nullptr; - QComboBox* _leftEyeJoint = nullptr; - QComboBox* _rightEyeJoint = nullptr; - QComboBox* _neckJoint = nullptr; - QComboBox* _rootJoint = nullptr; - QComboBox* _leanJoint = nullptr; - QComboBox* _headJoint = nullptr; - QComboBox* _leftHandJoint = nullptr; - QComboBox* _rightHandJoint = nullptr; - QVBoxLayout* _freeJoints = nullptr; -}; - -#endif // hifi_ModelUploader_h diff --git a/interface/src/scripting/WebWindowClass.cpp b/interface/src/scripting/WebWindowClass.cpp index be87870f26..2e0f88c776 100644 --- a/interface/src/scripting/WebWindowClass.cpp +++ b/interface/src/scripting/WebWindowClass.cpp @@ -73,7 +73,7 @@ void WebWindowClass::setVisible(bool visible) { QScriptValue WebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { WebWindowClass* retVal; QString file = context->argument(0).toString(); - QMetaObject::invokeMethod(WindowScriptingInterface::getInstance(), "doCreateWebWindow", Qt::BlockingQueuedConnection, + QMetaObject::invokeMethod(DependencyManager::get().data(), "doCreateWebWindow", Qt::BlockingQueuedConnection, Q_RETURN_ARG(WebWindowClass*, retVal), Q_ARG(const QString&, file), Q_ARG(QString, context->argument(1).toString()), diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 8ec9fbbb82..52de31df3c 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -25,11 +25,6 @@ #include "WindowScriptingInterface.h" -WindowScriptingInterface* WindowScriptingInterface::getInstance() { - static WindowScriptingInterface sharedInstance; - return &sharedInstance; -} - WindowScriptingInterface::WindowScriptingInterface() : _editDialog(NULL), _nonBlockingFormActive(false), @@ -37,6 +32,7 @@ WindowScriptingInterface::WindowScriptingInterface() : { const DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::hostnameChanged, this, &WindowScriptingInterface::domainChanged); + connect(Application::getInstance(), &Application::svoImportRequested, this, &WindowScriptingInterface::svoImportRequested); } WebWindowClass* WindowScriptingInterface::doCreateWebWindow(const QString& title, const QString& url, int width, int height) { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 5c0aa4f0a8..34942366eb 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -21,7 +21,7 @@ #include "WebWindowClass.h" -class WindowScriptingInterface : public QObject { +class WindowScriptingInterface : public QObject, public Dependency { Q_OBJECT Q_PROPERTY(int innerWidth READ getInnerWidth) Q_PROPERTY(int innerHeight READ getInnerHeight) @@ -29,7 +29,7 @@ class WindowScriptingInterface : public QObject { Q_PROPERTY(int y READ getY) Q_PROPERTY(bool cursorVisible READ isCursorVisible WRITE setCursorVisible) public: - static WindowScriptingInterface* getInstance(); + WindowScriptingInterface(); int getInnerWidth(); int getInnerHeight(); int getX(); @@ -60,6 +60,7 @@ signals: void domainChanged(const QString& domainHostname); void inlineButtonClicked(const QString& name); void nonBlockingFormClosed(); + void svoImportRequested(const QString& url); private slots: QScriptValue showAlert(const QString& message); @@ -85,7 +86,6 @@ private slots: WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height); private: - WindowScriptingInterface(); QString jsRegExp2QtRegExp(QString string); QDialog* createForm(const QString& title, QScriptValue form); diff --git a/interface/src/ui/ModelsBrowser.h b/interface/src/ui/ModelsBrowser.h index fa09a67826..0c8bb59c85 100644 --- a/interface/src/ui/ModelsBrowser.h +++ b/interface/src/ui/ModelsBrowser.h @@ -16,6 +16,8 @@ #include #include +class QNetworkReply; + enum ModelType { ENTITY_MODEL, HEAD_MODEL,