diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index c2aec9b058..502cf15aa2 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -107,6 +107,10 @@ BakeVersion currentBakeVersionForAssetType(BakedAssetType type) { } } +QString getBakeMapping(const AssetUtils::AssetHash& hash, const QString& relativeFilePath) { + return AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + relativeFilePath; +} + const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server"; void AssetServer::bakeAsset(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath) { @@ -141,26 +145,27 @@ std::pair AssetServer::getAssetStatus(const A return { AssetUtils::Baked, "" }; } - auto dotIndex = path.lastIndexOf("."); - if (dotIndex == -1) { + BakedAssetType type = assetTypeForFilename(path); + if (type == BakedAssetType::Undefined) { return { AssetUtils::Irrelevant, "" }; } - auto extension = path.mid(dotIndex + 1); + bool loaded; + AssetMeta meta; + std::tie(loaded, meta) = readMetaFile(hash); - QString bakedFilename; - - if (BAKEABLE_MODEL_EXTENSIONS.contains(extension)) { - bakedFilename = BAKED_MODEL_SIMPLE_NAME; - } else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit()) && hasMetaFile(hash)) { - bakedFilename = BAKED_TEXTURE_SIMPLE_NAME; - } else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) { - bakedFilename = BAKED_SCRIPT_SIMPLE_NAME; - } else { + // We create a meta file for Skyboxes at runtime when they get requested + // Otherwise, textures don't get baked by themselves. + if (type == BakedAssetType::Texture && !loaded) { return { AssetUtils::Irrelevant, "" }; } - auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + bakedFilename; + QString bakedFilename = bakedFilenameForAssetType(type); + auto bakedPath = getBakeMapping(hash, bakedFilename); + if (loaded && !meta.redirectTarget.isEmpty()) { + bakedPath = meta.redirectTarget; + } + auto jt = _fileMappings.find(bakedPath); if (jt != _fileMappings.end()) { if (jt->second == hash) { @@ -168,14 +173,8 @@ std::pair AssetServer::getAssetStatus(const A } else { return { AssetUtils::Baked, "" }; } - } else { - bool loaded; - AssetMeta meta; - - std::tie(loaded, meta) = readMetaFile(hash); - if (loaded && meta.failedLastBake) { - return { AssetUtils::Error, meta.lastBakeErrors }; - } + } else if (loaded && meta.failedLastBake) { + return { AssetUtils::Error, meta.lastBakeErrors }; } return { AssetUtils::Pending, "" }; @@ -227,8 +226,16 @@ bool AssetServer::needsToBeBaked(const AssetUtils::AssetPath& path, const AssetU return false; } + bool loaded; + AssetMeta meta; + std::tie(loaded, meta) = readMetaFile(assetHash); + QString bakedFilename = bakedFilenameForAssetType(type); - auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + assetHash + "/" + bakedFilename; + auto bakedPath = getBakeMapping(assetHash, bakedFilename); + if (loaded && !meta.redirectTarget.isEmpty()) { + bakedPath = meta.redirectTarget; + } + auto mappingIt = _fileMappings.find(bakedPath); bool bakedMappingExists = mappingIt != _fileMappings.end(); @@ -238,10 +245,8 @@ bool AssetServer::needsToBeBaked(const AssetUtils::AssetPath& path, const AssetU return false; } - bool loaded; - AssetMeta meta; - std::tie(loaded, meta) = readMetaFile(assetHash); - + // We create a meta file for Skyboxes at runtime when they get requested + // Otherwise, textures don't get baked by themselves. if (type == BakedAssetType::Texture && !loaded) { return false; } @@ -633,36 +638,33 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, NLPacketLi if (it != _fileMappings.end()) { // check if we should re-direct to a baked asset - - // first, figure out from the mapping extension what type of file this is - auto assetPathExtension = assetPath.mid(assetPath.lastIndexOf('.') + 1).toLower(); - - auto type = assetTypeForFilename(assetPath); - QString bakedRootFile = bakedFilenameForAssetType(type); - auto originalAssetHash = it->second; QString redirectedAssetHash; - QString bakedAssetPath; quint8 wasRedirected = false; bool bakingDisabled = false; - if (!bakedRootFile.isEmpty()) { - // we ran into an asset for which we could have a baked version, let's check if it's ready - bakedAssetPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + bakedRootFile; - auto bakedIt = _fileMappings.find(bakedAssetPath); + bool loaded; + AssetMeta meta; + std::tie(loaded, meta) = readMetaFile(originalAssetHash); - if (bakedIt != _fileMappings.end()) { - if (bakedIt->second != originalAssetHash) { - qDebug() << "Did find baked version for: " << originalAssetHash << assetPath; - // we found a baked version of the requested asset to serve, redirect to that - redirectedAssetHash = bakedIt->second; - wasRedirected = true; - } else { - qDebug() << "Did not find baked version for: " << originalAssetHash << assetPath << " (disabled)"; - bakingDisabled = true; - } + auto type = assetTypeForFilename(assetPath); + QString bakedRootFile = bakedFilenameForAssetType(type); + QString bakedAssetPath = getBakeMapping(originalAssetHash, bakedRootFile); + + if (loaded && !meta.redirectTarget.isEmpty()) { + bakedAssetPath = meta.redirectTarget; + } + + auto bakedIt = _fileMappings.find(bakedAssetPath); + if (bakedIt != _fileMappings.end()) { + if (bakedIt->second != originalAssetHash) { + qDebug() << "Did find baked version for: " << originalAssetHash << assetPath; + // we found a baked version of the requested asset to serve, redirect to that + redirectedAssetHash = bakedIt->second; + wasRedirected = true; } else { - qDebug() << "Did not find baked version for: " << originalAssetHash << assetPath; + qDebug() << "Did not find baked version for: " << originalAssetHash << assetPath << " (disabled)"; + bakingDisabled = true; } } @@ -684,20 +686,13 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, NLPacketLi auto query = QUrlQuery(url.query()); bool isSkybox = query.hasQueryItem("skybox"); - if (isSkybox) { - bool loaded; - AssetMeta meta; - std::tie(loaded, meta) = readMetaFile(originalAssetHash); - - if (!loaded) { - AssetMeta needsBakingMeta; - needsBakingMeta.bakeVersion = NEEDS_BAKING_BAKE_VERSION; - - writeMetaFile(originalAssetHash, needsBakingMeta); - if (!bakingDisabled) { - maybeBake(assetPath, originalAssetHash); - } + if (isSkybox && !loaded) { + AssetMeta needsBakingMeta; + needsBakingMeta.bakeVersion = NEEDS_BAKING_BAKE_VERSION; + writeMetaFile(originalAssetHash, needsBakingMeta); + if (!bakingDisabled) { + maybeBake(assetPath, originalAssetHash); } } } @@ -1297,14 +1292,6 @@ bool AssetServer::renameMapping(AssetUtils::AssetPath oldPath, AssetUtils::Asset } } -static const QString BAKED_ASSET_SIMPLE_FBX_NAME = "asset.fbx"; -static const QString BAKED_ASSET_SIMPLE_TEXTURE_NAME = "texture.ktx"; -static const QString BAKED_ASSET_SIMPLE_JS_NAME = "asset.js"; - -QString getBakeMapping(const AssetUtils::AssetHash& hash, const QString& relativeFilePath) { - return AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + relativeFilePath; -} - void AssetServer::handleFailedBake(QString originalAssetHash, QString assetPath, QString errors) { qDebug() << "Failed to bake: " << originalAssetHash << assetPath << "(" << errors << ")"; @@ -1326,12 +1313,78 @@ void AssetServer::handleFailedBake(QString originalAssetHash, QString assetPath, } void AssetServer::handleCompletedBake(QString originalAssetHash, QString originalAssetPath, - QString bakedTempOutputDir, QVector bakedFilePaths) { + QString bakedTempOutputDir) { + auto reportCompletion = [this, originalAssetPath, originalAssetHash](bool errorCompletingBake, + QString errorReason, + QString redirectTarget) { + auto type = assetTypeForFilename(originalAssetPath); + auto currentTypeVersion = currentBakeVersionForAssetType(type); + + AssetMeta meta; + meta.bakeVersion = currentTypeVersion; + meta.failedLastBake = errorCompletingBake; + meta.redirectTarget = redirectTarget; + + if (errorCompletingBake) { + qWarning() << "Could not complete bake for" << originalAssetHash; + meta.lastBakeErrors = errorReason; + } + + writeMetaFile(originalAssetHash, meta); + + _pendingBakes.remove(originalAssetHash); + }; + bool errorCompletingBake { false }; QString errorReason; + QString redirectTarget; qDebug() << "Completing bake for " << originalAssetHash; + // Find the directory containing the baked content + QDir outputDir(bakedTempOutputDir); + QString outputDirName = outputDir.dirName(); + auto directories = outputDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + QString bakedDirectoryPath; + for (const auto& dirName : directories) { + outputDir.cd(dirName); + if (outputDir.exists("baked") && outputDir.exists("original")) { + bakedDirectoryPath = outputDir.filePath("baked"); + break; + } + outputDir.cdUp(); + } + if (bakedDirectoryPath.isEmpty()) { + errorCompletingBake = true; + errorReason = "Failed to find baking output"; + + // Cleanup temporary output directory + PathUtils::deleteMyTemporaryDir(outputDirName); + reportCompletion(errorCompletingBake, errorReason, redirectTarget); + return; + } + + // Compile list of all the baked files + QDirIterator it(bakedDirectoryPath, QDirIterator::Subdirectories); + QVector bakedFilePaths; + while (it.hasNext()) { + it.next(); + if (it.fileInfo().isFile()) { + bakedFilePaths.push_back(it.filePath()); + } + } + if (bakedFilePaths.isEmpty()) { + errorCompletingBake = true; + errorReason = "Baking output has no files"; + + // Cleanup temporary output directory + PathUtils::deleteMyTemporaryDir(outputDirName); + reportCompletion(errorCompletingBake, errorReason, redirectTarget); + return; + } + + QDir bakedDirectory(bakedDirectoryPath); + for (auto& filePath : bakedFilePaths) { // figure out the hash for the contents of this file QFile file(filePath); @@ -1340,89 +1393,72 @@ void AssetServer::handleCompletedBake(QString originalAssetHash, QString origina AssetUtils::AssetHash bakedFileHash; - if (file.open(QIODevice::ReadOnly)) { - QCryptographicHash hasher(QCryptographicHash::Sha256); - - if (hasher.addData(&file)) { - bakedFileHash = hasher.result().toHex(); - } else { - // stop handling this bake, couldn't hash the contents of the file - errorCompletingBake = true; - errorReason = "Failed to finalize bake"; - break; - } - - // first check that we don't already have this bake file in our list - auto bakeFileDestination = _filesDirectory.absoluteFilePath(bakedFileHash); - if (!QFile::exists(bakeFileDestination)) { - // copy each to our files folder (with the hash as their filename) - if (!file.copy(_filesDirectory.absoluteFilePath(bakedFileHash))) { - // stop handling this bake, couldn't copy the bake file into our files directory - errorCompletingBake = true; - errorReason = "Failed to copy baked assets to asset server"; - break; - } - } - - // setup the mapping for this bake file - auto relativeFilePath = QUrl(filePath).fileName(); - qDebug() << "Relative file path is: " << relativeFilePath; - if (relativeFilePath.endsWith(".fbx", Qt::CaseInsensitive)) { - // for an FBX file, we replace the filename with the simple name - // (to handle the case where two mapped assets have the same hash but different names) - relativeFilePath = BAKED_ASSET_SIMPLE_FBX_NAME; - } else if (relativeFilePath.endsWith(".js", Qt::CaseInsensitive)) { - relativeFilePath = BAKED_ASSET_SIMPLE_JS_NAME; - } else if (!originalAssetPath.endsWith(".fbx", Qt::CaseInsensitive)) { - relativeFilePath = BAKED_ASSET_SIMPLE_TEXTURE_NAME; - } - - QString bakeMapping = getBakeMapping(originalAssetHash, relativeFilePath); - - // add a mapping (under the hidden baked folder) for this file resulting from the bake - if (setMapping(bakeMapping, bakedFileHash)) { - qDebug() << "Added" << bakeMapping << "for bake file" << bakedFileHash << "from bake of" << originalAssetHash; - } else { - qDebug() << "Failed to set mapping"; - // stop handling this bake, couldn't add a mapping for this bake file - errorCompletingBake = true; - errorReason = "Failed to finalize bake"; - break; - } - } else { + if (!file.open(QIODevice::ReadOnly)) { qDebug() << "Failed to open baked file: " << filePath; // stop handling this bake, we couldn't open one of the files for reading errorCompletingBake = true; - errorReason = "Failed to finalize bake"; + errorReason = "Could not open baked file " + file.fileName(); break; } - } - for (auto& filePath : bakedFilePaths) { - QFile file(filePath); - if (!file.remove()) { - qWarning() << "Failed to remove temporary file:" << filePath; + QCryptographicHash hasher(QCryptographicHash::Sha256); + + if (!hasher.addData(&file)) { + // stop handling this bake, couldn't hash the contents of the file + errorCompletingBake = true; + errorReason = "Could not hash data for " + file.fileName(); + break; } - } - if (!QDir(bakedTempOutputDir).rmdir(".")) { - qWarning() << "Failed to remove temporary directory:" << bakedTempOutputDir; + + bakedFileHash = hasher.result().toHex(); + + // first check that we don't already have this bake file in our list + auto bakeFileDestination = _filesDirectory.absoluteFilePath(bakedFileHash); + if (!QFile::exists(bakeFileDestination)) { + // copy each to our files folder (with the hash as their filename) + if (!file.copy(_filesDirectory.absoluteFilePath(bakedFileHash))) { + // stop handling this bake, couldn't copy the bake file into our files directory + errorCompletingBake = true; + errorReason = "Failed to copy baked assets to asset server"; + break; + } + } + + // setup the mapping for this bake file + auto relativeFilePath = bakedDirectory.relativeFilePath(filePath); + + QString bakeMapping = getBakeMapping(originalAssetHash, relativeFilePath); + + // Check if this is the file we should redirect to when someone asks for the original asset + if ((relativeFilePath.endsWith(".baked.fst", Qt::CaseInsensitive) && originalAssetPath.endsWith(".fbx")) || + (relativeFilePath.endsWith(".texmeta.json", Qt::CaseInsensitive) && !originalAssetPath.endsWith(".fbx"))) { + if (!redirectTarget.isEmpty()) { + qWarning() << "Found multiple baked redirect target for" << originalAssetPath; + } + redirectTarget = bakeMapping; + } + + // add a mapping (under the hidden baked folder) for this file resulting from the bake + if (!setMapping(bakeMapping, bakedFileHash)) { + qDebug() << "Failed to set mapping"; + // stop handling this bake, couldn't add a mapping for this bake file + errorCompletingBake = true; + errorReason = "Failed to set mapping for baked file " + file.fileName(); + break; + } + + qDebug() << "Added" << bakeMapping << "for bake file" << bakedFileHash << "from bake of" << originalAssetHash; } - auto type = assetTypeForFilename(originalAssetPath); - auto currentTypeVersion = currentBakeVersionForAssetType(type); - AssetMeta meta; - meta.bakeVersion = currentTypeVersion; - meta.failedLastBake = errorCompletingBake; - - if (errorCompletingBake) { - qWarning() << "Could not complete bake for" << originalAssetHash; - meta.lastBakeErrors = errorReason; + if (redirectTarget.isEmpty()) { + errorCompletingBake = true; + errorReason = "Could not find root file for baked output"; } - writeMetaFile(originalAssetHash, meta); - - _pendingBakes.remove(originalAssetHash); + // Cleanup temporary output directory + PathUtils::deleteMyTemporaryDir(outputDirName); + reportCompletion(errorCompletingBake, errorReason, redirectTarget); } void AssetServer::handleAbortedBake(QString originalAssetHash, QString assetPath) { @@ -1435,6 +1471,7 @@ void AssetServer::handleAbortedBake(QString originalAssetHash, QString assetPath static const QString BAKE_VERSION_KEY = "bake_version"; static const QString FAILED_LAST_BAKE_KEY = "failed_last_bake"; static const QString LAST_BAKE_ERRORS_KEY = "last_bake_errors"; +static const QString REDIRECT_TARGET_KEY = "redirect_target"; std::pair AssetServer::readMetaFile(AssetUtils::AssetHash hash) { auto metaFilePath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + "meta.json"; @@ -1461,6 +1498,7 @@ std::pair AssetServer::readMetaFile(AssetUtils::AssetHash hash) auto bakeVersion = root[BAKE_VERSION_KEY]; auto failedLastBake = root[FAILED_LAST_BAKE_KEY]; auto lastBakeErrors = root[LAST_BAKE_ERRORS_KEY]; + auto redirectTarget = root[REDIRECT_TARGET_KEY]; if (bakeVersion.isDouble() && failedLastBake.isBool() @@ -1470,6 +1508,7 @@ std::pair AssetServer::readMetaFile(AssetUtils::AssetHash hash) meta.bakeVersion = bakeVersion.toInt(); meta.failedLastBake = failedLastBake.toBool(); meta.lastBakeErrors = lastBakeErrors.toString(); + meta.redirectTarget = redirectTarget.toString(); return { true, meta }; } else { @@ -1488,6 +1527,7 @@ bool AssetServer::writeMetaFile(AssetUtils::AssetHash originalAssetHash, const A metaFileObject[BAKE_VERSION_KEY] = (int)meta.bakeVersion; metaFileObject[FAILED_LAST_BAKE_KEY] = meta.failedLastBake; metaFileObject[LAST_BAKE_ERRORS_KEY] = meta.lastBakeErrors; + metaFileObject[REDIRECT_TARGET_KEY] = meta.redirectTarget; QJsonDocument metaFileDoc; metaFileDoc.setObject(metaFileObject); @@ -1521,10 +1561,18 @@ bool AssetServer::setBakingEnabled(const AssetUtils::AssetPathList& paths, bool if (type == BakedAssetType::Undefined) { continue; } - QString bakedFilename = bakedFilenameForAssetType(type); auto hash = it->second; + + bool loaded; + AssetMeta meta; + std::tie(loaded, meta) = readMetaFile(hash); + + QString bakedFilename = bakedFilenameForAssetType(type); auto bakedMapping = getBakeMapping(hash, bakedFilename); + if (loaded && !meta.redirectTarget.isEmpty()) { + bakedMapping = meta.redirectTarget; + } auto it = _fileMappings.find(bakedMapping); bool currentlyDisabled = (it != _fileMappings.end() && it->second == hash); diff --git a/assignment-client/src/assets/AssetServer.h b/assignment-client/src/assets/AssetServer.h index b3d0f18a8f..fe84df5141 100644 --- a/assignment-client/src/assets/AssetServer.h +++ b/assignment-client/src/assets/AssetServer.h @@ -62,12 +62,10 @@ enum class ScriptBakeVersion : BakeVersion { }; struct AssetMeta { - AssetMeta() { - } - BakeVersion bakeVersion { INITIAL_BAKE_VERSION }; bool failedLastBake { false }; QString lastBakeErrors; + QString redirectTarget; }; class BakeAssetTask; @@ -139,8 +137,7 @@ private: void bakeAsset(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath); /// Move baked content for asset to baked directory and update baked status - void handleCompletedBake(QString originalAssetHash, QString assetPath, QString bakedTempOutputDir, - QVector bakedFilePaths); + void handleCompletedBake(QString originalAssetHash, QString assetPath, QString bakedTempOutputDir); void handleFailedBake(QString originalAssetHash, QString assetPath, QString errors); void handleAbortedBake(QString originalAssetHash, QString assetPath); diff --git a/assignment-client/src/assets/BakeAssetTask.cpp b/assignment-client/src/assets/BakeAssetTask.cpp index ecb4ede5d8..7c845f9b38 100644 --- a/assignment-client/src/assets/BakeAssetTask.cpp +++ b/assignment-client/src/assets/BakeAssetTask.cpp @@ -36,33 +36,38 @@ BakeAssetTask::BakeAssetTask(const AssetUtils::AssetHash& assetHash, const Asset }); } -void cleanupTempFiles(QString tempOutputDir, std::vector files) { - for (const auto& filename : files) { - QFile f { filename }; - if (!f.remove()) { - qDebug() << "Failed to remove:" << filename; - } - } - if (!tempOutputDir.isEmpty()) { - QDir dir { tempOutputDir }; - if (!dir.rmdir(".")) { - qDebug() << "Failed to remove temporary directory:" << tempOutputDir; - } - } -}; - void BakeAssetTask::run() { if (_isBaking.exchange(true)) { qWarning() << "Tried to start bake asset task while already baking"; return; } + // Make a new temporary directory for the Oven to work in QString tempOutputDir = PathUtils::generateTemporaryDir(); + QString tempOutputDirName = QDir(tempOutputDir).dirName(); + if (tempOutputDir.isEmpty()) { + QString errors = "Could not create temporary working directory"; + emit bakeFailed(_assetHash, _assetPath, errors); + PathUtils::deleteMyTemporaryDir(tempOutputDirName); + return; + } + + // Copy file to bake the temporary dir and give a name the oven can work with + auto assetName = _assetPath.split("/").last(); + auto tempAssetPath = tempOutputDir + "/" + assetName; + auto success = QFile::copy(_filePath, tempAssetPath); + if (!success) { + QString errors = "Couldn't copy file to bake to temporary directory"; + emit bakeFailed(_assetHash, _assetPath, errors); + PathUtils::deleteMyTemporaryDir(tempOutputDirName); + return; + } + auto base = QFileInfo(QCoreApplication::applicationFilePath()).absoluteDir(); QString path = base.absolutePath() + "/oven"; QString extension = _assetPath.mid(_assetPath.lastIndexOf('.') + 1); QStringList args { - "-i", _filePath, + "-i", tempAssetPath, "-o", tempOutputDir, "-t", extension, }; @@ -72,10 +77,11 @@ void BakeAssetTask::run() { QEventLoop loop; connect(_ovenProcess.get(), static_cast(&QProcess::finished), - this, [&loop, this, tempOutputDir](int exitCode, QProcess::ExitStatus exitStatus) { + this, [&loop, this, tempOutputDir, tempAssetPath, tempOutputDirName](int exitCode, QProcess::ExitStatus exitStatus) { qDebug() << "Baking process finished: " << exitCode << exitStatus; if (exitStatus == QProcess::CrashExit) { + PathUtils::deleteMyTemporaryDir(tempOutputDirName); if (_wasAborted) { emit bakeAborted(_assetHash, _assetPath); } else { @@ -83,16 +89,10 @@ void BakeAssetTask::run() { emit bakeFailed(_assetHash, _assetPath, errors); } } else if (exitCode == OVEN_STATUS_CODE_SUCCESS) { - QDir outputDir = tempOutputDir; - auto files = outputDir.entryInfoList(QDir::Files); - QVector outputFiles; - for (auto& file : files) { - outputFiles.push_back(file.absoluteFilePath()); - } - - emit bakeComplete(_assetHash, _assetPath, tempOutputDir, outputFiles); + emit bakeComplete(_assetHash, _assetPath, tempOutputDir); } else if (exitStatus == QProcess::NormalExit && exitCode == OVEN_STATUS_CODE_ABORT) { _wasAborted.store(true); + PathUtils::deleteMyTemporaryDir(tempOutputDirName); emit bakeAborted(_assetHash, _assetPath); } else { QString errors; @@ -107,6 +107,7 @@ void BakeAssetTask::run() { errors = "Unknown error occurred while baking"; } } + PathUtils::deleteMyTemporaryDir(tempOutputDirName); emit bakeFailed(_assetHash, _assetPath, errors); } @@ -115,7 +116,10 @@ void BakeAssetTask::run() { qDebug() << "Starting oven for " << _assetPath; _ovenProcess->start(path, args, QIODevice::ReadOnly); - if (!_ovenProcess->waitForStarted(-1)) { + qDebug() << "Running:" << path << args; + if (!_ovenProcess->waitForStarted()) { + PathUtils::deleteMyTemporaryDir(tempOutputDirName); + QString errors = "Oven process failed to start"; emit bakeFailed(_assetHash, _assetPath, errors); return; diff --git a/assignment-client/src/assets/BakeAssetTask.h b/assignment-client/src/assets/BakeAssetTask.h index 24b070d08a..2d50a26bc1 100644 --- a/assignment-client/src/assets/BakeAssetTask.h +++ b/assignment-client/src/assets/BakeAssetTask.h @@ -37,7 +37,7 @@ public slots: void abort(); signals: - void bakeComplete(QString assetHash, QString assetPath, QString tempOutputDir, QVector outputFiles); + void bakeComplete(QString assetHash, QString assetPath, QString tempOutputDir); void bakeFailed(QString assetHash, QString assetPath, QString errors); void bakeAborted(QString assetHash, QString assetPath); diff --git a/cmake/externals/LibOVR/CMakeLists.txt b/cmake/externals/LibOVR/CMakeLists.txt index ae4cf6320e..481753f7e0 100644 --- a/cmake/externals/LibOVR/CMakeLists.txt +++ b/cmake/externals/LibOVR/CMakeLists.txt @@ -17,8 +17,8 @@ if (WIN32) ExternalProject_Add( ${EXTERNAL_NAME} - URL https://public.highfidelity.com/dependencies/ovr_sdk_win_1.26.0_public.zip - URL_MD5 06804ff9727b910dcd04a37c800053b5 + URL https://hifi-public.s3.amazonaws.com/dependencies/ovr_sdk_win_1.35.0.zip + URL_MD5 1e3e8b2101387af07ff9c841d0ea285e CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= PATCH_COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/LibOVRCMakeLists.txt" /CMakeLists.txt LOG_DOWNLOAD 1 diff --git a/interface/resources/qml/BubbleIcon.qml b/interface/resources/qml/BubbleIcon.qml index f4e99f136c..b5a7ddb2d8 100644 --- a/interface/resources/qml/BubbleIcon.qml +++ b/interface/resources/qml/BubbleIcon.qml @@ -23,15 +23,15 @@ Rectangle { property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled; function updateOpacity() { - if (ignoreRadiusEnabled) { - bubbleRect.opacity = 1.0; - } else { - bubbleRect.opacity = 0.7; - } + var rectOpacity = ignoreRadiusEnabled ? 1.0 : (mouseArea.containsMouse ? 1.0 : 0.7); + bubbleRect.opacity = rectOpacity; } Component.onCompleted: { updateOpacity(); + AvatarInputs.ignoreRadiusEnabledChanged.connect(function() { + ignoreRadiusEnabled = AvatarInputs.ignoreRadiusEnabled; + }); } onIgnoreRadiusEnabledChanged: { @@ -74,10 +74,10 @@ Rectangle { } drag.target: dragTarget; onContainsMouseChanged: { - var rectOpacity = (ignoreRadiusEnabled && containsMouse) ? 1.0 : (containsMouse ? 1.0 : 0.7); if (containsMouse) { Tablet.playSound(TabletEnums.ButtonHover); } + var rectOpacity = ignoreRadiusEnabled ? 1.0 : (mouseArea.containsMouse ? 1.0 : 0.7); bubbleRect.opacity = rectOpacity; } } diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 5f8cc2eefb..65453ba21d 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -952,7 +952,7 @@ Rectangle { text: "LOG IN" onClicked: { - sendToScript({method: 'needsLogIn_loginClicked'}); + sendToScript({method: 'marketplace_loginClicked'}); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml b/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml index 0a1593161a..e17196b492 100644 --- a/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml +++ b/interface/resources/qml/hifi/commerce/wallet/NeedsLogIn.qml @@ -137,7 +137,7 @@ Item { width: parent.width/2 - anchors.leftMargin*2; text: "Cancel" onClicked: { - sendToScript({method: 'needsLogIn_cancelClicked'}); + sendToScript({method: 'passphrasePopup_cancelClicked'}); } } @@ -155,7 +155,7 @@ Item { width: parent.width/2 - anchors.rightMargin*2; text: "Log In" onClicked: { - sendToScript({method: 'needsLogIn_loginClicked'}); + sendToScript({method: 'marketplace_loginClicked'}); } } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 55a6fc6672..9205658fe0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1861,12 +1861,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); // So that camera doesn't auto-switch to third person. - } else if (Menu::getInstance()->isOptionChecked(MenuOption::IndependentMode)) { - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - cameraMenuChanged(); - } else if (Menu::getInstance()->isOptionChecked(MenuOption::CameraEntityMode)) { - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - cameraMenuChanged(); } { @@ -2337,7 +2331,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo DependencyManager::get()->setPrecisionPicking(rayPickID, value); }); - EntityItem::setBillboardRotationOperator([this](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) { + EntityItem::setBillboardRotationOperator([](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) { if (billboardMode == BillboardMode::YAW) { //rotate about vertical to face the camera glm::vec3 dPosition = frustumPos - position; @@ -2365,7 +2359,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo DependencyManager::get()->setKickConfirmationOperator([this] (const QUuid& nodeID) { userKickConfirmation(nodeID); }); - render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { + render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([=](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { bool isTablet = url == TabletScriptingInterface::QML; if (htmlContent) { webSurface = DependencyManager::get()->acquire(render::entities::WebEntityRenderer::QML); @@ -2405,7 +2399,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo const uint8_t TABLET_FPS = 90; webSurface->setMaxFps(isTablet ? TABLET_FPS : DEFAULT_MAX_FPS); }); - render::entities::WebEntityRenderer::setReleaseWebSurfaceOperator([this](QSharedPointer& webSurface, bool& cachedWebSurface, std::vector& connections) { + render::entities::WebEntityRenderer::setReleaseWebSurfaceOperator([=](QSharedPointer& webSurface, bool& cachedWebSurface, std::vector& connections) { QQuickItem* rootItem = webSurface->getRootItem(); // Fix for crash in QtWebEngineCore when rapidly switching domains @@ -5758,9 +5752,6 @@ void Application::cycleCamera() { menu->setIsOptionChecked(MenuOption::ThirdPerson, false); menu->setIsOptionChecked(MenuOption::FullscreenMirror, true); - } else if (menu->isOptionChecked(MenuOption::IndependentMode) || menu->isOptionChecked(MenuOption::CameraEntityMode)) { - // do nothing if in independent or camera entity modes - return; } cameraMenuChanged(); // handle the menu change } @@ -5776,12 +5767,6 @@ void Application::cameraModeChanged() { case CAMERA_MODE_MIRROR: Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, true); break; - case CAMERA_MODE_INDEPENDENT: - Menu::getInstance()->setIsOptionChecked(MenuOption::IndependentMode, true); - break; - case CAMERA_MODE_ENTITY: - Menu::getInstance()->setIsOptionChecked(MenuOption::CameraEntityMode, true); - break; default: break; } @@ -5825,14 +5810,6 @@ void Application::cameraMenuChanged() { getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); } } - } else if (menu->isOptionChecked(MenuOption::IndependentMode)) { - if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { - _myCamera.setMode(CAMERA_MODE_INDEPENDENT); - } - } else if (menu->isOptionChecked(MenuOption::CameraEntityMode)) { - if (_myCamera.getMode() != CAMERA_MODE_ENTITY) { - _myCamera.setMode(CAMERA_MODE_ENTITY); - } } } @@ -6747,6 +6724,11 @@ void Application::updateRenderArgs(float deltaTime) { } } + appRenderArgs._renderArgs._stencilMaskMode = getActiveDisplayPlugin()->getStencilMaskMode(); + if (appRenderArgs._renderArgs._stencilMaskMode == StencilMaskMode::MESH) { + appRenderArgs._renderArgs._stencilMaskOperator = getActiveDisplayPlugin()->getStencilMaskMeshOperator(); + } + { QMutexLocker viewLocker(&_viewMutex); _myCamera.loadViewFrustum(_displayViewFrustum); @@ -8434,11 +8416,23 @@ void Application::loadAvatarBrowser() const { DependencyManager::get()->openTablet(); } +void Application::addSnapshotOperator(const SnapshotOperator& snapshotOperator) { + std::lock_guard lock(_snapshotMutex); + _snapshotOperators.push(snapshotOperator); + _hasPrimarySnapshot = _hasPrimarySnapshot || std::get<2>(snapshotOperator); +} + +bool Application::takeSnapshotOperators(std::queue& snapshotOperators) { + std::lock_guard lock(_snapshotMutex); + bool hasPrimarySnapshot = _hasPrimarySnapshot; + _hasPrimarySnapshot = false; + _snapshotOperators.swap(snapshotOperators); + return hasPrimarySnapshot; +} + void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) { - postLambdaEvent([notify, includeAnimated, aspectRatio, filename, this] { - // Get a screenshot and save it - QString path = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename, - TestScriptingInterface::getInstance()->getTestResultsLocation()); + addSnapshotOperator(std::make_tuple([notify, includeAnimated, aspectRatio, filename](const QImage& snapshot) { + QString path = DependencyManager::get()->saveSnapshot(snapshot, filename, TestScriptingInterface::getInstance()->getTestResultsLocation()); // If we're not doing an animated snapshot as well... if (!includeAnimated) { @@ -8447,19 +8441,20 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa emit DependencyManager::get()->stillSnapshotTaken(path, notify); } } else if (!SnapshotAnimated::isAlreadyTakingSnapshotAnimated()) { - // Get an animated GIF snapshot and save it - SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get()); + qApp->postLambdaEvent([path, aspectRatio] { + // Get an animated GIF snapshot and save it + SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, DependencyManager::get()); + }); } - }); + }, aspectRatio, true)); } void Application::takeSecondaryCameraSnapshot(const bool& notify, const QString& filename) { - postLambdaEvent([notify, filename, this] { - QString snapshotPath = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, - TestScriptingInterface::getInstance()->getTestResultsLocation()); + addSnapshotOperator(std::make_tuple([notify, filename](const QImage& snapshot) { + QString snapshotPath = DependencyManager::get()->saveSnapshot(snapshot, filename, TestScriptingInterface::getInstance()->getTestResultsLocation()); emit DependencyManager::get()->stillSnapshotTaken(snapshotPath, notify); - }); + }, 0.0f, false)); } void Application::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) { diff --git a/interface/src/Application.h b/interface/src/Application.h index e0e8821037..12523f3108 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -345,6 +345,10 @@ public: void toggleAwayMode(); #endif + using SnapshotOperator = std::tuple, float, bool>; + void addSnapshotOperator(const SnapshotOperator& snapshotOperator); + bool takeSnapshotOperators(std::queue& snapshotOperators); + signals: void svoImportRequested(const QString& url); @@ -789,6 +793,9 @@ private: AudioInjectorPointer _snapshotSoundInjector; SharedSoundPointer _snapshotSound; SharedSoundPointer _sampleSound; + std::mutex _snapshotMutex; + std::queue _snapshotOperators; + bool _hasPrimarySnapshot { false }; DisplayPluginPointer _autoSwitchDisplayModeSupportedHMDPlugin; QString _autoSwitchDisplayModeSupportedHMDPluginName; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 2c34ecd780..0b7c4bd5db 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -194,20 +194,6 @@ Menu::Menu() { viewMirrorAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup)); - // View > Independent - auto viewIndependentAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu, - MenuOption::IndependentMode, 0, - false, qApp, SLOT(cameraMenuChanged()))); - - viewIndependentAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup)); - - // View > Entity Camera - auto viewEntityCameraAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu, - MenuOption::CameraEntityMode, 0, - false, qApp, SLOT(cameraMenuChanged()))); - - viewEntityCameraAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup)); - viewMenu->addSeparator(); // View > Center Player In View diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 3611faaf8f..550913bb21 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -53,7 +53,6 @@ namespace MenuOption { const QString BookmarkAvatarEntities = "Bookmark Avatar Entities"; const QString BookmarkLocation = "Bookmark Location"; const QString CalibrateCamera = "Calibrate Camera"; - const QString CameraEntityMode = "Entity Mode"; const QString CenterPlayerInView = "Center Player In View"; const QString Chat = "Chat..."; const QString ClearDiskCache = "Clear Disk Cache"; @@ -120,7 +119,6 @@ namespace MenuOption { const QString Help = "Help..."; const QString HomeLocation = "Home "; const QString IncreaseAvatarSize = "Increase Avatar Size"; - const QString IndependentMode = "Independent Mode"; const QString ActionMotorControl = "Enable Default Motor Control"; const QString LastLocation = "Last Location"; const QString LoadScript = "Open and Run Script File..."; diff --git a/interface/src/ModelSelector.cpp b/interface/src/ModelSelector.cpp index 3223e3ab9c..6da9327cac 100644 --- a/interface/src/ModelSelector.cpp +++ b/interface/src/ModelSelector.cpp @@ -18,9 +18,6 @@ #include #include -static const QString AVATAR_HEAD_AND_BODY_STRING = "Avatar Body with Head"; -static const QString ENTITY_MODEL_STRING = "Entity Model"; - ModelSelector::ModelSelector() { QFormLayout* form = new QFormLayout(this); diff --git a/interface/src/SecondaryCamera.cpp b/interface/src/SecondaryCamera.cpp index 12c9636746..da2874a3f4 100644 --- a/interface/src/SecondaryCamera.cpp +++ b/interface/src/SecondaryCamera.cpp @@ -152,10 +152,12 @@ public: _cachedArgsPointer->_viewport = args->_viewport; _cachedArgsPointer->_displayMode = args->_displayMode; _cachedArgsPointer->_renderMode = args->_renderMode; + _cachedArgsPointer->_stencilMaskMode = args->_stencilMaskMode; args->_blitFramebuffer = destFramebuffer; args->_viewport = glm::ivec4(0, 0, destFramebuffer->getWidth(), destFramebuffer->getHeight()); args->_displayMode = RenderArgs::MONO; args->_renderMode = RenderArgs::RenderMode::SECONDARY_CAMERA_RENDER_MODE; + args->_stencilMaskMode = StencilMaskMode::NONE; gpu::doInBatch("SecondaryCameraJob::run", args->_context, [&](gpu::Batch& batch) { batch.disableContextStereo(); @@ -255,10 +257,11 @@ public: void run(const render::RenderContextPointer& renderContext, const RenderArgsPointer& cachedArgs) { auto args = renderContext->args; if (cachedArgs) { - args->_blitFramebuffer = cachedArgs->_blitFramebuffer; - args->_viewport = cachedArgs->_viewport; - args->_displayMode = cachedArgs->_displayMode; - args->_renderMode = cachedArgs->_renderMode; + args->_blitFramebuffer = cachedArgs->_blitFramebuffer; + args->_viewport = cachedArgs->_viewport; + args->_displayMode = cachedArgs->_displayMode; + args->_renderMode = cachedArgs->_renderMode; + args->_stencilMaskMode = cachedArgs->_stencilMaskMode; } args->popViewFrustum(); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 390500dd2a..da69afd923 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -735,7 +735,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic boxHit._distance = FLT_MAX; for (size_t i = 0; i < hit._boundJoints.size(); i++) { - assert(hit._boundJoints[i] < multiSpheres.size()); + assert(hit._boundJoints[i] < (int)multiSpheres.size()); auto &mSphere = multiSpheres[hit._boundJoints[i]]; if (mSphere.isValid()) { float boundDistance = FLT_MAX; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 25c7a788b3..935c92a4e0 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1630,7 +1630,9 @@ void MyAvatar::handleChangedAvatarEntityData() { if (!skip) { sanitizeAvatarEntityProperties(properties); entityTree->withWriteLock([&] { - entityTree->updateEntity(id, properties); + if (entityTree->updateEntity(id, properties)) { + packetSender->queueEditAvatarEntityMessage(entityTree, id); + } }); } } @@ -5811,12 +5813,19 @@ void MyAvatar::releaseGrab(const QUuid& grabID) { } void MyAvatar::addAvatarHandsToFlow(const std::shared_ptr& otherAvatar) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "addAvatarHandsToFlow", + Q_ARG(const std::shared_ptr&, otherAvatar)); + return; + } auto &flow = _skeletonModel->getRig().getFlow(); - for (auto &handJointName : HAND_COLLISION_JOINTS) { - int jointIndex = otherAvatar->getJointIndex(handJointName); - if (jointIndex != -1) { - glm::vec3 position = otherAvatar->getJointPosition(jointIndex); - flow.setOthersCollision(otherAvatar->getID(), jointIndex, position); + if (otherAvatar != nullptr && flow.getActive()) { + for (auto &handJointName : HAND_COLLISION_JOINTS) { + int jointIndex = otherAvatar->getJointIndex(handJointName); + if (jointIndex != -1) { + glm::vec3 position = otherAvatar->getJointPosition(jointIndex); + flow.setOthersCollision(otherAvatar->getID(), jointIndex, position); + } } } } diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 5644f9ea4c..e1e872ac24 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -41,326 +41,246 @@ #include "ui/SecurityImageProvider.h" #include "scripting/HMDScriptingInterface.h" -static const char* KEY_FILE = "hifikey"; -static const char* INSTRUCTIONS_FILE = "backup_instructions.html"; -static const char* IMAGE_HEADER = "-----BEGIN SECURITY IMAGE-----\n"; -static const char* IMAGE_FOOTER = "-----END SECURITY IMAGE-----\n"; +namespace { + const char* KEY_FILE = "hifikey"; + const char* INSTRUCTIONS_FILE = "backup_instructions.html"; + const char* IMAGE_HEADER = "-----BEGIN SECURITY IMAGE-----\n"; + const char* IMAGE_FOOTER = "-----END SECURITY IMAGE-----\n"; -void initialize() { - static bool initialized = false; - if (!initialized) { - SSL_load_error_strings(); - SSL_library_init(); - OpenSSL_add_all_algorithms(); - initialized = true; - } -} - -QString keyFilePath() { - auto accountManager = DependencyManager::get(); - return PathUtils::getAppDataFilePath(QString("%1.%2").arg(accountManager->getAccountInfo().getUsername(), KEY_FILE)); -} -bool Wallet::copyKeyFileFrom(const QString& pathname) { - QString existing = getKeyFilePath(); - qCDebug(commerce) << "Old keyfile" << existing; - if (!existing.isEmpty()) { - QString backup = QString(existing).insert(existing.indexOf(KEY_FILE) - 1, - QDateTime::currentDateTime().toString(Qt::ISODate).replace(":", "")); - qCDebug(commerce) << "Renaming old keyfile to" << backup; - if (!QFile::rename(existing, backup)) { - qCCritical(commerce) << "Unable to backup" << existing << "to" << backup; - return false; + void initialize() { + static bool initialized = false; + if (!initialized) { + SSL_load_error_strings(); + SSL_library_init(); + OpenSSL_add_all_algorithms(); + initialized = true; } } - QString destination = keyFilePath(); - bool result = QFile::copy(pathname, destination); - qCDebug(commerce) << "copy" << pathname << "to" << destination << "=>" << result; - return result; -} -// use the cached _passphrase if it exists, otherwise we need to prompt -int passwordCallback(char* password, int maxPasswordSize, int rwFlag, void* u) { - // just return a hardcoded pwd for now - auto wallet = DependencyManager::get(); - auto passphrase = wallet->getPassphrase(); - if (passphrase && !passphrase->isEmpty()) { - QString saltedPassphrase(*passphrase); - saltedPassphrase.append(wallet->getSalt()); - strcpy(password, saltedPassphrase.toUtf8().constData()); - return static_cast(passphrase->size()); - } else { - // this shouldn't happen - so lets log it to tell us we have - // a problem with the flow... - qCCritical(commerce) << "no cached passphrase while decrypting!"; - return 0; + QString keyFilePath() { + auto accountManager = DependencyManager::get(); + return PathUtils::getAppDataFilePath(QString("%1.%2").arg(accountManager->getAccountInfo().getUsername(), KEY_FILE)); } -} -EC_KEY* readKeys(QString filename) { - QFile file(filename); - EC_KEY* key = NULL; - if (file.open(QFile::ReadOnly)) { - // file opened successfully - qCDebug(commerce) << "opened key file" << filename; + // use the cached _passphrase if it exists, otherwise we need to prompt + int passwordCallback(char* password, int maxPasswordSize, int rwFlag, void* u) { + // just return a hardcoded pwd for now + auto wallet = DependencyManager::get(); + auto passphrase = wallet->getPassphrase(); + if (passphrase && !passphrase->isEmpty()) { + QString saltedPassphrase(*passphrase); + saltedPassphrase.append(wallet->getSalt()); + strcpy(password, saltedPassphrase.toUtf8().constData()); + return static_cast(passphrase->size()); + } else { + // this shouldn't happen - so lets log it to tell us we have + // a problem with the flow... + qCCritical(commerce) << "no cached passphrase while decrypting!"; + return 0; + } + } - QByteArray pemKeyBytes = file.readAll(); - BIO* bufio = BIO_new_mem_buf((void*)pemKeyBytes.constData(), pemKeyBytes.length()); - if ((key = PEM_read_bio_EC_PUBKEY(bufio, NULL, NULL, NULL))) { - // now read private key + EC_KEY* readKeys(QString filename) { + QFile file(filename); + EC_KEY* key = NULL; + if (file.open(QFile::ReadOnly)) { + // file opened successfully + qCDebug(commerce) << "opened key file" << filename; - qCDebug(commerce) << "read public key"; + QByteArray pemKeyBytes = file.readAll(); + BIO* bufio = BIO_new_mem_buf((void*)pemKeyBytes.constData(), pemKeyBytes.length()); + if ((key = PEM_read_bio_EC_PUBKEY(bufio, NULL, NULL, NULL))) { + // now read private key - if ((key = PEM_read_bio_ECPrivateKey(bufio, &key, passwordCallback, NULL))) { - qCDebug(commerce) << "read private key"; - BIO_free(bufio); - file.close(); + qCDebug(commerce) << "read public key"; + + if ((key = PEM_read_bio_ECPrivateKey(bufio, &key, passwordCallback, NULL))) { + qCDebug(commerce) << "read private key"; + } else { + qCDebug(commerce) << "failed to read private key"; + } } else { - qCDebug(commerce) << "failed to read private key"; + qCDebug(commerce) << "failed to read public key"; } - } else { - qCDebug(commerce) << "failed to read public key"; - } - BIO_free(bufio); - file.close(); - } else { - qCDebug(commerce) << "failed to open key file" << filename; - } - return key; -} - -bool Wallet::writeBackupInstructions() { - QString inputFilename(PathUtils::resourcesPath() + "html/commerce/backup_instructions.html"); - QString outputFilename = PathUtils::getAppDataFilePath(INSTRUCTIONS_FILE); - QFile inputFile(inputFilename); - QFile outputFile(outputFilename); - bool retval = false; - - if (getKeyFilePath().isEmpty()) { - return false; - } - - if (QFile::exists(inputFilename) && inputFile.open(QIODevice::ReadOnly)) { - if (outputFile.open(QIODevice::ReadWrite)) { - // Read the data from the original file, then close it - QByteArray fileData = inputFile.readAll(); - inputFile.close(); - - // Translate the data from the original file into a QString - QString text(fileData); - - // Replace the necessary string - text.replace(QString("HIFIKEY_PATH_REPLACEME"), keyFilePath()); - - // Write the new text back to the file - outputFile.write(text.toUtf8()); - - // Close the output file - outputFile.close(); - - retval = true; - qCDebug(commerce) << "wrote html file successfully"; - } else { - qCDebug(commerce) << "failed to open output html file" << outputFilename; - } - } else { - qCDebug(commerce) << "failed to open input html file" << inputFilename; - } - return retval; -} - -bool writeKeys(QString filename, EC_KEY* keys) { - BIO* bio = BIO_new(BIO_s_mem()); - bool retval = false; - if (!PEM_write_bio_EC_PUBKEY(bio, keys)) { - BIO_free(bio); - qCCritical(commerce) << "failed to write public key"; - return retval; - } - - if (!PEM_write_bio_ECPrivateKey(bio, keys, EVP_des_ede3_cbc(), NULL, 0, passwordCallback, NULL)) { - BIO_free(bio); - qCCritical(commerce) << "failed to write private key"; - return retval; - } - - QFile file(filename); - if (!file.open(QIODevice::WriteOnly)) { - const char* bio_data; - long bio_size = BIO_get_mem_data(bio, &bio_data); - - QByteArray keyBytes(bio_data, bio_size); - file.write(keyBytes); - retval = true; - qCDebug(commerce) << "wrote keys successfully"; - file.close(); - } else { - qCDebug(commerce) << "failed to open key file" << filename; - } - BIO_free(bio); - return retval; -} - -bool Wallet::setWallet(const QByteArray& wallet) { - QFile file(keyFilePath()); - if (!file.open(QIODevice::WriteOnly)) { - qCCritical(commerce) << "Unable to open wallet for write in" << keyFilePath(); - return false; - } - if (file.write(wallet) != wallet.count()) { - qCCritical(commerce) << "Unable to write wallet in" << keyFilePath(); - return false; - } - file.close(); - return true; -} -QByteArray Wallet::getWallet() { - QFile file(keyFilePath()); - if (!file.open(QIODevice::ReadOnly)) { - qCInfo(commerce) << "No existing wallet in" << keyFilePath(); - return QByteArray(); - } - QByteArray wallet = file.readAll(); - file.close(); - return wallet; -} - -QPair generateECKeypair() { - EC_KEY* keyPair = EC_KEY_new_by_curve_name(NID_secp256k1); - QPair retval{}; - - EC_KEY_set_asn1_flag(keyPair, OPENSSL_EC_NAMED_CURVE); - if (!EC_KEY_generate_key(keyPair)) { - qCDebug(commerce) << "Error generating EC Keypair -" << ERR_get_error(); - return retval; - } - - // grab the public key and private key from the file - unsigned char* publicKeyDER = NULL; - int publicKeyLength = i2d_EC_PUBKEY(keyPair, &publicKeyDER); - - unsigned char* privateKeyDER = NULL; - int privateKeyLength = i2d_ECPrivateKey(keyPair, &privateKeyDER); - - if (publicKeyLength <= 0 || privateKeyLength <= 0) { - qCDebug(commerce) << "Error getting DER public or private key from EC struct -" << ERR_get_error(); - - // cleanup the EC struct - EC_KEY_free(keyPair); - - // cleanup the public and private key DER data, if required - if (publicKeyLength > 0) { - OPENSSL_free(publicKeyDER); - } - - if (privateKeyLength > 0) { - OPENSSL_free(privateKeyDER); - } - - return retval; - } - - if (!writeKeys(keyFilePath(), keyPair)) { - qCDebug(commerce) << "couldn't save keys!"; - return retval; - } - - EC_KEY_free(keyPair); - - // prepare the return values. TODO: Fix this - we probably don't really even want the - // private key at all (better to read it when we need it?). Or maybe we do, when we have - // multiple keys? - retval.first = new QByteArray(reinterpret_cast(publicKeyDER), publicKeyLength); - retval.second = new QByteArray(reinterpret_cast(privateKeyDER), privateKeyLength); - - // cleanup the publicKeyDER and publicKeyDER data - OPENSSL_free(publicKeyDER); - OPENSSL_free(privateKeyDER); - return retval; -} -// END copied code (which will soon change) - -// the public key can just go into a byte array -QByteArray readPublicKey(QString filename) { - QByteArray retval; - QFile file(filename); - if (file.open(QIODevice::ReadOnly)) { - // file opened successfully - qCDebug(commerce) << "opened key file" << filename; - - QByteArray pemKeyBytes = file.readAll(); - BIO* bufio = BIO_new_mem_buf((void*)pemKeyBytes.constData(), pemKeyBytes.length()); - - EC_KEY* key = PEM_read_bio_EC_PUBKEY(bufio, NULL, NULL, NULL); - if (key) { - // file read successfully - unsigned char* publicKeyDER = NULL; - int publicKeyLength = i2d_EC_PUBKEY(key, &publicKeyDER); - // TODO: check for 0 length? - - // cleanup - EC_KEY_free(key); - - qCDebug(commerce) << "parsed public key file successfully"; - - QByteArray retval((char*)publicKeyDER, publicKeyLength); - OPENSSL_free(publicKeyDER); BIO_free(bufio); file.close(); + } else { + qCDebug(commerce) << "failed to open key file" << filename; + } + return key; + } + + bool writeKeys(QString filename, EC_KEY* keys) { + BIO* bio = BIO_new(BIO_s_mem()); + bool retval = false; + if (!PEM_write_bio_EC_PUBKEY(bio, keys)) { + BIO_free(bio); + qCCritical(commerce) << "failed to write public key"; return retval; - } else { - qCDebug(commerce) << "couldn't parse" << filename; } - BIO_free(bufio); - file.close(); - } else { - qCDebug(commerce) << "couldn't open" << filename; - } - return QByteArray(); -} -// the private key should be read/copied into heap memory. For now, we need the EC_KEY struct -// so I'll return that. -EC_KEY* readPrivateKey(QString filename) { - QFile file(filename); - EC_KEY* key = NULL; - if (file.open(QIODevice::ReadOnly)) { - // file opened successfully - qCDebug(commerce) << "opened key file" << filename; - - QByteArray pemKeyBytes = file.readAll(); - BIO* bufio = BIO_new_mem_buf((void*)pemKeyBytes.constData(), pemKeyBytes.length()); - - if ((key = PEM_read_bio_ECPrivateKey(bufio, &key, passwordCallback, NULL))) { - qCDebug(commerce) << "parsed private key file successfully"; - - } else { - qCDebug(commerce) << "couldn't parse" << filename; - // if the passphrase is wrong, then let's not cache it - DependencyManager::get()->setPassphrase(""); + if (!PEM_write_bio_ECPrivateKey(bio, keys, EVP_des_ede3_cbc(), NULL, 0, passwordCallback, NULL)) { + BIO_free(bio); + qCCritical(commerce) << "failed to write private key"; + return retval; } - BIO_free(bufio); - file.close(); - } else { - qCDebug(commerce) << "couldn't open" << filename; - } - return key; -} -// QT's QByteArray will convert to Base64 without any embedded newlines. This just -// writes it with embedded newlines, which is more readable. -void outputBase64WithNewlines(QFile& file, const QByteArray& b64Array) { - for (int i = 0; i < b64Array.size(); i += 64) { - file.write(b64Array.mid(i, 64)); - file.write("\n"); - } -} + QFile file(filename); + if (!file.open(QIODevice::WriteOnly)) { + const char* bio_data; + long bio_size = BIO_get_mem_data(bio, &bio_data); -void initializeAESKeys(unsigned char* ivec, unsigned char* ckey, const QByteArray& salt) { - // use the ones in the wallet - auto wallet = DependencyManager::get(); - memcpy(ivec, wallet->getIv(), 16); - memcpy(ckey, wallet->getCKey(), 32); -} + QByteArray keyBytes(bio_data, bio_size); + file.write(keyBytes); + retval = true; + qCDebug(commerce) << "wrote keys successfully"; + file.close(); + } else { + qCDebug(commerce) << "failed to open key file" << filename; + } + BIO_free(bio); + return retval; + } + + QPair generateECKeypair() { + EC_KEY* keyPair = EC_KEY_new_by_curve_name(NID_secp256k1); + QPair retval {}; + + EC_KEY_set_asn1_flag(keyPair, OPENSSL_EC_NAMED_CURVE); + if (!EC_KEY_generate_key(keyPair)) { + qCDebug(commerce) << "Error generating EC Keypair -" << ERR_get_error(); + return retval; + } + + // grab the public key and private key from the file + unsigned char* publicKeyDER = NULL; + int publicKeyLength = i2d_EC_PUBKEY(keyPair, &publicKeyDER); + + unsigned char* privateKeyDER = NULL; + int privateKeyLength = i2d_ECPrivateKey(keyPair, &privateKeyDER); + + if (publicKeyLength <= 0 || privateKeyLength <= 0) { + qCDebug(commerce) << "Error getting DER public or private key from EC struct -" << ERR_get_error(); + + // cleanup the EC struct + EC_KEY_free(keyPair); + + // cleanup the public and private key DER data, if required + if (publicKeyLength > 0) { + OPENSSL_free(publicKeyDER); + } + + if (privateKeyLength > 0) { + OPENSSL_free(privateKeyDER); + } + + return retval; + } + + if (!writeKeys(keyFilePath(), keyPair)) { + qCDebug(commerce) << "couldn't save keys!"; + return retval; + } + + EC_KEY_free(keyPair); + + // prepare the return values. TODO: Fix this - we probably don't really even want the + // private key at all (better to read it when we need it?). Or maybe we do, when we have + // multiple keys? + retval.first = new QByteArray(reinterpret_cast(publicKeyDER), publicKeyLength); + retval.second = new QByteArray(reinterpret_cast(privateKeyDER), privateKeyLength); + + // cleanup the publicKeyDER and publicKeyDER data + OPENSSL_free(publicKeyDER); + OPENSSL_free(privateKeyDER); + return retval; + } + // END copied code (which will soon change) + + // the public key can just go into a byte array + QByteArray readPublicKey(QString filename) { + QByteArray retval; + QFile file(filename); + if (file.open(QIODevice::ReadOnly)) { + // file opened successfully + qCDebug(commerce) << "opened key file" << filename; + + QByteArray pemKeyBytes = file.readAll(); + BIO* bufio = BIO_new_mem_buf((void*)pemKeyBytes.constData(), pemKeyBytes.length()); + + EC_KEY* key = PEM_read_bio_EC_PUBKEY(bufio, NULL, NULL, NULL); + if (key) { + // file read successfully + unsigned char* publicKeyDER = NULL; + int publicKeyLength = i2d_EC_PUBKEY(key, &publicKeyDER); + // TODO: check for 0 length? + + // cleanup + EC_KEY_free(key); + + qCDebug(commerce) << "parsed public key file successfully"; + + QByteArray retval((char*)publicKeyDER, publicKeyLength); + OPENSSL_free(publicKeyDER); + BIO_free(bufio); + file.close(); + return retval; + } else { + qCDebug(commerce) << "couldn't parse" << filename; + } + BIO_free(bufio); + file.close(); + } else { + qCDebug(commerce) << "couldn't open" << filename; + } + return QByteArray(); + } + + // the private key should be read/copied into heap memory. For now, we need the EC_KEY struct + // so I'll return that. + EC_KEY* readPrivateKey(QString filename) { + QFile file(filename); + EC_KEY* key = NULL; + if (file.open(QIODevice::ReadOnly)) { + // file opened successfully + qCDebug(commerce) << "opened key file" << filename; + + QByteArray pemKeyBytes = file.readAll(); + BIO* bufio = BIO_new_mem_buf((void*)pemKeyBytes.constData(), pemKeyBytes.length()); + + if ((key = PEM_read_bio_ECPrivateKey(bufio, &key, passwordCallback, NULL))) { + qCDebug(commerce) << "parsed private key file successfully"; + + } else { + qCDebug(commerce) << "couldn't parse" << filename; + // if the passphrase is wrong, then let's not cache it + DependencyManager::get()->setPassphrase(""); + } + BIO_free(bufio); + file.close(); + } else { + qCDebug(commerce) << "couldn't open" << filename; + } + return key; + } + + // QT's QByteArray will convert to Base64 without any embedded newlines. This just + // writes it with embedded newlines, which is more readable. + void outputBase64WithNewlines(QFile& file, const QByteArray& b64Array) { + for (int i = 0; i < b64Array.size(); i += 64) { + file.write(b64Array.mid(i, 64)); + file.write("\n"); + } + } + + void initializeAESKeys(unsigned char* ivec, unsigned char* ckey, const QByteArray& salt) { + // use the ones in the wallet + auto wallet = DependencyManager::get(); + memcpy(ivec, wallet->getIv(), 16); + memcpy(ckey, wallet->getCKey(), 32); + } + +} // close unnamed namespace Wallet::Wallet() { auto nodeList = DependencyManager::get(); @@ -423,6 +343,88 @@ Wallet::~Wallet() { } } +bool Wallet::setWallet(const QByteArray& wallet) { + QFile file(keyFilePath()); + if (!file.open(QIODevice::WriteOnly)) { + qCCritical(commerce) << "Unable to open wallet for write in" << keyFilePath(); + return false; + } + if (file.write(wallet) != wallet.count()) { + qCCritical(commerce) << "Unable to write wallet in" << keyFilePath(); + return false; + } + file.close(); + return true; +} +QByteArray Wallet::getWallet() { + QFile file(keyFilePath()); + if (!file.open(QIODevice::ReadOnly)) { + qCInfo(commerce) << "No existing wallet in" << keyFilePath(); + return QByteArray(); + } + QByteArray wallet = file.readAll(); + file.close(); + return wallet; +} + +bool Wallet::copyKeyFileFrom(const QString& pathname) { + QString existing = getKeyFilePath(); + qCDebug(commerce) << "Old keyfile" << existing; + if (!existing.isEmpty()) { + QString backup = QString(existing).insert(existing.indexOf(KEY_FILE) - 1, + QDateTime::currentDateTime().toString(Qt::ISODate).replace(":", "")); + qCDebug(commerce) << "Renaming old keyfile to" << backup; + if (!QFile::rename(existing, backup)) { + qCCritical(commerce) << "Unable to backup" << existing << "to" << backup; + return false; + } + } + QString destination = keyFilePath(); + bool result = QFile::copy(pathname, destination); + qCDebug(commerce) << "copy" << pathname << "to" << destination << "=>" << result; + return result; +} + +bool Wallet::writeBackupInstructions() { + QString inputFilename(PathUtils::resourcesPath() + "html/commerce/backup_instructions.html"); + QString outputFilename = PathUtils::getAppDataFilePath(INSTRUCTIONS_FILE); + QFile inputFile(inputFilename); + QFile outputFile(outputFilename); + bool retval = false; + + if (getKeyFilePath().isEmpty()) { + return false; + } + + if (QFile::exists(inputFilename) && inputFile.open(QIODevice::ReadOnly)) { + if (outputFile.open(QIODevice::ReadWrite)) { + // Read the data from the original file, then close it + QByteArray fileData = inputFile.readAll(); + inputFile.close(); + + // Translate the data from the original file into a QString + QString text(fileData); + + // Replace the necessary string + text.replace(QString("HIFIKEY_PATH_REPLACEME"), keyFilePath()); + + // Write the new text back to the file + outputFile.write(text.toUtf8()); + + // Close the output file + outputFile.close(); + + retval = true; + qCDebug(commerce) << "wrote html file successfully"; + } else { + qCDebug(commerce) << "failed to open output html file" << outputFilename; + } + } else { + qCDebug(commerce) << "failed to open input html file" << inputFilename; + } + return retval; +} + bool Wallet::setPassphrase(const QString& passphrase) { if (_passphrase) { delete _passphrase; diff --git a/interface/src/graphics/GraphicsEngine.cpp b/interface/src/graphics/GraphicsEngine.cpp index c2137d3d97..267822baf2 100644 --- a/interface/src/graphics/GraphicsEngine.cpp +++ b/interface/src/graphics/GraphicsEngine.cpp @@ -244,6 +244,7 @@ void GraphicsEngine::render_performFrame() { finalFramebuffer = framebufferCache->getFramebuffer(); } + std::queue snapshotOperators; if (!_programsCompiled.load()) { gpu::doInBatch("splashFrame", _gpuContext, [&](gpu::Batch& batch) { batch.setFramebuffer(finalFramebuffer); @@ -271,6 +272,7 @@ void GraphicsEngine::render_performFrame() { PROFILE_RANGE(render, "/runRenderFrame"); renderArgs._hudOperator = displayPlugin->getHUDOperator(); renderArgs._hudTexture = qApp->getApplicationOverlay().getOverlayTexture(); + renderArgs._takingSnapshot = qApp->takeSnapshotOperators(snapshotOperators); renderArgs._blitFramebuffer = finalFramebuffer; render_runRenderFrame(&renderArgs); } @@ -285,6 +287,7 @@ void GraphicsEngine::render_performFrame() { frameBufferCache->releaseFramebuffer(framebuffer); } }; + frame->snapshotOperators = snapshotOperators; // deliver final scene rendering commands to the display plugin { PROFILE_RANGE(render, "/pluginOutput"); diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index caae946116..b406c097e7 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -174,14 +174,10 @@ void Audio::setPTTDesktop(bool enabled) { _pttDesktop = enabled; } }); - if (!enabled) { - // Set to default behavior (unmuted for Desktop) on Push-To-Talk disable. - setMutedDesktop(true); - } else { - // Should be muted when not pushing to talk while PTT is enabled. + if (enabled || _settingsLoaded) { + // Set to default behavior (muted for Desktop) on Push-To-Talk disable or when enabled. Settings also need to be loaded. setMutedDesktop(true); } - if (changed) { emit pushToTalkChanged(enabled); emit pushToTalkDesktopChanged(enabled); @@ -202,12 +198,9 @@ void Audio::setPTTHMD(bool enabled) { _pttHMD = enabled; } }); - if (!enabled) { - // Set to default behavior (unmuted for HMD) on Push-To-Talk disable. - setMutedHMD(false); - } else { - // Should be muted when not pushing to talk while PTT is enabled. - setMutedHMD(true); + if (enabled || _settingsLoaded) { + // Set to default behavior (unmuted for HMD) on Push-To-Talk disable or muted for when PTT is enabled. + setMutedHMD(enabled); } if (changed) { @@ -231,6 +224,7 @@ void Audio::loadData() { auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted()), Q_ARG(bool, false)); + _settingsLoaded = true; } bool Audio::getPTTHMD() const { @@ -357,10 +351,12 @@ void Audio::onContextChanged() { changed = true; } }); - if (isHMD) { - setMuted(getMutedHMD()); - } else { - setMuted(getMutedDesktop()); + if (_settingsLoaded) { + bool isMuted = isHMD ? getMutedHMD() : getMutedDesktop(); + setMuted(isMuted); + // always set audio client muted state on context changed - sometimes setMuted does not catch it. + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); } if (changed) { emit contextChanged(isHMD ? Audio::HMD : Audio::DESKTOP); diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 00da566b30..f7116ced3a 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -409,6 +409,7 @@ protected: private: + bool _settingsLoaded { false }; float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; float _localInjectorGain { 0.0f }; // in dB diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index b4f504822f..459b224474 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -138,7 +138,7 @@ void LoginDialog::login(const QString& username, const QString& password) const void LoginDialog::loginThroughOculus() { qDebug() << "Attempting to login through Oculus"; if (auto oculusPlatformPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { - oculusPlatformPlugin->requestNonceAndUserID([this] (QString nonce, QString oculusID) { + oculusPlatformPlugin->requestNonceAndUserID([] (QString nonce, QString oculusID) { DependencyManager::get()->requestAccessTokenWithOculus(nonce, oculusID); }); } diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index 60c039ce1f..d97c401351 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -159,47 +159,57 @@ void Snapshot::save360Snapshot(const glm::vec3& cameraPosition, secondaryCameraRenderConfig->setOrientation(CAMERA_ORIENTATION_DOWN); _snapshotIndex = 0; + _taking360Snapshot = true; _snapshotTimer.start(SNAPSHOT_360_TIMER_INTERVAL); } void Snapshot::takeNextSnapshot() { - SecondaryCameraJobConfig* config = - static_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera")); + if (_taking360Snapshot) { + if (!_waitingOnSnapshot) { + _waitingOnSnapshot = true; + qApp->addSnapshotOperator(std::make_tuple([this](const QImage& snapshot) { + // Order is: + // 0. Down + // 1. Front + // 2. Left + // 3. Back + // 4. Right + // 5. Up + if (_snapshotIndex < 6) { + _imageArray[_snapshotIndex] = snapshot; + } - // Order is: - // 0. Down - // 1. Front - // 2. Left - // 3. Back - // 4. Right - // 5. Up - if (_snapshotIndex < 6) { - _imageArray[_snapshotIndex] = qApp->getActiveDisplayPlugin()->getSecondaryCameraScreenshot(); - } + SecondaryCameraJobConfig* config = static_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera")); + if (_snapshotIndex == 0) { + // Setup for Front Image capture + config->setOrientation(CAMERA_ORIENTATION_FRONT); + } else if (_snapshotIndex == 1) { + // Setup for Left Image capture + config->setOrientation(CAMERA_ORIENTATION_LEFT); + } else if (_snapshotIndex == 2) { + // Setup for Back Image capture + config->setOrientation(CAMERA_ORIENTATION_BACK); + } else if (_snapshotIndex == 3) { + // Setup for Right Image capture + config->setOrientation(CAMERA_ORIENTATION_RIGHT); + } else if (_snapshotIndex == 4) { + // Setup for Up Image capture + config->setOrientation(CAMERA_ORIENTATION_UP); + } else if (_snapshotIndex == 5) { + _taking360Snapshot = false; + } - if (_snapshotIndex == 0) { - // Setup for Front Image capture - config->setOrientation(CAMERA_ORIENTATION_FRONT); - } else if (_snapshotIndex == 1) { - // Setup for Left Image capture - config->setOrientation(CAMERA_ORIENTATION_LEFT); - } else if (_snapshotIndex == 2) { - // Setup for Back Image capture - config->setOrientation(CAMERA_ORIENTATION_BACK); - } else if (_snapshotIndex == 3) { - // Setup for Right Image capture - config->setOrientation(CAMERA_ORIENTATION_RIGHT); - } else if (_snapshotIndex == 4) { - // Setup for Up Image capture - config->setOrientation(CAMERA_ORIENTATION_UP); - } else if (_snapshotIndex > 5) { + _waitingOnSnapshot = false; + _snapshotIndex++; + }, 0.0f, false)); + } + } else { _snapshotTimer.stop(); // Reset secondary camera render config - static_cast( - qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCameraJob.ToneMapping")) - ->setCurve(1); + SecondaryCameraJobConfig* config = static_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera")); + static_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCameraJob.ToneMapping"))->setCurve(1); config->resetSizeSpectatorCamera(qApp->getWindow()->geometry().width(), qApp->getWindow()->geometry().height()); config->setProperty("attachedEntityId", _oldAttachedEntityId); config->setProperty("vFoV", _oldvFoV); @@ -217,8 +227,6 @@ void Snapshot::takeNextSnapshot() { QtConcurrent::run([this]() { convertToEquirectangular(); }); } } - - _snapshotIndex++; } void Snapshot::convertToCubemap() { diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index 77bdfd4ac1..f13f4cd587 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -97,6 +97,8 @@ private: bool _cubemapOutputFormat; QTimer _snapshotTimer; qint16 _snapshotIndex; + bool _waitingOnSnapshot { false }; + bool _taking360Snapshot { false }; bool _oldEnabled; QVariant _oldAttachedEntityId; QVariant _oldOrientation; diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index 9d58d89385..b8cffca8ab 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -27,7 +27,6 @@ QString SnapshotAnimated::snapshotAnimatedPath; QString SnapshotAnimated::snapshotStillPath; QVector SnapshotAnimated::snapshotAnimatedFrameVector; QVector SnapshotAnimated::snapshotAnimatedFrameDelayVector; -Application* SnapshotAnimated::app; float SnapshotAnimated::aspectRatio; QSharedPointer SnapshotAnimated::snapshotAnimatedDM; GifWriter SnapshotAnimated::snapshotAnimatedGifWriter; @@ -36,12 +35,11 @@ GifWriter SnapshotAnimated::snapshotAnimatedGifWriter; Setting::Handle SnapshotAnimated::alsoTakeAnimatedSnapshot("alsoTakeAnimatedSnapshot", true); Setting::Handle SnapshotAnimated::snapshotAnimatedDuration("snapshotAnimatedDuration", SNAPSNOT_ANIMATED_DURATION_SECS); -void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm) { +void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, QSharedPointer dm) { // If we're not in the middle of capturing an animated snapshot... if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { SnapshotAnimated::snapshotAnimatedTimer = new QTimer(); SnapshotAnimated::aspectRatio = aspectRatio; - SnapshotAnimated::app = app; SnapshotAnimated::snapshotAnimatedDM = dm; // Define the output location of the still and animated snapshots. SnapshotAnimated::snapshotStillPath = pathStill; @@ -62,44 +60,45 @@ void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio void SnapshotAnimated::captureFrames() { if (SnapshotAnimated::snapshotAnimatedTimerRunning) { - // Get a screenshot from the display, then scale the screenshot down, - // then convert it to the image format the GIF library needs, - // then save all that to the QImage named "frame" - QImage frame(SnapshotAnimated::app->getActiveDisplayPlugin()->getScreenshot(SnapshotAnimated::aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); - SnapshotAnimated::snapshotAnimatedFrameVector.append(frame); + qApp->addSnapshotOperator(std::make_tuple([](const QImage& snapshot) { + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame = snapshot.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); + SnapshotAnimated::snapshotAnimatedFrameVector.append(frame); - // If that was the first frame... - if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { - // Record the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - // Record the first frame timestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; - SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // If this is an intermediate or the final frame... - } else { - // Push the current frame delay onto the vector - SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp)) / 10)); - // Record the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + // If that was the first frame... + if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + // Record the first frame timestamp + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; + SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // If this is an intermediate or the final frame... + } else { + // Push the current frame delay onto the vector + SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp)) / 10)); + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - // If that was the last frame... - if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) { - SnapshotAnimated::snapshotAnimatedTimerRunning = false; - - // Notify the user that we're processing the snapshot - // This also pops up the "Share" dialog. The unprocessed GIF will be visualized as a loading icon until processingGifCompleted() is called. - emit SnapshotAnimated::snapshotAnimatedDM->processingGifStarted(SnapshotAnimated::snapshotStillPath); - - // Kick off the thread that'll pack the frames into the GIF - QtConcurrent::run(processFrames); - // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE - // that the slot will not be called again in the future. - // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html - SnapshotAnimated::snapshotAnimatedTimer->stop(); - delete SnapshotAnimated::snapshotAnimatedTimer; + // If that was the last frame... + if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) { + SnapshotAnimated::snapshotAnimatedTimerRunning = false; + } } - } + }, SnapshotAnimated::aspectRatio, true)); + } else { + // Notify the user that we're processing the snapshot + // This also pops up the "Share" dialog. The unprocessed GIF will be visualized as a loading icon until processingGifCompleted() is called. + emit SnapshotAnimated::snapshotAnimatedDM->processingGifStarted(SnapshotAnimated::snapshotStillPath); + + // Kick off the thread that'll pack the frames into the GIF + QtConcurrent::run(processFrames); + // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE + // that the slot will not be called again in the future. + // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html + SnapshotAnimated::snapshotAnimatedTimer->stop(); + delete SnapshotAnimated::snapshotAnimatedTimer; } } diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h index 87ce533fc3..15734f57c8 100644 --- a/interface/src/ui/SnapshotAnimated.h +++ b/interface/src/ui/SnapshotAnimated.h @@ -42,7 +42,6 @@ private: static QVector snapshotAnimatedFrameVector; static QVector snapshotAnimatedFrameDelayVector; static QSharedPointer snapshotAnimatedDM; - static Application* app; static float aspectRatio; static GifWriter snapshotAnimatedGifWriter; @@ -51,7 +50,7 @@ private: static void processFrames(); static void clearTempVariables(); public: - static void saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); + static void saveSnapshotAnimated(QString pathStill, float aspectRatio, QSharedPointer dm); static bool isAlreadyTakingSnapshotAnimated() { return snapshotAnimatedFirstFrameTimestamp != 0; }; static Setting::Handle alsoTakeAnimatedSnapshot; static Setting::Handle snapshotAnimatedDuration; diff --git a/libraries/animation/src/AnimVariant.h b/libraries/animation/src/AnimVariant.h index eb9ebd33dd..a8bdb885e5 100644 --- a/libraries/animation/src/AnimVariant.h +++ b/libraries/animation/src/AnimVariant.h @@ -261,7 +261,7 @@ public: qCDebug(animation) << " " << pair.first << "=" << pair.second.getString(); break; default: - assert(("invalid AnimVariant::Type", false)); + assert(false); } } } diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index 01897ee5e9..9b80041bcf 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -142,6 +142,27 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector& meshes, const } else if (object->name == "Texture" || object->name == "Video") { // this is an embedded texture, we need to remove it from the FBX object = rootChild.children.erase(object); + } else if (object->name == "Material") { + for (FBXNode& materialChild : object->children) { + if (materialChild.name == "Properties60" || materialChild.name == "Properties70") { + // This is a properties node + // Remove the material texture scale because that is now included in the material JSON + // Texture nodes are removed, so their texture scale is effectively gone already + static const QVariant MAYA_UV_SCALE = hifi::ByteArray("Maya|uv_scale"); + static const QVariant MAYA_UV_OFFSET = hifi::ByteArray("Maya|uv_offset"); + for (int i = 0; i < materialChild.children.size(); i++) { + const auto& prop = materialChild.children[i]; + const auto& propertyName = prop.properties.at(0); + if (propertyName == MAYA_UV_SCALE || + propertyName == MAYA_UV_OFFSET) { + materialChild.children.removeAt(i); + --i; + } + } + } + } + + object++; } else { object++; } diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 79e31014fd..85e203a2cb 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -276,4 +276,8 @@ void MaterialBaker::setMaterials(const QHash& materials, addTexture(material.name, image::TextureUsage::SCATTERING_TEXTURE, material.scatteringTexture); addTexture(material.name, image::TextureUsage::LIGHTMAP_TEXTURE, material.lightmapTexture); } +} + +void MaterialBaker::setMaterials(const NetworkMaterialResourcePointer& materialResource) { + _materialResource = materialResource; } \ No newline at end of file diff --git a/libraries/baking/src/MaterialBaker.h b/libraries/baking/src/MaterialBaker.h index 7a7411142e..9469466cb9 100644 --- a/libraries/baking/src/MaterialBaker.h +++ b/libraries/baking/src/MaterialBaker.h @@ -31,6 +31,9 @@ public: QString getBakedMaterialData() const { return _bakedMaterialData; } void setMaterials(const QHash& materials, const QString& baseURL); + void setMaterials(const NetworkMaterialResourcePointer& materialResource); + + NetworkMaterialResourcePointer getNetworkMaterialResource() const { return _materialResource; } static void setNextOvenWorkerThreadOperator(std::function getNextOvenWorkerThreadOperator) { _getNextOvenWorkerThreadOperator = getNextOvenWorkerThreadOperator; } diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 00f7a38627..9de32158c5 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -241,14 +241,12 @@ void ModelBaker::bakeSourceCopy() { config->getJobConfig("BuildDracoMesh")->setEnabled(true); // Do not permit potentially lossy modification of joint data meant for runtime ((PrepareJointsConfig*)config->getJobConfig("PrepareJoints"))->passthrough = true; - // The resources parsed from this job will not be used for now - // TODO: Proper full baking of all materials for a model - config->getJobConfig("ParseMaterialMapping")->setEnabled(false); // Begin hfm baking baker.run(); _hfmModel = baker.getHFMModel(); + _materialMapping = baker.getMaterialMapping(); dracoMeshes = baker.getDracoMeshes(); dracoMaterialLists = baker.getDracoMaterialLists(); } @@ -260,7 +258,7 @@ void ModelBaker::bakeSourceCopy() { return; } - if (_hfmModel->materials.size() > 0) { + if (!_hfmModel->materials.isEmpty()) { _materialBaker = QSharedPointer( new MaterialBaker(_modelURL.fileName(), true, _bakedOutputDir), &MaterialBaker::deleteLater @@ -269,7 +267,7 @@ void ModelBaker::bakeSourceCopy() { connect(_materialBaker.data(), &MaterialBaker::finished, this, &ModelBaker::handleFinishedMaterialBaker); _materialBaker->bake(); } else { - outputBakedFST(); + bakeMaterialMap(); } } @@ -285,26 +283,14 @@ void ModelBaker::handleFinishedMaterialBaker() { auto baseName = relativeBakedMaterialURL.left(relativeBakedMaterialURL.lastIndexOf('.')); relativeBakedMaterialURL = baseName + BAKED_MATERIAL_EXTENSION; - // First we add the materials in the model - QJsonArray materialMapping; - for (auto material : _hfmModel->materials) { - QJsonObject json; - json["mat::" + material.name] = relativeBakedMaterialURL + "#" + material.name; - materialMapping.push_back(json); - } - - // The we add any existing mappings from the mapping - if (_mapping.contains(MATERIAL_MAPPING_FIELD)) { - QByteArray materialMapValue = _mapping[MATERIAL_MAPPING_FIELD].toByteArray(); - QJsonObject oldMaterialMapping = QJsonDocument::fromJson(materialMapValue).object(); - for (auto key : oldMaterialMapping.keys()) { + auto materialResource = baker->getNetworkMaterialResource(); + if (materialResource) { + for (auto materialName : materialResource->parsedMaterials.names) { QJsonObject json; - json[key] = oldMaterialMapping[key]; - materialMapping.push_back(json); + json[QString("mat::" + QString(materialName.c_str()))] = relativeBakedMaterialURL + "#" + materialName.c_str(); + _materialMappingJSON.push_back(json); } } - - _mapping[MATERIAL_MAPPING_FIELD] = QJsonDocument(materialMapping).toJson(QJsonDocument::Compact); } else { // this material failed to bake - this doesn't fail the entire bake but we need to add the errors from // the material to our warnings @@ -314,7 +300,62 @@ void ModelBaker::handleFinishedMaterialBaker() { handleWarning("Failed to bake the materials for model with URL " + _modelURL.toString()); } - outputBakedFST(); + bakeMaterialMap(); +} + +void ModelBaker::bakeMaterialMap() { + if (!_materialMapping.empty()) { + // TODO: The existing material map must be baked in order, so we do it all on this thread to preserve the order. + // It could be spread over multiple threads if we had a good way of preserving the order once all of the bakers are done + _materialBaker = QSharedPointer( + new MaterialBaker("materialMap" + QString::number(_materialMapIndex++), true, _bakedOutputDir), + &MaterialBaker::deleteLater + ); + _materialBaker->setMaterials(_materialMapping.front().second); + connect(_materialBaker.data(), &MaterialBaker::finished, this, &ModelBaker::handleFinishedMaterialMapBaker); + _materialBaker->bake(); + } else { + outputBakedFST(); + } +} + +void ModelBaker::handleFinishedMaterialMapBaker() { + auto baker = qobject_cast(sender()); + + if (baker) { + if (!baker->hasErrors()) { + // this MaterialBaker is done and everything went according to plan + qCDebug(model_baking) << "Adding baked material to FST mapping " << baker->getBakedMaterialData(); + + QString materialName; + { + auto materialResource = baker->getNetworkMaterialResource(); + if (materialResource) { + auto url = materialResource->getURL(); + if (!url.isEmpty()) { + QString urlString = url.toDisplayString(); + auto index = urlString.lastIndexOf("#"); + if (index != -1) { + materialName = urlString.right(urlString.length() - index); + } + } + } + } + + QJsonObject json; + json[QString(_materialMapping.front().first.c_str())] = baker->getMaterialData() + BAKED_MATERIAL_EXTENSION + materialName; + _materialMappingJSON.push_back(json); + } else { + // this material failed to bake - this doesn't fail the entire bake but we need to add the errors from + // the material to our warnings + _warningList << baker->getWarnings(); + } + } else { + handleWarning("Failed to bake the materialMap for model with URL " + _modelURL.toString() + " and mapping target " + _materialMapping.front().first.c_str()); + } + + _materialMapping.erase(_materialMapping.begin()); + bakeMaterialMap(); } void ModelBaker::outputUnbakedFST() { @@ -363,6 +404,9 @@ void ModelBaker::outputBakedFST() { outputMapping[FILENAME_FIELD] = _bakedModelURL.fileName(); outputMapping.remove(TEXDIR_FIELD); outputMapping.remove(COMMENT_FIELD); + if (!_materialMappingJSON.isEmpty()) { + outputMapping[MATERIAL_MAPPING_FIELD] = QJsonDocument(_materialMappingJSON).toJson(QJsonDocument::Compact); + } hifi::ByteArray fstOut = FSTReader::writeMapping(outputMapping); QFile fstOutputFile { outputFSTURL }; diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index f280481803..b98d9716e1 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -16,6 +16,7 @@ #include #include #include +#include #include "Baker.h" #include "MaterialBaker.h" @@ -80,14 +81,19 @@ protected slots: void handleModelNetworkReply(); virtual void bakeSourceCopy(); void handleFinishedMaterialBaker(); + void handleFinishedMaterialMapBaker(); private: void outputUnbakedFST(); void outputBakedFST(); + void bakeMaterialMap(); bool _hasBeenBaked { false }; hfm::Model::Pointer _hfmModel; + MaterialMapping _materialMapping; + int _materialMapIndex { 0 }; + QJsonArray _materialMappingJSON; QSharedPointer _materialBaker; }; diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp index 47a213cf71..fcd695bed8 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp @@ -25,12 +25,4 @@ void NullDisplayPlugin::submitFrame(const gpu::FramePointer& frame) { if (frame) { _gpuContext->consumeFrameUpdates(frame); } -} - -QImage NullDisplayPlugin::getScreenshot(float aspectRatio) const { - return QImage(); -} - -QImage NullDisplayPlugin::getSecondaryCameraScreenshot() const { - return QImage(); -} +} \ No newline at end of file diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h index e4ff1b8b37..7aa763bab9 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h @@ -17,8 +17,6 @@ public: glm::uvec2 getRecommendedRenderSize() const override; void submitFrame(const gpu::FramePointer& newFrame) override; - QImage getScreenshot(float aspectRatio = 0.0f) const override; - QImage getSecondaryCameraScreenshot() const override; void copyTextureToQuickFramebuffer(NetworkTexturePointer source, QOpenGLFramebufferObject* target, GLsync* fenceSync) override {}; void pluginUpdate() override {}; private: diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index a91a952bb4..4c3de004bd 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -721,6 +721,19 @@ void OpenGLDisplayPlugin::present() { compositeLayers(); } + { // If we have any snapshots this frame, handle them + PROFILE_RANGE_EX(render, "snapshotOperators", 0xffff00ff, frameId) + while (!_currentFrame->snapshotOperators.empty()) { + auto& snapshotOperator = _currentFrame->snapshotOperators.front(); + if (std::get<2>(snapshotOperator)) { + std::get<0>(snapshotOperator)(getScreenshot(std::get<1>(snapshotOperator))); + } else { + std::get<0>(snapshotOperator)(getSecondaryCameraScreenshot()); + } + _currentFrame->snapshotOperators.pop(); + } + } + // Take the composite framebuffer and send it to the output device { PROFILE_RANGE_EX(render, "internalPresent", 0xff00ffff, frameId) @@ -785,7 +798,7 @@ bool OpenGLDisplayPlugin::setDisplayTexture(const QString& name) { return !!_displayTexture; } -QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) const { +QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) { auto size = _compositeFramebuffer->getSize(); if (isHmd()) { size.x /= 2; @@ -801,24 +814,18 @@ QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) const { corner.x = round((size.x - bestSize.x) / 2.0f); corner.y = round((size.y - bestSize.y) / 2.0f); } - auto glBackend = const_cast(*this).getGLBackend(); QImage screenshot(bestSize.x, bestSize.y, QImage::Format_ARGB32); - withOtherThreadContext([&] { - glBackend->downloadFramebuffer(_compositeFramebuffer, ivec4(corner, bestSize), screenshot); - }); + getGLBackend()->downloadFramebuffer(_compositeFramebuffer, ivec4(corner, bestSize), screenshot); return screenshot.mirrored(false, true); } -QImage OpenGLDisplayPlugin::getSecondaryCameraScreenshot() const { +QImage OpenGLDisplayPlugin::getSecondaryCameraScreenshot() { auto textureCache = DependencyManager::get(); auto secondaryCameraFramebuffer = textureCache->getSpectatorCameraFramebuffer(); gpu::Vec4i region(0, 0, secondaryCameraFramebuffer->getWidth(), secondaryCameraFramebuffer->getHeight()); - auto glBackend = const_cast(*this).getGLBackend(); QImage screenshot(region.z, region.w, QImage::Format_ARGB32); - withOtherThreadContext([&] { - glBackend->downloadFramebuffer(secondaryCameraFramebuffer, region, screenshot); - }); + getGLBackend()->downloadFramebuffer(secondaryCameraFramebuffer, region, screenshot); return screenshot.mirrored(false, true); } diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 49a38ecb4c..e56804c151 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -60,8 +60,6 @@ public: virtual bool setDisplayTexture(const QString& name) override; virtual bool onDisplayTextureReset() { return false; }; - QImage getScreenshot(float aspectRatio = 0.0f) const override; - QImage getSecondaryCameraScreenshot() const override; float presentRate() const override; @@ -185,5 +183,8 @@ protected: // be serialized through this mutex mutable Mutex _presentMutex; float _hudAlpha{ 1.0f }; + + QImage getScreenshot(float aspectRatio); + QImage getSecondaryCameraScreenshot(); }; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 321bcc3fd2..874454b391 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -199,7 +199,7 @@ void HmdDisplayPlugin::internalPresent() { float newWidth = sourceSize.x - shiftLeftBy; // Experimentally adjusted the region presented in preview to avoid seeing the masked pixels and recenter the center... - static float SCALE_WIDTH = 0.9f; + static float SCALE_WIDTH = 0.8f; static float SCALE_OFFSET = 2.0f; newWidth *= SCALE_WIDTH; shiftLeftBy *= SCALE_OFFSET; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index d8c0ce8e1d..e952b1e8db 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -48,6 +48,8 @@ public: void pluginUpdate() override {}; + virtual StencilMaskMode getStencilMaskMode() const override { return StencilMaskMode::PAINT; } + signals: void hmdMountedChanged(); void hmdVisibleChanged(bool visible); diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp index 01d1098daa..9a634a85ad 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp @@ -313,11 +313,9 @@ void MaterialEntityRenderer::doRender(RenderArgs* args) { batch.setModelTransform(renderTransform); - if (args->_renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { - drawMaterial->setTextureTransforms(textureTransform, MaterialMappingMode::UV, true); - - // bind the material - RenderPipelines::bindMaterial(drawMaterial, batch, args->_enableTexturing); + drawMaterial->setTextureTransforms(textureTransform, MaterialMappingMode::UV, true); + // bind the material + if (RenderPipelines::bindMaterial(drawMaterial, batch, args->_renderMode, args->_enableTexturing)) { args->_details._materialSwitches++; } diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index d859d4b739..2548ae5914 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -291,8 +291,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline); } } else { - if (args->_renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { - RenderPipelines::bindMaterials(materials, batch, args->_enableTexturing); + if (RenderPipelines::bindMaterials(materials, batch, args->_renderMode, args->_enableTexturing)) { args->_details._materialSwitches++; } diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index a532064b6c..e1ede9192a 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -791,7 +791,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef bool otherOverwrites = overwriteLocalData && !weOwnSimulation; // calculate hasGrab once outside the lambda rather than calling it every time inside bool hasGrab = stillHasGrabAction(); - auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection, hasGrab](quint64 updatedTimestamp, bool valueChanged) { + auto shouldUpdate = [lastEdited, otherOverwrites, filterRejection, hasGrab](quint64 updatedTimestamp, bool valueChanged) { if (hasGrab) { return false; } diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index a4d18fb111..0264accdc3 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -909,7 +909,6 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) { auto& node = _file.nodes[nodeIndex]; if (node.defined["mesh"]) { - qCDebug(modelformat) << "node_transforms" << node.transforms; foreach(auto &primitive, _file.meshes[node.mesh].primitives) { hfmModel.meshes.append(HFMMesh()); HFMMesh& mesh = hfmModel.meshes[hfmModel.meshes.size() - 1]; @@ -1276,7 +1275,6 @@ HFMTexture GLTFSerializer::getHFMTexture(const GLTFTexture& texture) { QString fname = hifi::URL(url).fileName(); hifi::URL textureUrl = _url.resolved(url); - qCDebug(modelformat) << "fname: " << fname; fbxtex.name = fname; fbxtex.filename = textureUrl.toEncoded(); @@ -1370,10 +1368,7 @@ bool GLTFSerializer::readArray(const hifi::ByteArray& bin, int byteOffset, int c blobstream.setByteOrder(QDataStream::LittleEndian); blobstream.setVersion(QDataStream::Qt_5_9); blobstream.setFloatingPointPrecision(QDataStream::FloatingPointPrecision::SinglePrecision); - - qCDebug(modelformat) << "size1: " << count; - int dataskipped = blobstream.skipRawData(byteOffset); - qCDebug(modelformat) << "dataskipped: " << dataskipped; + blobstream.skipRawData(byteOffset); int bufferCount = 0; switch (accessorType) { @@ -1467,6 +1462,38 @@ void GLTFSerializer::retriangulate(const QVector& inIndices, const QVector< } } +void GLTFSerializer::glTFDebugDump() { + qCDebug(modelformat) << "---------------- Nodes ----------------"; + for (GLTFNode node : _file.nodes) { + if (node.defined["mesh"]) { + qCDebug(modelformat) << "\n"; + qCDebug(modelformat) << " node_transforms" << node.transforms; + qCDebug(modelformat) << "\n"; + } + } + + qCDebug(modelformat) << "---------------- Accessors ----------------"; + for (GLTFAccessor accessor : _file.accessors) { + qCDebug(modelformat) << "\n"; + qCDebug(modelformat) << "count: " << accessor.count; + qCDebug(modelformat) << "byteOffset: " << accessor.byteOffset; + qCDebug(modelformat) << "\n"; + } + + qCDebug(modelformat) << "---------------- Textures ----------------"; + for (GLTFTexture texture : _file.textures) { + if (texture.defined["source"]) { + qCDebug(modelformat) << "\n"; + QString url = _file.images[texture.source].uri; + QString fname = hifi::URL(url).fileName(); + qCDebug(modelformat) << "fname: " << fname; + qCDebug(modelformat) << "\n"; + } + } + + qCDebug(modelformat) << "\n"; +} + void GLTFSerializer::hfmDebugDump(const HFMModel& hfmModel) { qCDebug(modelformat) << "---------------- hfmModel ----------------"; qCDebug(modelformat) << " hasSkeletonJoints =" << hfmModel.hasSkeletonJoints; @@ -1607,5 +1634,8 @@ void GLTFSerializer::hfmDebugDump(const HFMModel& hfmModel) { qCDebug(modelformat) << "\n"; } + qCDebug(modelformat) << "---------------- GLTF Model ----------------"; + glTFDebugDump(); + qCDebug(modelformat) << "\n"; } diff --git a/libraries/fbx/src/GLTFSerializer.h b/libraries/fbx/src/GLTFSerializer.h index a9e7152e5b..8784667d1e 100755 --- a/libraries/fbx/src/GLTFSerializer.h +++ b/libraries/fbx/src/GLTFSerializer.h @@ -784,6 +784,7 @@ private: void setHFMMaterial(HFMMaterial& fbxmat, const GLTFMaterial& material); HFMTexture getHFMTexture(const GLTFTexture& texture); + void glTFDebugDump(); void hfmDebugDump(const HFMModel& hfmModel); }; diff --git a/libraries/gpu/src/gpu/Frame.h b/libraries/gpu/src/gpu/Frame.h index 3787ebfacd..9aa99e50f7 100644 --- a/libraries/gpu/src/gpu/Frame.h +++ b/libraries/gpu/src/gpu/Frame.h @@ -9,6 +9,7 @@ #define hifi_gpu_Frame_h #include +#include #include "Forward.h" #include "Batch.h" @@ -41,6 +42,8 @@ namespace gpu { /// How to process the framebuffer when the frame dies. MUST BE THREAD SAFE FramebufferRecycler framebufferRecycler; + std::queue, float, bool>> snapshotOperators; + protected: friend class Deserializer; diff --git a/libraries/graphics-scripting/src/graphics-scripting/Forward.h b/libraries/graphics-scripting/src/graphics-scripting/Forward.h index d2d330167d..6d1b9d83d2 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/Forward.h +++ b/libraries/graphics-scripting/src/graphics-scripting/Forward.h @@ -59,8 +59,8 @@ namespace scriptable { * @property {string} occlusionMap * @property {string} lightmapMap * @property {string} scatteringMap - * @property {string} texCoordTransform0 - * @property {string} texCoordTransform1 + * @property {Mat4|string} texCoordTransform0 + * @property {Mat4|string} texCoordTransform1 * @property {string} lightmapParams * @property {string} materialParams * @property {boolean} defaultFallthrough @@ -93,6 +93,7 @@ namespace scriptable { QString occlusionMap; QString lightmapMap; QString scatteringMap; + std::array texCoordTransforms; bool defaultFallthrough; std::unordered_map propertyFallthroughs; // not actually exposed to script diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp index 3bd4af601c..eb4bfa197c 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp @@ -470,9 +470,13 @@ namespace scriptable { // These need to be implemented, but set the fallthrough for now if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM0)) { obj.setProperty("texCoordTransform0", FALLTHROUGH); + } else if (material.texCoordTransforms[0] != mat4()) { + obj.setProperty("texCoordTransform0", mat4toScriptValue(engine, material.texCoordTransforms[0])); } if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM1)) { obj.setProperty("texCoordTransform1", FALLTHROUGH); + } else if (material.texCoordTransforms[1] != mat4()) { + obj.setProperty("texCoordTransform1", mat4toScriptValue(engine, material.texCoordTransforms[1])); } if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::LIGHTMAP_PARAMS)) { obj.setProperty("lightmapParams", FALLTHROUGH); diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp index caee4ceb2a..8825a26bfe 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp @@ -119,6 +119,10 @@ scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPoint if (map && map->getTextureSource()) { scatteringMap = map->getTextureSource()->getUrl().toString(); } + + for (int i = 0; i < graphics::Material::NUM_TEXCOORD_TRANSFORMS; i++) { + texCoordTransforms[i] = material->getTexCoordTransform(i); + } } } diff --git a/libraries/graphics/src/graphics/Light.cpp b/libraries/graphics/src/graphics/Light.cpp index 76d8a6030a..8a7281880e 100755 --- a/libraries/graphics/src/graphics/Light.cpp +++ b/libraries/graphics/src/graphics/Light.cpp @@ -73,6 +73,22 @@ bool Light::getCastShadows() const { return _castShadows; } +void Light::setShadowsMaxDistance(const float maxDistance) { + _shadowsMaxDistance = std::max(0.0f, maxDistance); +} + +float Light::getShadowsMaxDistance() const { + return _shadowsMaxDistance; +} + +void Light::setShadowsBiasScale(const float scale) { + _shadowsBiasScale = std::max(0.0f, scale); +} + +float Light::getShadowsBiasScale() const { + return _shadowsBiasScale; +} + void Light::setColor(const Color& color) { _lightSchemaBuffer.edit().irradiance.color = color; updateLightRadius(); diff --git a/libraries/graphics/src/graphics/Light.h b/libraries/graphics/src/graphics/Light.h index bb9fb3e5b9..824a9138c0 100755 --- a/libraries/graphics/src/graphics/Light.h +++ b/libraries/graphics/src/graphics/Light.h @@ -106,6 +106,12 @@ public: void setCastShadows(const bool castShadows); bool getCastShadows() const; + void setShadowsMaxDistance(const float maxDistance); + float getShadowsMaxDistance() const; + + void setShadowsBiasScale(const float scale); + float getShadowsBiasScale() const; + void setOrientation(const Quat& orientation); const glm::quat& getOrientation() const { return _transform.getRotation(); } @@ -192,10 +198,11 @@ protected: Type _type { SUN }; float _spotCos { -1.0f }; // stored here to be able to reset the spot angle when turning the type spot on/off - void updateLightRadius(); - + float _shadowsMaxDistance{ 40.0f }; + float _shadowsBiasScale{ 1.0f }; bool _castShadows{ false }; + void updateLightRadius(); }; typedef std::shared_ptr< Light > LightPointer; diff --git a/libraries/graphics/src/graphics/Material.h b/libraries/graphics/src/graphics/Material.h index 80b247bed0..330feaa61c 100755 --- a/libraries/graphics/src/graphics/Material.h +++ b/libraries/graphics/src/graphics/Material.h @@ -324,6 +324,7 @@ public: void setModel(const std::string& model) { _model = model; } glm::mat4 getTexCoordTransform(uint i) const { return _texcoordTransforms[i]; } + void setTexCoordTransform(uint i, const glm::mat4& mat4) { _texcoordTransforms[i] = mat4; } glm::vec2 getLightmapParams() const { return _lightmapParams; } glm::vec2 getMaterialParams() const { return _materialParams; } diff --git a/libraries/graphics/src/graphics/MaterialTextures.slh b/libraries/graphics/src/graphics/MaterialTextures.slh index c725aae9bb..92e76e5736 100644 --- a/libraries/graphics/src/graphics/MaterialTextures.slh +++ b/libraries/graphics/src/graphics/MaterialTextures.slh @@ -149,7 +149,6 @@ float fetchScatteringMap(vec2 uv) { <@endfunc@> - <@func fetchMaterialTexturesCoord0(matKey, texcoord0, albedo, roughness, normal, metallic, emissive, scattering)@> if (getTexMapArray()._materialParams.y != 1.0 && clamp(<$texcoord0$>, vec2(0.0), vec2(1.0)) != <$texcoord0$>) { discard; diff --git a/libraries/material-networking/src/material-networking/MaterialCache.cpp b/libraries/material-networking/src/material-networking/MaterialCache.cpp index 5a5f4ab54b..745504fb3d 100644 --- a/libraries/material-networking/src/material-networking/MaterialCache.cpp +++ b/libraries/material-networking/src/material-networking/MaterialCache.cpp @@ -177,6 +177,8 @@ std::pair> NetworkMaterialResource material->setModel(modelString); } + std::array texcoordTransforms; + if (modelString == HIFI_PBR) { const QString FALLTHROUGH("fallthrough"); for (auto& key : materialJSON.keys()) { @@ -372,8 +374,11 @@ std::pair> NetworkMaterialResource if (valueString == FALLTHROUGH) { material->setPropertyDoesFallthrough(graphics::Material::ExtraFlagBit::TEXCOORDTRANSFORM0); } + } else if (value.isObject()) { + auto valueVariant = value.toVariant(); + glm::mat4 transform = mat4FromVariant(valueVariant); + texcoordTransforms[0] = transform; } - // TODO: implement texCoordTransform0 } else if (key == "texCoordTransform1") { auto value = materialJSON.value(key); if (value.isString()) { @@ -381,8 +386,11 @@ std::pair> NetworkMaterialResource if (valueString == FALLTHROUGH) { material->setPropertyDoesFallthrough(graphics::Material::ExtraFlagBit::TEXCOORDTRANSFORM1); } + } else if (value.isObject()) { + auto valueVariant = value.toVariant(); + glm::mat4 transform = mat4FromVariant(valueVariant); + texcoordTransforms[1] = transform; } - // TODO: implement texCoordTransform1 } else if (key == "lightmapParams") { auto value = materialJSON.value(key); if (value.isString()) { @@ -409,6 +417,15 @@ std::pair> NetworkMaterialResource } } } + + // Do this after the texture maps are defined, so it overrides the default transforms + for (int i = 0; i < graphics::Material::NUM_TEXCOORD_TRANSFORMS; i++) { + mat4 newTransform = texcoordTransforms[i]; + if (newTransform != mat4() || newTransform != material->getTexCoordTransform(i)) { + material->setTexCoordTransform(i, newTransform); + } + } + return std::pair>(name, material); } diff --git a/libraries/material-networking/src/material-networking/MaterialCache.h b/libraries/material-networking/src/material-networking/MaterialCache.h index 4894054de4..7ed0453187 100644 --- a/libraries/material-networking/src/material-networking/MaterialCache.h +++ b/libraries/material-networking/src/material-networking/MaterialCache.h @@ -108,6 +108,7 @@ private: using NetworkMaterialResourcePointer = QSharedPointer; using MaterialMapping = std::vector>; +Q_DECLARE_METATYPE(MaterialMapping) class MaterialCache : public ResourceCache { public: diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index 6ceb5d328a..25e7eeb25d 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -635,11 +635,9 @@ void NetworkTexture::makeLocalRequest() { } bool NetworkTexture::handleFailedRequest(ResourceRequest::Result result) { - if (_currentlyLoadingResourceType != ResourceType::KTX - && result == ResourceRequest::Result::RedirectFail) { - + if (_shouldFailOnRedirect && result == ResourceRequest::Result::RedirectFail) { auto newPath = _request->getRelativePathUrl(); - if (newPath.fileName().endsWith(".ktx")) { + if (newPath.fileName().toLower().endsWith(".ktx")) { _currentlyLoadingResourceType = ResourceType::KTX; _activeUrl = newPath; _shouldFailOnRedirect = false; diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp index 2e378965de..25a45cefe5 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -52,7 +52,7 @@ std::vector createMaterialList(const hfm::Mesh& mesh) { } std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::vector& normals, const std::vector& tangents, const std::vector& materialList) { - Q_ASSERT(normals.size() == 0 || normals.size() == mesh.vertices.size()); + Q_ASSERT(normals.size() == 0 || (int)normals.size() == mesh.vertices.size()); Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 23b365dd03..26bd20d967 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -31,8 +31,6 @@ Q_LOGGING_CATEGORY(trace_resource_parse_geometry, "trace.resource.parse.geometry") -class GeometryReader; - class GeometryExtra { public: const GeometryMappingPair& mapping; @@ -87,113 +85,6 @@ namespace std { }; } -QUrl resolveTextureBaseUrl(const QUrl& url, const QUrl& textureBaseUrl) { - return textureBaseUrl.isValid() ? textureBaseUrl : url; -} - -class GeometryMappingResource : public GeometryResource { - Q_OBJECT -public: - GeometryMappingResource(const QUrl& url) : GeometryResource(url) {}; - - QString getType() const override { return "GeometryMapping"; } - - virtual void downloadFinished(const QByteArray& data) override; - -private slots: - void onGeometryMappingLoaded(bool success); - -private: - GeometryResource::Pointer _geometryResource; - QMetaObject::Connection _connection; -}; - -void GeometryMappingResource::downloadFinished(const QByteArray& data) { - PROFILE_ASYNC_BEGIN(resource_parse_geometry, "GeometryMappingResource::downloadFinished", _url.toString(), - { { "url", _url.toString() } }); - - // store parsed contents of FST file - _mapping = FSTReader::readMapping(data); - - QString filename = _mapping.value("filename").toString(); - - if (filename.isNull()) { - finishedLoading(false); - } else { - const QString baseURL = _mapping.value("baseURL").toString(); - const QUrl base = _effectiveBaseURL.resolved(baseURL); - QUrl url = base.resolved(filename); - - QString texdir = _mapping.value(TEXDIR_FIELD).toString(); - if (!texdir.isNull()) { - if (!texdir.endsWith('/')) { - texdir += '/'; - } - _textureBaseUrl = resolveTextureBaseUrl(url, base.resolved(texdir)); - } else { - _textureBaseUrl = url.resolved(QUrl(".")); - } - - auto scripts = FSTReader::getScripts(base, _mapping); - if (scripts.size() > 0) { - _mapping.remove(SCRIPT_FIELD); - for (auto &scriptPath : scripts) { - _mapping.insertMulti(SCRIPT_FIELD, scriptPath); - } - } - - auto animGraphVariant = _mapping.value("animGraphUrl"); - - if (animGraphVariant.isValid()) { - QUrl fstUrl(animGraphVariant.toString()); - if (fstUrl.isValid()) { - _animGraphOverrideUrl = base.resolved(fstUrl); - } else { - _animGraphOverrideUrl = QUrl(); - } - } else { - _animGraphOverrideUrl = QUrl(); - } - - auto modelCache = DependencyManager::get(); - GeometryExtra extra { GeometryMappingPair(base, _mapping), _textureBaseUrl, false }; - - // Get the raw GeometryResource - _geometryResource = modelCache->getResource(url, QUrl(), &extra, std::hash()(extra)).staticCast(); - // Avoid caching nested resources - their references will be held by the parent - _geometryResource->_isCacheable = false; - - if (_geometryResource->isLoaded()) { - onGeometryMappingLoaded(!_geometryResource->getURL().isEmpty()); - } else { - if (_connection) { - disconnect(_connection); - } - - _connection = connect(_geometryResource.data(), &Resource::finished, - this, &GeometryMappingResource::onGeometryMappingLoaded); - } - } -} - -void GeometryMappingResource::onGeometryMappingLoaded(bool success) { - if (success && _geometryResource) { - _hfmModel = _geometryResource->_hfmModel; - _materialMapping = _geometryResource->_materialMapping; - _meshParts = _geometryResource->_meshParts; - _meshes = _geometryResource->_meshes; - _materials = _geometryResource->_materials; - - // Avoid holding onto extra references - _geometryResource.reset(); - // Make sure connection will not trigger again - disconnect(_connection); // FIXME Should not have to do this - } - - PROFILE_ASYNC_END(resource_parse_geometry, "GeometryMappingResource::downloadFinished", _url.toString()); - finishedLoading(success); -} - class GeometryReader : public QRunnable { public: GeometryReader(const ModelLoader& modelLoader, QWeakPointer& resource, const QUrl& url, const GeometryMappingPair& mapping, @@ -282,8 +173,16 @@ void GeometryReader::run() { hfmModel->scripts.push_back(script.toString()); } } + + // Do processing on the model + baker::Baker modelBaker(hfmModel, _mapping.second, _mapping.first); + modelBaker.run(); + + auto processedHFMModel = modelBaker.getHFMModel(); + auto materialMapping = modelBaker.getMaterialMapping(); + QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition", - Q_ARG(HFMModel::Pointer, hfmModel), Q_ARG(GeometryMappingPair, _mapping)); + Q_ARG(HFMModel::Pointer, processedHFMModel), Q_ARG(MaterialMapping, materialMapping)); } catch (const std::exception&) { auto resource = _resource.toStrongRef(); if (resource) { @@ -300,60 +199,133 @@ void GeometryReader::run() { } } -class GeometryDefinitionResource : public GeometryResource { - Q_OBJECT -public: - GeometryDefinitionResource(const ModelLoader& modelLoader, const QUrl& url) : GeometryResource(url), _modelLoader(modelLoader) {} - GeometryDefinitionResource(const GeometryDefinitionResource& other) : - GeometryResource(other), - _modelLoader(other._modelLoader), - _mapping(other._mapping), - _combineParts(other._combineParts) {} +QUrl resolveTextureBaseUrl(const QUrl& url, const QUrl& textureBaseUrl) { + return textureBaseUrl.isValid() ? textureBaseUrl : url; +} - QString getType() const override { return "GeometryDefinition"; } +GeometryResource::GeometryResource(const GeometryResource& other) : + Resource(other), + Geometry(other), + _modelLoader(other._modelLoader), + _mappingPair(other._mappingPair), + _textureBaseURL(other._textureBaseURL), + _combineParts(other._combineParts), + _isCacheable(other._isCacheable) +{ + if (other._geometryResource) { + _startedLoading = false; + } +} - virtual void downloadFinished(const QByteArray& data) override; +void GeometryResource::downloadFinished(const QByteArray& data) { + if (_effectiveBaseURL.fileName().toLower().endsWith(".fst")) { + PROFILE_ASYNC_BEGIN(resource_parse_geometry, "GeometryResource::downloadFinished", _url.toString(), { { "url", _url.toString() } }); - void setExtra(void* extra) override; + // store parsed contents of FST file + _mapping = FSTReader::readMapping(data); -protected: - Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, const GeometryMappingPair& mapping); + QString filename = _mapping.value("filename").toString(); -private: - ModelLoader _modelLoader; - GeometryMappingPair _mapping; - bool _combineParts; -}; + if (filename.isNull()) { + finishedLoading(false); + } else { + const QString baseURL = _mapping.value("baseURL").toString(); + const QUrl base = _effectiveBaseURL.resolved(baseURL); + QUrl url = base.resolved(filename); -void GeometryDefinitionResource::setExtra(void* extra) { + QString texdir = _mapping.value(TEXDIR_FIELD).toString(); + if (!texdir.isNull()) { + if (!texdir.endsWith('/')) { + texdir += '/'; + } + _textureBaseURL = resolveTextureBaseUrl(url, base.resolved(texdir)); + } else { + _textureBaseURL = url.resolved(QUrl(".")); + } + + auto scripts = FSTReader::getScripts(base, _mapping); + if (scripts.size() > 0) { + _mapping.remove(SCRIPT_FIELD); + for (auto &scriptPath : scripts) { + _mapping.insertMulti(SCRIPT_FIELD, scriptPath); + } + } + + auto animGraphVariant = _mapping.value("animGraphUrl"); + + if (animGraphVariant.isValid()) { + QUrl fstUrl(animGraphVariant.toString()); + if (fstUrl.isValid()) { + _animGraphOverrideUrl = base.resolved(fstUrl); + } else { + _animGraphOverrideUrl = QUrl(); + } + } else { + _animGraphOverrideUrl = QUrl(); + } + + auto modelCache = DependencyManager::get(); + GeometryExtra extra { GeometryMappingPair(base, _mapping), _textureBaseURL, false }; + + // Get the raw GeometryResource + _geometryResource = modelCache->getResource(url, QUrl(), &extra, std::hash()(extra)).staticCast(); + // Avoid caching nested resources - their references will be held by the parent + _geometryResource->_isCacheable = false; + + if (_geometryResource->isLoaded()) { + onGeometryMappingLoaded(!_geometryResource->getURL().isEmpty()); + } else { + if (_connection) { + disconnect(_connection); + } + + _connection = connect(_geometryResource.data(), &Resource::finished, this, &GeometryResource::onGeometryMappingLoaded); + } + } + } else { + if (_url != _effectiveBaseURL) { + _url = _effectiveBaseURL; + _textureBaseURL = _effectiveBaseURL; + } + QThreadPool::globalInstance()->start(new GeometryReader(_modelLoader, _self, _effectiveBaseURL, _mappingPair, data, _combineParts, _request->getWebMediaType())); + } +} + +void GeometryResource::onGeometryMappingLoaded(bool success) { + if (success && _geometryResource) { + _hfmModel = _geometryResource->_hfmModel; + _materialMapping = _geometryResource->_materialMapping; + _meshParts = _geometryResource->_meshParts; + _meshes = _geometryResource->_meshes; + _materials = _geometryResource->_materials; + + // Avoid holding onto extra references + _geometryResource.reset(); + // Make sure connection will not trigger again + disconnect(_connection); // FIXME Should not have to do this + } + + PROFILE_ASYNC_END(resource_parse_geometry, "GeometryResource::downloadFinished", _url.toString()); + finishedLoading(success); +} + +void GeometryResource::setExtra(void* extra) { const GeometryExtra* geometryExtra = static_cast(extra); - _mapping = geometryExtra ? geometryExtra->mapping : GeometryMappingPair(QUrl(), QVariantHash()); - _textureBaseUrl = geometryExtra ? resolveTextureBaseUrl(_url, geometryExtra->textureBaseUrl) : QUrl(); + _mappingPair = geometryExtra ? geometryExtra->mapping : GeometryMappingPair(QUrl(), QVariantHash()); + _textureBaseURL = geometryExtra ? resolveTextureBaseUrl(_url, geometryExtra->textureBaseUrl) : QUrl(); _combineParts = geometryExtra ? geometryExtra->combineParts : true; } -void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { - if (_url != _effectiveBaseURL) { - _url = _effectiveBaseURL; - _textureBaseUrl = _effectiveBaseURL; - } - QThreadPool::globalInstance()->start(new GeometryReader(_modelLoader, _self, _effectiveBaseURL, _mapping, data, _combineParts, _request->getWebMediaType())); -} - -void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel, const GeometryMappingPair& mapping) { - // Do processing on the model - baker::Baker modelBaker(hfmModel, mapping.second, mapping.first); - modelBaker.run(); - +void GeometryResource::setGeometryDefinition(HFMModel::Pointer hfmModel, const MaterialMapping& materialMapping) { // Assume ownership of the processed HFMModel - _hfmModel = modelBaker.getHFMModel(); - _materialMapping = modelBaker.getMaterialMapping(); + _hfmModel = hfmModel; + _materialMapping = materialMapping; // Copy materials QHash materialIDAtlas; for (const HFMMaterial& material : _hfmModel->materials) { materialIDAtlas[material.materialID] = _materials.size(); - _materials.push_back(std::make_shared(material, _textureBaseUrl)); + _materials.push_back(std::make_shared(material, _textureBaseURL)); } std::shared_ptr meshes = std::make_shared(); @@ -376,6 +348,23 @@ void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmMode finishedLoading(true); } +void GeometryResource::deleter() { + resetTextures(); + Resource::deleter(); +} + +void GeometryResource::setTextures() { + if (_hfmModel) { + for (const HFMMaterial& material : _hfmModel->materials) { + _materials.push_back(std::make_shared(material, _textureBaseURL)); + } + } +} + +void GeometryResource::resetTextures() { + _materials.clear(); +} + ModelCache::ModelCache() { const qint64 GEOMETRY_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE; setUnusedResourceCacheSize(GEOMETRY_DEFAULT_UNUSED_MAX_SIZE); @@ -388,26 +377,14 @@ ModelCache::ModelCache() { } QSharedPointer ModelCache::createResource(const QUrl& url) { - Resource* resource = nullptr; - if (url.path().toLower().endsWith(".fst")) { - resource = new GeometryMappingResource(url); - } else { - resource = new GeometryDefinitionResource(_modelLoader, url); - } - - return QSharedPointer(resource, &Resource::deleter); + return QSharedPointer(new GeometryResource(url, _modelLoader), &GeometryResource::deleter); } QSharedPointer ModelCache::createResourceCopy(const QSharedPointer& resource) { - if (resource->getURL().path().toLower().endsWith(".fst")) { - return QSharedPointer(new GeometryMappingResource(*resource.staticCast()), &Resource::deleter); - } else { - return QSharedPointer(new GeometryDefinitionResource(*resource.staticCast()), &Resource::deleter); - } + return QSharedPointer(new GeometryResource(*resource.staticCast()), &GeometryResource::deleter); } -GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url, - const GeometryMappingPair& mapping, const QUrl& textureBaseUrl) { +GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url, const GeometryMappingPair& mapping, const QUrl& textureBaseUrl) { bool combineParts = true; GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts }; GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra, std::hash()(geometryExtra)).staticCast(); @@ -498,6 +475,20 @@ bool Geometry::areTexturesLoaded() const { material->checkResetOpacityMap(); } + for (auto& materialMapping : _materialMapping) { + if (materialMapping.second) { + for (auto& materialPair : materialMapping.second->parsedMaterials.networkMaterials) { + if (materialPair.second) { + if (materialPair.second->isMissingTexture()) { + return false; + } + + materialPair.second->checkResetOpacityMap(); + } + } + } + } + _areTexturesLoaded = true; } return true; @@ -513,23 +504,6 @@ const std::shared_ptr Geometry::getShapeMaterial(int partID) co return nullptr; } -void GeometryResource::deleter() { - resetTextures(); - Resource::deleter(); -} - -void GeometryResource::setTextures() { - if (_hfmModel) { - for (const HFMMaterial& material : _hfmModel->materials) { - _materials.push_back(std::make_shared(material, _textureBaseUrl)); - } - } -} - -void GeometryResource::resetTextures() { - _materials.clear(); -} - void GeometryResourceWatcher::startWatching() { connect(_resource.data(), &Resource::finished, this, &GeometryResourceWatcher::resourceFinished); connect(_resource.data(), &Resource::onRefresh, this, &GeometryResourceWatcher::resourceRefreshed); diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index ca1ceaff16..f9ae2dccd6 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -24,8 +24,6 @@ class MeshPart; -class GeometryMappingResource; - using GeometryMappingPair = std::pair; Q_DECLARE_METATYPE(GeometryMappingPair) @@ -60,8 +58,6 @@ public: const QVariantHash& getMapping() const { return _mapping; } protected: - friend class GeometryMappingResource; - // Shared across all geometries, constant throughout lifetime std::shared_ptr _hfmModel; MaterialMapping _materialMapping; @@ -80,23 +76,29 @@ private: /// A geometry loaded from the network. class GeometryResource : public Resource, public Geometry { + Q_OBJECT public: using Pointer = QSharedPointer; - GeometryResource(const QUrl& url) : Resource(url) {} - GeometryResource(const GeometryResource& other) : - Resource(other), - Geometry(other), - _textureBaseUrl(other._textureBaseUrl), - _isCacheable(other._isCacheable) {} + GeometryResource(const QUrl& url, const ModelLoader& modelLoader) : Resource(url), _modelLoader(modelLoader) {} + GeometryResource(const GeometryResource& other); - virtual bool areTexturesLoaded() const override { return isLoaded() && Geometry::areTexturesLoaded(); } + QString getType() const override { return "Geometry"; } virtual void deleter() override; + virtual void downloadFinished(const QByteArray& data) override; + void setExtra(void* extra) override; + + virtual bool areTexturesLoaded() const override { return isLoaded() && Geometry::areTexturesLoaded(); } + +private slots: + void onGeometryMappingLoaded(bool success); + protected: friend class ModelCache; - friend class GeometryMappingResource; + + Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, const MaterialMapping& materialMapping); // Geometries may not hold onto textures while cached - that is for the texture cache // Instead, these methods clear and reset textures from the geometry when caching/loading @@ -104,10 +106,18 @@ protected: void setTextures(); void resetTextures(); - QUrl _textureBaseUrl; - virtual bool isCacheable() const override { return _loaded && _isCacheable; } - bool _isCacheable { true }; + +private: + ModelLoader _modelLoader; + GeometryMappingPair _mappingPair; + QUrl _textureBaseURL; + bool _combineParts; + + GeometryResource::Pointer _geometryResource; + QMetaObject::Connection _connection; + + bool _isCacheable{ true }; }; class GeometryResourceWatcher : public QObject { @@ -158,7 +168,7 @@ public: const QUrl& textureBaseUrl = QUrl()); protected: - friend class GeometryMappingResource; + friend class GeometryResource; virtual QSharedPointer createResource(const QUrl& url) override; QSharedPointer createResourceCopy(const QSharedPointer& resource) override; diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index d5abb27a27..44d3d1ee4d 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -576,6 +576,7 @@ Resource::Resource(const Resource& other) : Resource::Resource(const QUrl& url) : _url(url), + _effectiveBaseURL(url), _activeUrl(url), _requestID(++requestID) { init(); diff --git a/libraries/oculusMobile/src/ovr/VrHandler.cpp b/libraries/oculusMobile/src/ovr/VrHandler.cpp index a7b0f9f8ee..aff1f256b5 100644 --- a/libraries/oculusMobile/src/ovr/VrHandler.cpp +++ b/libraries/oculusMobile/src/ovr/VrHandler.cpp @@ -28,7 +28,7 @@ static AAssetManager* ASSET_MANAGER = nullptr; -#define USE_BLIT_PRESENT 0 +#define USE_BLIT_PRESENT 1 #if !USE_BLIT_PRESENT diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index aa52e57c3f..c88d76e661 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -27,6 +27,7 @@ #include #include #include "Plugin.h" +#include "StencilMaskMode.h" class QOpenGLFramebufferObject; @@ -172,10 +173,6 @@ public: return QRect(0, 0, recommendedSize.x, recommendedSize.y); } - // Fetch the most recently displayed image as a QImage - virtual QImage getScreenshot(float aspectRatio = 0.0f) const = 0; - virtual QImage getSecondaryCameraScreenshot() const = 0; - // will query the underlying hmd api to compute the most recent head pose virtual bool beginFrameRender(uint32_t frameIndex) { return true; } @@ -221,6 +218,10 @@ public: // for updating plugin-related commands. Mimics the input plugin. virtual void pluginUpdate() = 0; + virtual StencilMaskMode getStencilMaskMode() const { return StencilMaskMode::NONE; } + using StencilMaskMeshOperator = std::function; + virtual StencilMaskMeshOperator getStencilMaskMeshOperator() { return nullptr; } + signals: void recommendedFramebufferSizeChanged(const QSize& size); void resetSensorsRequested(); diff --git a/libraries/render-utils/src/LightStage.cpp b/libraries/render-utils/src/LightStage.cpp index 6913949286..524deaaad2 100644 --- a/libraries/render-utils/src/LightStage.cpp +++ b/libraries/render-utils/src/LightStage.cpp @@ -120,7 +120,7 @@ float LightStage::Shadow::Cascade::computeFarDistance(const ViewFrustum& viewFru return far; } -LightStage::Shadow::Shadow(graphics::LightPointer light, float maxDistance, unsigned int cascadeCount) : +LightStage::Shadow::Shadow(graphics::LightPointer light, unsigned int cascadeCount) : _light{ light } { cascadeCount = std::min(cascadeCount, (unsigned int)SHADOW_CASCADE_MAX_COUNT); Schema schema; @@ -149,70 +149,79 @@ LightStage::Shadow::Shadow(graphics::LightPointer light, float maxDistance, unsi cascade.framebuffer->setDepthBuffer(map, depthFormat, cascadeIndex); } - setMaxDistance(maxDistance); + if (light) { + setMaxDistance(light->getShadowsMaxDistance()); + } } void LightStage::Shadow::setLight(graphics::LightPointer light) { _light = light; + if (light) { + setMaxDistance(light->getShadowsMaxDistance()); + } } - void LightStage::Shadow::setMaxDistance(float value) { - // This overlaping factor isn't really used directly for blending of shadow cascades. It - // just there to be sure the cascades do overlap. The blending width used is relative - // to the UV space and is set in the Schema with invCascadeBlendWidth. - static const auto OVERLAP_FACTOR = 1.0f / 5.0f; + static const auto MINIMUM_MAXDISTANCE = 1e-3f; - _maxDistance = std::max(0.0f, value); + value = std::max(MINIMUM_MAXDISTANCE, value); + if (value != _maxDistance) { + // This overlaping factor isn't really used directly for blending of shadow cascades. It's + // just there to be sure the cascades do overlap. The blending width used is relative + // to the UV space and is set in the Schema with invCascadeBlendWidth. + static const auto OVERLAP_FACTOR = 1.0f / 5.0f; - if (_cascades.size() == 1) { - _cascades.front().setMinDistance(0.0f); - _cascades.front().setMaxDistance(_maxDistance); - } else { - // Distribute the cascades along that distance - // TODO : these parameters should be exposed to the user as part of the light entity parameters, no? - static const auto LOW_MAX_DISTANCE = 2.0f; - static const auto MAX_RESOLUTION_LOSS = 0.6f; // Between 0 and 1, 0 giving tighter distributions + _maxDistance = value; - // The max cascade distance is computed by multiplying the previous cascade's max distance by a certain - // factor. There is a "user" factor that is computed from a desired max resolution loss in the shadow - // and an optimal one based on the global min and max shadow distance, all cascades considered. The final - // distance is a gradual blend between the two - const auto userDistanceScale = 1.0f / (1.0f - MAX_RESOLUTION_LOSS); - const auto optimalDistanceScale = powf(_maxDistance / LOW_MAX_DISTANCE, 1.0f / (_cascades.size() - 1)); + if (_cascades.size() == 1) { + _cascades.front().setMinDistance(0.0f); + _cascades.front().setMaxDistance(_maxDistance); + } else { + // Distribute the cascades along that distance + // TODO : these parameters should be exposed to the user as part of the light entity parameters, no? + static const auto LOW_MAX_DISTANCE = 2.0f; + static const auto MAX_RESOLUTION_LOSS = 0.6f; // Between 0 and 1, 0 giving tighter distributions - float maxCascadeUserDistance = LOW_MAX_DISTANCE; - float maxCascadeOptimalDistance = LOW_MAX_DISTANCE; - float minCascadeDistance = 0.0f; + // The max cascade distance is computed by multiplying the previous cascade's max distance by a certain + // factor. There is a "user" factor that is computed from a desired max resolution loss in the shadow + // and an optimal one based on the global min and max shadow distance, all cascades considered. The final + // distance is a gradual blend between the two + const auto userDistanceScale = 1.0f / (1.0f - MAX_RESOLUTION_LOSS); + const auto optimalDistanceScale = powf(_maxDistance / LOW_MAX_DISTANCE, 1.0f / (_cascades.size() - 1)); - for (size_t cascadeIndex = 0; cascadeIndex < _cascades.size(); ++cascadeIndex) { - float blendFactor = cascadeIndex / float(_cascades.size() - 1); - float maxCascadeDistance; + float maxCascadeUserDistance = LOW_MAX_DISTANCE; + float maxCascadeOptimalDistance = LOW_MAX_DISTANCE; + float minCascadeDistance = 0.0f; - if (cascadeIndex == size_t(_cascades.size() - 1)) { - maxCascadeDistance = _maxDistance; - } else { - maxCascadeDistance = maxCascadeUserDistance + (maxCascadeOptimalDistance - maxCascadeUserDistance)*blendFactor*blendFactor; + for (size_t cascadeIndex = 0; cascadeIndex < _cascades.size(); ++cascadeIndex) { + float blendFactor = cascadeIndex / float(_cascades.size() - 1); + float maxCascadeDistance; + + if (cascadeIndex == size_t(_cascades.size() - 1)) { + maxCascadeDistance = _maxDistance; + } else { + maxCascadeDistance = maxCascadeUserDistance + (maxCascadeOptimalDistance - maxCascadeUserDistance)*blendFactor*blendFactor; + } + + float shadowOverlapDistance = maxCascadeDistance * OVERLAP_FACTOR; + + _cascades[cascadeIndex].setMinDistance(minCascadeDistance); + _cascades[cascadeIndex].setMaxDistance(maxCascadeDistance + shadowOverlapDistance); + + // Compute distances for next cascade + minCascadeDistance = maxCascadeDistance; + maxCascadeUserDistance = maxCascadeUserDistance * userDistanceScale; + maxCascadeOptimalDistance = maxCascadeOptimalDistance * optimalDistanceScale; + maxCascadeUserDistance = std::min(maxCascadeUserDistance, _maxDistance); } - - float shadowOverlapDistance = maxCascadeDistance * OVERLAP_FACTOR; - - _cascades[cascadeIndex].setMinDistance(minCascadeDistance); - _cascades[cascadeIndex].setMaxDistance(maxCascadeDistance + shadowOverlapDistance); - - // Compute distances for next cascade - minCascadeDistance = maxCascadeDistance; - maxCascadeUserDistance = maxCascadeUserDistance * userDistanceScale; - maxCascadeOptimalDistance = maxCascadeOptimalDistance * optimalDistanceScale; - maxCascadeUserDistance = std::min(maxCascadeUserDistance, _maxDistance); } - } - // Update the buffer - const auto& lastCascade = _cascades.back(); - auto& schema = _schemaBuffer.edit(); - schema.maxDistance = _maxDistance; - schema.invFalloffDistance = 1.0f / (OVERLAP_FACTOR*lastCascade.getMaxDistance()); + // Update the buffer + const auto& lastCascade = _cascades.back(); + auto& schema = _schemaBuffer.edit(); + schema.maxDistance = _maxDistance; + schema.invFalloffDistance = 1.0f / (OVERLAP_FACTOR*lastCascade.getMaxDistance()); + } } void LightStage::Shadow::setKeylightFrustum(const ViewFrustum& viewFrustum, diff --git a/libraries/render-utils/src/LightStage.h b/libraries/render-utils/src/LightStage.h index 1fb1754862..4da66843cc 100644 --- a/libraries/render-utils/src/LightStage.h +++ b/libraries/render-utils/src/LightStage.h @@ -74,7 +74,7 @@ public: float left, float right, float bottom, float top, float viewMaxShadowDistance) const; }; - Shadow(graphics::LightPointer light, float maxDistance, unsigned int cascadeCount = 1); + Shadow(graphics::LightPointer light, unsigned int cascadeCount = 1); void setLight(graphics::LightPointer light); @@ -104,16 +104,14 @@ public: }; protected: - using Cascades = std::vector; static const glm::mat4 _biasMatrix; graphics::LightPointer _light; - float _maxDistance; + float _maxDistance{ 0.0f }; Cascades _cascades; - UniformBufferView _schemaBuffer = nullptr; }; diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 2634784f3a..7be5f93a95 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -154,8 +154,7 @@ void MeshPartPayload::render(RenderArgs* args) { bindMesh(batch); // apply material properties - if (args->_renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { - RenderPipelines::bindMaterials(_drawMaterials, batch, args->_enableTexturing); + if (RenderPipelines::bindMaterials(_drawMaterials, batch, args->_renderMode, args->_enableTexturing)) { args->_details._materialSwitches++; } @@ -434,8 +433,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) { } // apply material properties - if (args->_renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { - RenderPipelines::bindMaterials(_drawMaterials, batch, args->_enableTexturing); + if (RenderPipelines::bindMaterials(_drawMaterials, batch, args->_renderMode, args->_enableTexturing)) { args->_details._materialSwitches++; } diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index d52f1da043..d808823d0c 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -364,7 +364,7 @@ void RenderDeferredTaskDebug::build(JobModel& task, const render::Varying& input sprintf(jobName, "DrawShadowFrustum%d", i); task.addJob(jobName, shadowFrustum, glm::vec3(0.0f, tint, 1.0f)); if (!renderShadowTaskOut.isNull()) { - const auto& shadowCascadeSceneBBoxes = renderShadowTaskOut; + const auto& shadowCascadeSceneBBoxes = renderShadowTaskOut.get(); const auto shadowBBox = shadowCascadeSceneBBoxes[ExtractFrustums::SHADOW_CASCADE0_FRUSTUM + i]; sprintf(jobName, "DrawShadowBBox%d", i); task.addJob(jobName, shadowBBox, glm::vec3(1.0f, tint, 0.0f)); diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index c4a7368f39..ac2eb8e475 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -373,10 +373,10 @@ void initZPassPipelines(ShapePlumber& shapePlumber, gpu::StatePointer state, con gpu::Shader::createProgram(deformed_model_shadow_fade_dq), state, extraBatchSetter, itemSetter); } -void RenderPipelines::bindMaterial(graphics::MaterialPointer& material, gpu::Batch& batch, bool enableTextures) { +bool RenderPipelines::bindMaterial(graphics::MaterialPointer& material, gpu::Batch& batch, render::Args::RenderMode renderMode, bool enableTextures) { graphics::MultiMaterial multiMaterial; multiMaterial.push(graphics::MaterialLayer(material, 0)); - bindMaterials(multiMaterial, batch, enableTextures); + return bindMaterials(multiMaterial, batch, renderMode, enableTextures); } void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial) { @@ -730,7 +730,7 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial multiMaterial.setInitialized(); } -void RenderPipelines::bindMaterials(graphics::MultiMaterial& multiMaterial, gpu::Batch& batch, bool enableTextures) { +bool RenderPipelines::bindMaterials(graphics::MultiMaterial& multiMaterial, gpu::Batch& batch, render::Args::RenderMode renderMode, bool enableTextures) { if (multiMaterial.shouldUpdate()) { updateMultiMaterial(multiMaterial); } @@ -738,8 +738,13 @@ void RenderPipelines::bindMaterials(graphics::MultiMaterial& multiMaterial, gpu: auto textureCache = DependencyManager::get(); static gpu::TextureTablePointer defaultMaterialTextures = std::make_shared(); + static gpu::BufferView defaultMaterialSchema; + static std::once_flag once; std::call_once(once, [textureCache] { + graphics::MultiMaterial::Schema schema; + defaultMaterialSchema = gpu::BufferView(std::make_shared(sizeof(schema), (const gpu::Byte*) &schema, sizeof(schema))); + defaultMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, textureCache->getWhiteTexture()); defaultMaterialTextures->setTexture(gr::Texture::MaterialMetallic, textureCache->getBlackTexture()); defaultMaterialTextures->setTexture(gr::Texture::MaterialRoughness, textureCache->getWhiteTexture()); @@ -749,17 +754,29 @@ void RenderPipelines::bindMaterials(graphics::MultiMaterial& multiMaterial, gpu: // MaterialEmissiveLightmap has to be set later }); - auto& schemaBuffer = multiMaterial.getSchemaBuffer(); - batch.setUniformBuffer(gr::Buffer::Material, schemaBuffer); - if (enableTextures) { - batch.setResourceTextureTable(multiMaterial.getTextureTable()); - } else { - auto key = multiMaterial.getMaterialKey(); - if (key.isLightmapMap()) { - defaultMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getBlackTexture()); - } else if (key.isEmissiveMap()) { - defaultMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getGrayTexture()); + // For shadows, we only need opacity mask information + auto key = multiMaterial.getMaterialKey(); + if (renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE || key.isOpacityMaskMap()) { + auto& schemaBuffer = multiMaterial.getSchemaBuffer(); + batch.setUniformBuffer(gr::Buffer::Material, schemaBuffer); + if (enableTextures) { + batch.setResourceTextureTable(multiMaterial.getTextureTable()); + } else { + if (renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { + if (key.isLightmapMap()) { + defaultMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getBlackTexture()); + } else if (key.isEmissiveMap()) { + defaultMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getGrayTexture()); + } + } + + batch.setResourceTextureTable(defaultMaterialTextures); } + return true; + } else { batch.setResourceTextureTable(defaultMaterialTextures); + batch.setUniformBuffer(gr::Buffer::Material, defaultMaterialSchema); + return false; } } + diff --git a/libraries/render-utils/src/RenderPipelines.h b/libraries/render-utils/src/RenderPipelines.h index 0f3d1160ef..5b04de08b6 100644 --- a/libraries/render-utils/src/RenderPipelines.h +++ b/libraries/render-utils/src/RenderPipelines.h @@ -12,12 +12,13 @@ #define hifi_RenderPipelines_h #include +#include class RenderPipelines { public: - static void bindMaterial(graphics::MaterialPointer& material, gpu::Batch& batch, bool enableTextures); static void updateMultiMaterial(graphics::MultiMaterial& multiMaterial); - static void bindMaterials(graphics::MultiMaterial& multiMaterial, gpu::Batch& batch, bool enableTextures); + static bool bindMaterial(graphics::MaterialPointer& material, gpu::Batch& batch, render::Args::RenderMode renderMode, bool enableTextures); + static bool bindMaterials(graphics::MultiMaterial& multiMaterial, gpu::Batch& batch, render::Args::RenderMode renderMode, bool enableTextures); }; diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index a261deefb8..5de89a11b5 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -34,7 +34,6 @@ #define SHADOW_FRUSTUM_NEAR 1.0f #define SHADOW_FRUSTUM_FAR 500.0f static const unsigned int SHADOW_CASCADE_COUNT{ 4 }; -static const float SHADOW_MAX_DISTANCE{ 40.0f }; using namespace render; @@ -90,7 +89,7 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende #endif }; - render::VaryingArray cascadeSceneBBoxes; + CascadeBoxes cascadeSceneBBoxes; for (auto i = 0; i < SHADOW_CASCADE_MAX_COUNT; i++) { char jobName[64]; @@ -336,7 +335,7 @@ void RenderShadowSetup::setConstantBias(int cascadeIndex, float value) { } void RenderShadowSetup::setSlopeBias(int cascadeIndex, float value) { - _bias[cascadeIndex]._slope = value * value * value * 0.01f; + _bias[cascadeIndex]._slope = value * value * value * 0.001f; } void RenderShadowSetup::run(const render::RenderContextPointer& renderContext, const Input& input, Output& output) { @@ -367,7 +366,7 @@ void RenderShadowSetup::run(const render::RenderContextPointer& renderContext, c output.edit2() = _cameraFrustum; if (!_globalShadowObject) { - _globalShadowObject = std::make_shared(graphics::LightPointer(), SHADOW_MAX_DISTANCE, SHADOW_CASCADE_COUNT); + _globalShadowObject = std::make_shared(currentKeyLight, SHADOW_CASCADE_COUNT); } _globalShadowObject->setLight(currentKeyLight); @@ -378,11 +377,12 @@ void RenderShadowSetup::run(const render::RenderContextPointer& renderContext, c unsigned int cascadeIndex; // Adjust each cascade frustum + const auto biasScale = currentKeyLight->getShadowsBiasScale(); for (cascadeIndex = 0; cascadeIndex < _globalShadowObject->getCascadeCount(); ++cascadeIndex) { auto& bias = _bias[cascadeIndex]; _globalShadowObject->setKeylightCascadeFrustum(cascadeIndex, args->getViewFrustum(), - SHADOW_FRUSTUM_NEAR, SHADOW_FRUSTUM_FAR, - bias._constant, bias._slope); + SHADOW_FRUSTUM_NEAR, SHADOW_FRUSTUM_FAR, + bias._constant, bias._slope * biasScale); } _shadowFrameCache->pushShadow(_globalShadowObject); diff --git a/libraries/render-utils/src/RenderShadowTask.h b/libraries/render-utils/src/RenderShadowTask.h index 4dc6f3073f..ef469a7247 100644 --- a/libraries/render-utils/src/RenderShadowTask.h +++ b/libraries/render-utils/src/RenderShadowTask.h @@ -52,8 +52,9 @@ class RenderShadowTask { public: // There is one AABox per shadow cascade + using CascadeBoxes = render::VaryingArray; using Input = render::VaryingSet2; - using Output = render::VaryingSet2, LightStage::ShadowFramePointer>; + using Output = render::VaryingSet2; using Config = RenderShadowTaskConfig; using JobModel = render::Task::ModelIO; @@ -92,10 +93,10 @@ public: float constantBias1{ 0.15f }; float constantBias2{ 0.175f }; float constantBias3{ 0.2f }; - float slopeBias0{ 0.6f }; - float slopeBias1{ 0.6f }; - float slopeBias2{ 0.7f }; - float slopeBias3{ 0.82f }; + float slopeBias0{ 0.4f }; + float slopeBias1{ 0.45f }; + float slopeBias2{ 0.65f }; + float slopeBias3{ 0.7f }; signals: void dirty(); diff --git a/libraries/render-utils/src/Shadow.slh b/libraries/render-utils/src/Shadow.slh index b503844554..94bec86b34 100644 --- a/libraries/render-utils/src/Shadow.slh +++ b/libraries/render-utils/src/Shadow.slh @@ -90,8 +90,8 @@ float evalShadowAttenuationPCF(int cascadeIndex, ShadowSampleOffsets offsets, ve return shadowAttenuation; } -float evalShadowCascadeAttenuation(int cascadeIndex, ShadowSampleOffsets offsets, vec4 shadowTexcoord, float oneMinusNdotL) { - float bias = getShadowFixedBias(cascadeIndex) + getShadowSlopeBias(cascadeIndex) * oneMinusNdotL; +float evalShadowCascadeAttenuation(int cascadeIndex, ShadowSampleOffsets offsets, vec4 shadowTexcoord, float slopeNdotL) { + float bias = getShadowFixedBias(cascadeIndex) + getShadowSlopeBias(cascadeIndex) * slopeNdotL; return evalShadowAttenuationPCF(cascadeIndex, offsets, shadowTexcoord, bias); } @@ -104,7 +104,8 @@ float evalShadowAttenuation(vec3 worldLightDir, vec4 worldPosition, float viewDe vec3 cascadeMix; bvec4 isPixelOnCascade; int cascadeIndex; - float oneMinusNdotL = 1.0 - clamp(dot(worldLightDir, worldNormal), 0.0, 1.0); + float NdotL = clamp(dot(worldLightDir, worldNormal), 0.0, 1.0); + float slopeNdotL = min(2.0, sqrt(1.0-NdotL*NdotL) / NdotL); for (cascadeIndex=0 ; cascadeIndex(); drawMask(*state); state->setColorWriteMask(gpu::State::WRITE_NONE); + state->setCullMode(gpu::State::CullMode::CULL_NONE); _meshStencilPipeline = gpu::Pipeline::create(program, state); } @@ -64,8 +64,28 @@ gpu::PipelinePointer PrepareStencil::getPaintStencilPipeline() { void PrepareStencil::run(const RenderContextPointer& renderContext, const gpu::FramebufferPointer& srcFramebuffer) { RenderArgs* args = renderContext->args; - // Only draw the stencil mask if in HMD mode or not forced. - if (!_forceDraw && (args->_displayMode != RenderArgs::STEREO_HMD)) { + if (args->_takingSnapshot) { + return; + } + + StencilMaskMode maskMode = _maskMode; + std::function maskOperator = [this](gpu::Batch& batch) { + auto mesh = getMesh(); + batch.setIndexBuffer(mesh->getIndexBuffer()); + batch.setInputFormat((mesh->getVertexFormat())); + batch.setInputStream(0, mesh->getVertexStream()); + + // Draw + auto part = mesh->getPartBuffer().get(0); + batch.drawIndexed(gpu::TRIANGLES, part._numIndices, part._startIndex); + }; + + if (maskMode == StencilMaskMode::NONE) { + maskMode = args->_stencilMaskMode; + maskOperator = args->_stencilMaskOperator; + } + + if (maskMode == StencilMaskMode::NONE || (maskMode == StencilMaskMode::MESH && !maskOperator)) { return; } @@ -74,20 +94,12 @@ void PrepareStencil::run(const RenderContextPointer& renderContext, const gpu::F batch.setViewportTransform(args->_viewport); - if (_maskMode < 0) { - batch.setPipeline(getMeshStencilPipeline()); - - auto mesh = getMesh(); - batch.setIndexBuffer(mesh->getIndexBuffer()); - batch.setInputFormat((mesh->getVertexFormat())); - batch.setInputStream(0, mesh->getVertexStream()); - - // Draw - auto part = mesh->getPartBuffer().get(0); - batch.drawIndexed(gpu::TRIANGLES, part._numIndices, part._startIndex); - } else { + if (maskMode == StencilMaskMode::PAINT) { batch.setPipeline(getPaintStencilPipeline()); batch.draw(gpu::TRIANGLE_STRIP, 4); + } else if (maskMode == StencilMaskMode::MESH) { + batch.setPipeline(getMeshStencilPipeline()); + maskOperator(batch); } }); } diff --git a/libraries/render-utils/src/StencilMaskPass.h b/libraries/render-utils/src/StencilMaskPass.h index a8e4d1e1f2..bca2ef17a5 100644 --- a/libraries/render-utils/src/StencilMaskPass.h +++ b/libraries/render-utils/src/StencilMaskPass.h @@ -15,17 +15,19 @@ #include #include #include +#include class PrepareStencilConfig : public render::Job::Config { Q_OBJECT - Q_PROPERTY(int maskMode MEMBER maskMode NOTIFY dirty) - Q_PROPERTY(bool forceDraw MEMBER forceDraw NOTIFY dirty) + Q_PROPERTY(StencilMaskMode maskMode MEMBER maskMode NOTIFY dirty) public: PrepareStencilConfig(bool enabled = true) : JobConfig(enabled) {} - int maskMode { 0 }; - bool forceDraw { false }; + // -1 -> don't force drawing (fallback to render args mode) + // 0 -> force draw without mesh + // 1 -> force draw with mesh + StencilMaskMode maskMode { StencilMaskMode::NONE }; signals: void dirty(); @@ -66,8 +68,7 @@ private: graphics::MeshPointer _mesh; graphics::MeshPointer getMesh(); - int _maskMode { 0 }; - bool _forceDraw { false }; + StencilMaskMode _maskMode { StencilMaskMode::NONE }; }; diff --git a/libraries/render-utils/src/deformed_model_shadow.slv b/libraries/render-utils/src/deformed_model_shadow.slv index cc6fb42f98..827fc69b32 100644 --- a/libraries/render-utils/src/deformed_model_shadow.slv +++ b/libraries/render-utils/src/deformed_model_shadow.slv @@ -18,9 +18,15 @@ <$declareMeshDeformer(_SCRIBE_NULL, _SCRIBE_NULL, 1, _SCRIBE_NULL, 1)$> <$declareMeshDeformerActivation(1, 1)$> +<@include graphics/Material.slh@> +<@include graphics/MaterialTextures.slh@> + +<$declareMaterialTexMapArrayBuffer()$> + <@include render-utils/ShaderConstants.h@> layout(location=RENDER_UTILS_ATTR_POSITION_WS) out vec4 _positionWS; +layout(location=RENDER_UTILS_ATTR_TEXCOORD01) out vec4 _texCoord01; void main(void) { vec4 deformedPosition = vec4(0.0, 0.0, 0.0, 0.0); @@ -33,5 +39,14 @@ void main(void) { TransformObject obj = getTransformObject(); <$transformModelToClipPos(cam, obj, deformedPosition, gl_Position)$> <$transformModelToWorldPos(obj, deformedPosition, _positionWS)$> + + Material mat = getMaterial(); + BITFIELD matKey = getMaterialKey(mat); + _texCoord01 = vec4(0.0, 0.0, 0.0, 0.0); + // If we have an opacity mask than we need the first tex coord + if ((matKey & OPACITY_MASK_MAP_BIT) != 0) { + TexMapArray texMapArray = getTexMapArray(); + <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _positionWS, _texCoord01.xy)$> + } } diff --git a/libraries/render-utils/src/deformed_model_shadow_dq.slv b/libraries/render-utils/src/deformed_model_shadow_dq.slv index 84b5dfb33b..646fc12ce9 100644 --- a/libraries/render-utils/src/deformed_model_shadow_dq.slv +++ b/libraries/render-utils/src/deformed_model_shadow_dq.slv @@ -18,9 +18,15 @@ <$declareMeshDeformer(_SCRIBE_NULL, _SCRIBE_NULL, 1, 1, 1)$> <$declareMeshDeformerActivation(1, 1)$> +<@include graphics/Material.slh@> +<@include graphics/MaterialTextures.slh@> + +<$declareMaterialTexMapArrayBuffer()$> + <@include render-utils/ShaderConstants.h@> layout(location=RENDER_UTILS_ATTR_POSITION_WS) out vec4 _positionWS; +layout(location=RENDER_UTILS_ATTR_TEXCOORD01) out vec4 _texCoord01; void main(void) { vec4 deformedPosition = vec4(0.0, 0.0, 0.0, 0.0); @@ -33,4 +39,13 @@ void main(void) { TransformObject obj = getTransformObject(); <$transformModelToClipPos(cam, obj, deformedPosition, gl_Position)$> <$transformModelToWorldPos(obj, deformedPosition, _positionWS)$> + + Material mat = getMaterial(); + BITFIELD matKey = getMaterialKey(mat); + _texCoord01 = vec4(0.0, 0.0, 0.0, 0.0); + // If we have an opacity mask than we need the first tex coord + if ((matKey & OPACITY_MASK_MAP_BIT) != 0) { + TexMapArray texMapArray = getTexMapArray(); + <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _positionWS, _texCoord01.xy)$> + } } diff --git a/libraries/render-utils/src/model_normal_map.slf b/libraries/render-utils/src/model_normal_map.slf index 5fbc81e35b..695fadbca8 100644 --- a/libraries/render-utils/src/model_normal_map.slf +++ b/libraries/render-utils/src/model_normal_map.slf @@ -32,7 +32,7 @@ void main(void) { <$fetchMaterialTexturesCoord1(matKey, _texCoord1, occlusionTex)$> float opacity = 1.0; - <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)&>; + <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; <$discardTransparent(opacity)$>; vec3 albedo = getMaterialAlbedo(mat); diff --git a/libraries/render-utils/src/model_shadow.slf b/libraries/render-utils/src/model_shadow.slf index 862fcd0cf6..6d8dfb7e2a 100644 --- a/libraries/render-utils/src/model_shadow.slf +++ b/libraries/render-utils/src/model_shadow.slf @@ -9,10 +9,26 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +<@include graphics/Material.slh@> +<@include graphics/MaterialTextures.slh@> +<@include render-utils/ShaderConstants.h@> + +<$declareMaterialTextures(ALBEDO, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL)$> + +layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; +#define _texCoord0 _texCoord01.xy layout(location=0) out vec4 _fragColor; void main(void) { + Material mat = getMaterial(); + BITFIELD matKey = getMaterialKey(mat); + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL)$> + + float opacity = 1.0; + <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + <$discardTransparent(opacity)$>; + // pass-through to set z-buffer _fragColor = vec4(1.0, 1.0, 1.0, 0.0); } diff --git a/libraries/render-utils/src/model_shadow.slv b/libraries/render-utils/src/model_shadow.slv index 8de77d7b1d..d455ea4ade 100644 --- a/libraries/render-utils/src/model_shadow.slv +++ b/libraries/render-utils/src/model_shadow.slv @@ -13,12 +13,16 @@ <@include gpu/Inputs.slh@> <@include gpu/Transform.slh@> +<@include graphics/Material.slh@> +<@include graphics/MaterialTextures.slh@> <$declareStandardTransform()$> +<$declareMaterialTexMapArrayBuffer()$> <@include render-utils/ShaderConstants.h@> layout(location=RENDER_UTILS_ATTR_POSITION_WS) out vec4 _positionWS; +layout(location=RENDER_UTILS_ATTR_TEXCOORD01) out vec4 _texCoord01; void main(void) { // standard transform @@ -26,4 +30,13 @@ void main(void) { TransformObject obj = getTransformObject(); <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> <$transformModelToWorldPos(obj, inPosition, _positionWS)$> + + Material mat = getMaterial(); + BITFIELD matKey = getMaterialKey(mat); + _texCoord01 = vec4(0.0, 0.0, 0.0, 0.0); + // If we have an opacity mask than we need the first tex coord + if ((matKey & OPACITY_MASK_MAP_BIT) != 0) { + TexMapArray texMapArray = getTexMapArray(); + <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _positionWS, _texCoord01.xy)$> + } } diff --git a/libraries/render-utils/src/model_shadow_fade.slf b/libraries/render-utils/src/model_shadow_fade.slf index 210b5482c8..1f94d4a0ac 100644 --- a/libraries/render-utils/src/model_shadow_fade.slf +++ b/libraries/render-utils/src/model_shadow_fade.slf @@ -9,13 +9,18 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // - +<@include graphics/Material.slh@> +<@include graphics/MaterialTextures.slh@> <@include render-utils/ShaderConstants.h@> +<$declareMaterialTextures(ALBEDO, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL)$> + <@include Fade.slh@> <$declareFadeFragment()$> layout(location=RENDER_UTILS_ATTR_POSITION_WS) in vec4 _positionWS; +layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01; +#define _texCoord0 _texCoord01.xy layout(location=0) out vec4 _fragColor; @@ -24,6 +29,14 @@ void main(void) { <$fetchFadeObjectParams(fadeParams)$> applyFadeClip(fadeParams, _positionWS.xyz); + Material mat = getMaterial(); + BITFIELD matKey = getMaterialKey(mat); + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL, _SCRIBE_NULL)$> + + float opacity = 1.0; + <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + <$discardTransparent(opacity)$>; + // pass-through to set z-buffer _fragColor = vec4(1.0, 1.0, 1.0, 0.0); } diff --git a/libraries/render/src/render/Args.h b/libraries/render/src/render/Args.h index 78e9909222..953a0b8223 100644 --- a/libraries/render/src/render/Args.h +++ b/libraries/render/src/render/Args.h @@ -16,6 +16,7 @@ #include #include +#include #include #include "Forward.h" @@ -133,6 +134,10 @@ namespace render { std::function _hudOperator; gpu::TexturePointer _hudTexture; + + bool _takingSnapshot { false }; + StencilMaskMode _stencilMaskMode { StencilMaskMode::NONE }; + std::function _stencilMaskOperator; }; } diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 60b426e46d..be60533406 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -185,6 +185,30 @@ QString PathUtils::generateTemporaryDir() { return ""; } +bool PathUtils::deleteMyTemporaryDir(QString dirName) { + QDir rootTempDir = QDir::tempPath(); + + QString appName = qApp->applicationName(); + QRegularExpression re { "^" + QRegularExpression::escape(appName) + "\\-(?\\d+)\\-(?\\d+)$" }; + + auto match = re.match(dirName); + auto pid = match.capturedRef("pid").toLongLong(); + + if (match.hasMatch() && rootTempDir.exists(dirName) && pid == qApp->applicationPid()) { + auto absoluteDirPath = QDir(rootTempDir.absoluteFilePath(dirName)); + + bool success = absoluteDirPath.removeRecursively(); + if (success) { + qDebug() << " Removing temporary directory: " << absoluteDirPath.absolutePath(); + } else { + qDebug() << " Failed to remove temporary directory: " << absoluteDirPath.absolutePath(); + } + return success; + } + + return false; +} + // Delete all temporary directories for an application int PathUtils::removeTemporaryApplicationDirs(QString appName) { if (appName.isNull()) { diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index 0096cb6c90..cac5c58ca4 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -56,6 +56,7 @@ public: static QString getAppLocalDataFilePath(const QString& filename); static QString generateTemporaryDir(); + static bool deleteMyTemporaryDir(QString dirName); static int removeTemporaryApplicationDirs(QString appName = QString::null); diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 597e537d8d..e23064c8a0 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -40,6 +40,7 @@ int qMapURLStringMetaTypeId = qRegisterMetaType>(); int socketErrorMetaTypeId = qRegisterMetaType(); int voidLambdaType = qRegisterMetaType>(); int variantLambdaType = qRegisterMetaType>(); +int stencilModeMetaTypeId = qRegisterMetaType(); void registerMetaTypes(QScriptEngine* engine) { qScriptRegisterMetaType(engine, vec2ToScriptValue, vec2FromScriptValue); @@ -64,6 +65,8 @@ void registerMetaTypes(QScriptEngine* engine) { qScriptRegisterMetaType(engine, collisionToScriptValue, collisionFromScriptValue); qScriptRegisterMetaType(engine, quuidToScriptValue, quuidFromScriptValue); qScriptRegisterMetaType(engine, aaCubeToScriptValue, aaCubeFromScriptValue); + + qScriptRegisterMetaType(engine, stencilMaskModeToScriptValue, stencilMaskModeFromScriptValue); } QScriptValue vec2ToScriptValue(QScriptEngine* engine, const glm::vec2& vec2) { @@ -636,6 +639,81 @@ void mat4FromScriptValue(const QScriptValue& object, glm::mat4& mat4) { mat4[3][3] = object.property("r3c3").toVariant().toFloat(); } +QVariant mat4ToVariant(const glm::mat4& mat4) { + if (mat4 != mat4) { + // NaN + return QVariant(); + } + QVariantMap object; + + object["r0c0"] = mat4[0][0]; + object["r1c0"] = mat4[0][1]; + object["r2c0"] = mat4[0][2]; + object["r3c0"] = mat4[0][3]; + object["r0c1"] = mat4[1][0]; + object["r1c1"] = mat4[1][1]; + object["r2c1"] = mat4[1][2]; + object["r3c1"] = mat4[1][3]; + object["r0c2"] = mat4[2][0]; + object["r1c2"] = mat4[2][1]; + object["r2c2"] = mat4[2][2]; + object["r3c2"] = mat4[2][3]; + object["r0c3"] = mat4[3][0]; + object["r1c3"] = mat4[3][1]; + object["r2c3"] = mat4[3][2]; + object["r3c3"] = mat4[3][3]; + + return object; +} + +glm::mat4 mat4FromVariant(const QVariant& object, bool& valid) { + glm::mat4 mat4; + valid = false; + if (!object.isValid() || object.isNull()) { + return mat4; + } else { + const static auto getElement = [](const QVariantMap& map, const char * key, float& value, bool& everyConversionValid) { + auto variantValue = map[key]; + if (variantValue.canConvert()) { + value = variantValue.toFloat(); + } else { + everyConversionValid = false; + } + }; + + auto map = object.toMap(); + bool everyConversionValid = true; + + getElement(map, "r0c0", mat4[0][0], everyConversionValid); + getElement(map, "r1c0", mat4[0][1], everyConversionValid); + getElement(map, "r2c0", mat4[0][2], everyConversionValid); + getElement(map, "r3c0", mat4[0][3], everyConversionValid); + getElement(map, "r0c1", mat4[1][0], everyConversionValid); + getElement(map, "r1c1", mat4[1][1], everyConversionValid); + getElement(map, "r2c1", mat4[1][2], everyConversionValid); + getElement(map, "r3c1", mat4[1][3], everyConversionValid); + getElement(map, "r0c2", mat4[2][0], everyConversionValid); + getElement(map, "r1c2", mat4[2][1], everyConversionValid); + getElement(map, "r2c2", mat4[2][2], everyConversionValid); + getElement(map, "r3c2", mat4[2][3], everyConversionValid); + getElement(map, "r0c3", mat4[3][0], everyConversionValid); + getElement(map, "r1c3", mat4[3][1], everyConversionValid); + getElement(map, "r2c3", mat4[3][2], everyConversionValid); + getElement(map, "r3c3", mat4[3][3], everyConversionValid); + + if (everyConversionValid) { + valid = true; + } + + return mat4; + } +} + +glm::mat4 mat4FromVariant(const QVariant& object) { + bool valid = false; + return mat4FromVariant(object, valid); +} + QScriptValue qVectorVec3ColorToScriptValue(QScriptEngine* engine, const QVector& vector) { QScriptValue array = engine->newArray(); for (int i = 0; i < vector.size(); i++) { @@ -1283,4 +1361,12 @@ QVariantMap parseTexturesToMap(QString newTextures, const QVariantMap& defaultTe } return toReturn; +} + +QScriptValue stencilMaskModeToScriptValue(QScriptEngine* engine, const StencilMaskMode& stencilMode) { + return engine->newVariant((int)stencilMode); +} + +void stencilMaskModeFromScriptValue(const QScriptValue& object, StencilMaskMode& stencilMode) { + stencilMode = StencilMaskMode(object.toVariant().toInt()); } \ No newline at end of file diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index ea2c5b8354..d3fb816f3c 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -25,6 +25,7 @@ #include "shared/Bilateral.h" #include "Transform.h" #include "PhysicsCollisionGroups.h" +#include "StencilMaskMode.h" class QColor; class QUrl; @@ -67,6 +68,10 @@ void registerMetaTypes(QScriptEngine* engine); QScriptValue mat4toScriptValue(QScriptEngine* engine, const glm::mat4& mat4); void mat4FromScriptValue(const QScriptValue& object, glm::mat4& mat4); +QVariant mat4ToVariant(const glm::mat4& mat4); +glm::mat4 mat4FromVariant(const QVariant& object, bool& valid); +glm::mat4 mat4FromVariant(const QVariant& object); + /**jsdoc * A 2-dimensional vector. * @@ -729,5 +734,8 @@ void qVectorMeshFaceFromScriptValue(const QScriptValue& array, QVector QVariantMap parseTexturesToMap(QString textures, const QVariantMap& defaultTextures); +Q_DECLARE_METATYPE(StencilMaskMode) +QScriptValue stencilMaskModeToScriptValue(QScriptEngine* engine, const StencilMaskMode& stencilMode); +void stencilMaskModeFromScriptValue(const QScriptValue& object, StencilMaskMode& stencilMode); #endif // hifi_RegisteredMetaTypes_h diff --git a/libraries/shared/src/StencilMaskMode.h b/libraries/shared/src/StencilMaskMode.h new file mode 100644 index 0000000000..98372890ec --- /dev/null +++ b/libraries/shared/src/StencilMaskMode.h @@ -0,0 +1,18 @@ +// +// Created by Sam Gondelman on 3/26/19. +// Copyright 2019 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_StencilMaskMode_h +#define hifi_StencilMaskMode_h + +enum class StencilMaskMode { + NONE = -1, // for legacy reasons, this is -1 + PAINT = 0, + MESH = 1 +}; + +#endif // hifi_StencilMaskMode_h \ No newline at end of file diff --git a/libraries/shared/src/VariantMapToScriptValue.cpp b/libraries/shared/src/VariantMapToScriptValue.cpp index 437f60a2ed..b3c02d99f4 100644 --- a/libraries/shared/src/VariantMapToScriptValue.cpp +++ b/libraries/shared/src/VariantMapToScriptValue.cpp @@ -44,7 +44,7 @@ QScriptValue variantToScriptValue(QVariant& qValue, QScriptEngine& scriptEngine) if (qValue.canConvert()) { return qValue.toFloat(); } - qCDebug(shared) << "unhandled QScript type" << qValue.type(); + //qCDebug(shared) << "unhandled QScript type" << qValue.type(); break; } diff --git a/libraries/task/src/task/Task.h b/libraries/task/src/task/Task.h index 632e8a222e..b74986235e 100644 --- a/libraries/task/src/task/Task.h +++ b/libraries/task/src/task/Task.h @@ -152,6 +152,7 @@ public: template static std::shared_ptr create(const std::string& name, const Varying& input, A&&... args) { + assert(input.canCast()); return std::make_shared(name, input, std::make_shared(), std::forward(args)...); } diff --git a/plugins/oculus/CMakeLists.txt b/plugins/oculus/CMakeLists.txt index abce753b4d..6ddc75e1e5 100644 --- a/plugins/oculus/CMakeLists.txt +++ b/plugins/oculus/CMakeLists.txt @@ -18,7 +18,7 @@ if (WIN32 AND (NOT USE_GLES)) link_hifi_libraries( shared task gl shaders gpu ${PLATFORM_GL_BACKEND} controllers ui qml plugins ui-plugins display-plugins input-plugins - audio-client networking render-utils + audio-client networking render-utils graphics ${PLATFORM_GL_BACKEND} ) include_hifi_library_headers(octree) diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index a67e3127e5..db26537a19 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -227,3 +227,66 @@ QVector OculusBaseDisplayPlugin::getSensorPositions() { return result; } + +DisplayPlugin::StencilMaskMeshOperator OculusBaseDisplayPlugin::getStencilMaskMeshOperator() { + if (_session) { + if (!_stencilMeshesInitialized) { + _stencilMeshesInitialized = true; + ovr::for_each_eye([&](ovrEyeType eye) { + ovrFovStencilDesc stencilDesc = { + ovrFovStencil_HiddenArea, 0, eye, + _eyeRenderDescs[eye].Fov, _eyeRenderDescs[eye].HmdToEyePose.Orientation + }; + // First we get the size of the buffer we need + ovrFovStencilMeshBuffer buffer = { 0, 0, nullptr, 0, 0, nullptr }; + ovrResult result = ovr_GetFovStencil(_session, &stencilDesc, &buffer); + if (!OVR_SUCCESS(result)) { + _stencilMeshesInitialized = false; + return; + } + + std::vector ovrVertices(buffer.UsedVertexCount); + std::vector ovrIndices(buffer.UsedIndexCount); + + // Now we populate the actual buffer + buffer = { (int)ovrVertices.size(), 0, ovrVertices.data(), (int)ovrIndices.size(), 0, ovrIndices.data() }; + result = ovr_GetFovStencil(_session, &stencilDesc, &buffer); + + if (!OVR_SUCCESS(result)) { + _stencilMeshesInitialized = false; + return; + } + + std::vector vertices; + vertices.reserve(ovrVertices.size()); + for (auto& ovrVertex : ovrVertices) { + // We need the vertices in clip space + vertices.emplace_back(ovrVertex.x - (1.0f - (float)eye), 2.0f * ovrVertex.y - 1.0f, 0.0f); + } + + std::vector indices; + indices.reserve(ovrIndices.size()); + for (auto& ovrIndex : ovrIndices) { + indices.push_back(ovrIndex); + } + + _stencilMeshes[eye] = graphics::Mesh::createIndexedTriangles_P3F((uint32_t)vertices.size(), (uint32_t)indices.size(), vertices.data(), indices.data()); + }); + } + + if (_stencilMeshesInitialized) { + return [&](gpu::Batch& batch) { + for (auto& mesh : _stencilMeshes) { + batch.setIndexBuffer(mesh->getIndexBuffer()); + batch.setInputFormat((mesh->getVertexFormat())); + batch.setInputStream(0, mesh->getVertexStream()); + + // Draw + auto part = mesh->getPartBuffer().get(0); + batch.drawIndexed(gpu::TRIANGLES, part._numIndices, part._startIndex); + } + }; + } + } + return nullptr; +} \ No newline at end of file diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.h b/plugins/oculus/src/OculusBaseDisplayPlugin.h index 1abb7cdad7..d442c365ae 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.h +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.h @@ -16,6 +16,8 @@ #define OVRPL_DISABLED #include +#include + class OculusBaseDisplayPlugin : public HmdDisplayPlugin { using Parent = HmdDisplayPlugin; public: @@ -34,6 +36,9 @@ public: QRectF getPlayAreaRect() override; QVector getSensorPositions() override; + virtual StencilMaskMode getStencilMaskMode() const override { return StencilMaskMode::MESH; } + virtual StencilMaskMeshOperator getStencilMaskMeshOperator() override; + protected: void customizeContext() override; void uncustomizeContext() override; @@ -52,4 +57,7 @@ protected: // ovrLayerEyeFovDepth _depthLayer; bool _hmdMounted { false }; bool _visible { true }; + + std::array _stencilMeshes; + bool _stencilMeshesInitialized { false }; }; diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 11d941dcd0..cd318dd9b4 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -784,3 +784,48 @@ QRectF OpenVrDisplayPlugin::getPlayAreaRect() { return QRectF(center.x, center.y, dimensions.x, dimensions.y); } + +DisplayPlugin::StencilMaskMeshOperator OpenVrDisplayPlugin::getStencilMaskMeshOperator() { + if (_system) { + if (!_stencilMeshesInitialized) { + _stencilMeshesInitialized = true; + for (auto eye : VR_EYES) { + vr::HiddenAreaMesh_t stencilMesh = _system->GetHiddenAreaMesh(eye); + if (stencilMesh.pVertexData && stencilMesh.unTriangleCount > 0) { + std::vector vertices; + std::vector indices; + + const int NUM_INDICES_PER_TRIANGLE = 3; + int numIndices = stencilMesh.unTriangleCount * NUM_INDICES_PER_TRIANGLE; + vertices.reserve(numIndices); + indices.reserve(numIndices); + for (int i = 0; i < numIndices; i++) { + vr::HmdVector2_t vertex2D = stencilMesh.pVertexData[i]; + // We need the vertices in clip space + vertices.emplace_back(vertex2D.v[0] - (1.0f - (float)eye), 2.0f * vertex2D.v[1] - 1.0f, 0.0f); + indices.push_back(i); + } + + _stencilMeshes[eye] = graphics::Mesh::createIndexedTriangles_P3F((uint32_t)vertices.size(), (uint32_t)indices.size(), vertices.data(), indices.data()); + } else { + _stencilMeshesInitialized = false; + } + } + } + + if (_stencilMeshesInitialized) { + return [&](gpu::Batch& batch) { + for (auto& mesh : _stencilMeshes) { + batch.setIndexBuffer(mesh->getIndexBuffer()); + batch.setInputFormat((mesh->getVertexFormat())); + batch.setInputStream(0, mesh->getVertexStream()); + + // Draw + auto part = mesh->getPartBuffer().get(0); + batch.drawIndexed(gpu::TRIANGLES, part._numIndices, part._startIndex); + } + }; + } + } + return nullptr; +} \ No newline at end of file diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index 265f328920..4b042a700d 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -13,6 +13,8 @@ #include +#include + const float TARGET_RATE_OpenVr = 90.0f; // FIXME: get from sdk tracked device property? This number is vive-only. namespace gl { @@ -67,6 +69,9 @@ public: QRectF getPlayAreaRect() override; + virtual StencilMaskMode getStencilMaskMode() const override { return StencilMaskMode::MESH; } + virtual StencilMaskMeshOperator getStencilMaskMeshOperator() override; + protected: bool internalActivate() override; void internalDeactivate() override; @@ -94,4 +99,7 @@ private: bool _asyncReprojectionActive { false }; bool _hmdMounted { false }; + + std::array _stencilMeshes; + bool _stencilMeshesInitialized { false }; }; diff --git a/scripts/developer/utilities/lib/jet/jet.js b/scripts/developer/utilities/lib/jet/jet.js index 40563e4b2c..52c13c5279 100644 --- a/scripts/developer/utilities/lib/jet/jet.js +++ b/scripts/developer/utilities/lib/jet/jet.js @@ -10,7 +10,12 @@ // "use strict"; - // traverse task tree + // traverse task tree recursively + // + // @param root: the root job config from where to traverse + // @param functor: the functor function() which is applied on every subjobs of root traversed + // if return true, then 'task_tree' is called recursively on that subjob + // @param depth: the depth of the recurse loop since the initial call. function task_traverse(root, functor, depth) { if (root.isTask()) { depth++; @@ -22,6 +27,9 @@ function task_traverse(root, functor, depth) { } } } + +// same function as 'task_traverse' with the depth being 0 +// and visisting the root job first. function task_traverseTree(root, functor) { if (functor(root, 0, 0)) { task_traverse(root, functor, 0) diff --git a/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml b/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml new file mode 100644 index 0000000000..165edcf2e1 --- /dev/null +++ b/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml @@ -0,0 +1,123 @@ +// +// jet/TaskPropView.qml +// +// Created by Sam Gateau, 2018/05/09 +// 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 +// + +import QtQuick 2.7 +import QtQuick.Controls 1.4 as Original +import QtQuick.Controls.Styles 1.4 + +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls + +import "../../prop" as Prop + +import "../jet.js" as Jet + +Prop.PropGroup { + + id: root; + + property var rootConfig : Render + property var jobPath: "" + property alias label: root.label + property alias indentDepth: root.indentDepth + + property var showProps: true + property var showSubs: true + property var jobEnabled: rootConfig.getConfig(jobPath).enabled + property var jobCpuTime: pullCpuTime() + + function pullCpuTime() { + if (jobEnabled) { + return rootConfig.getConfig(jobPath).cpuRunTime.toPrecision(3); + } else { + return '.' + } + } + + property var toggleJobActivation: function() { + console.log("the button has been pressed and jobEnabled is " + jobEnabled ) + jobEnabled = !jobEnabled; + rootConfig.getConfig(jobPath).enabled = jobEnabled; + } + + // Panel Header Data Component + panelHeaderData: Component { + Item { + id: header + Prop.PropLabel { + text: root.label + horizontalAlignment: Text.AlignHCenter + anchors.left: parent.left + anchors.right: cpuTime.left + anchors.verticalCenter: parent.verticalCenter + } + Prop.PropLabel { + id: cpuTime + visible: false // root.jobEnabled + width: 50 + text: jobCpuTime + horizontalAlignment: Text.AlignLeft + anchors.rightMargin: 5 + anchors.right:enabledIcon.right + anchors.verticalCenter: parent.verticalCenter + } + Prop.PropCanvasIcon { + id: enabledIcon + anchors.right:parent.right + anchors.verticalCenter: parent.verticalCenter + filled: root.jobEnabled + fillColor: (root.jobEnabled ? global.colorGreenHighlight : global.colorOrangeAccent) + icon: 5 + iconMouseArea.onClicked: { toggleJobActivation() } + } + + } + } + + function populatePropItems() { + var propsModel = [] + var props = Jet.job_propKeys(rootConfig.getConfig(jobPath)); + // console.log(JSON.stringify(props)); + if (showProps) { + for (var p in props) { + propsModel.push({"object": rootConfig.getConfig(jobPath), "property":props[p] }) + } + root.updatePropItems(root.propItemsPanel, propsModel); + } + + if (showSubs) { + Jet.task_traverse(rootConfig.getConfig(jobPath), + function(job, depth, index) { + var component = Qt.createComponent("./TaskPropView.qml"); + component.createObject(root.propItemsPanel, { + "label": job.objectName, + "rootConfig": root.rootConfig, + "jobPath": root.jobPath + '.' + job.objectName, + "showProps": root.showProps, + "showSubs": root.showSubs, + "indentDepth": root.indentDepth + 1, + }) + /* var component = Qt.createComponent("../../prop/PropItem.qml"); + component.createObject(root.propItemsPanel, { + "label": root.jobPath + '.' + job.objectName + ' num=' + index, + })*/ + // propsModel.push({"type": "printLabel", "label": root.jobPath + '.' + job.objectName + ' num=' + index }) + + return (depth < 1); + }, 0) + } + } + + Component.onCompleted: { + populatePropItems() + } + + +} \ No newline at end of file diff --git a/scripts/developer/utilities/lib/jet/qml/qmldir b/scripts/developer/utilities/lib/jet/qml/qmldir index e16820914b..2914c27c23 100644 --- a/scripts/developer/utilities/lib/jet/qml/qmldir +++ b/scripts/developer/utilities/lib/jet/qml/qmldir @@ -1,3 +1,4 @@ TaskList 1.0 TaskList.qml TaskViewList 1.0 TaskViewList.qml TaskTimeFrameView 1.0 TaskTimeFrameView.qml +TaskPropView 1.0 TaskPropView.qml \ No newline at end of file diff --git a/scripts/developer/utilities/lib/prop/PropBool.qml b/scripts/developer/utilities/lib/prop/PropBool.qml new file mode 100644 index 0000000000..1d50ec7dd3 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropBool.qml @@ -0,0 +1,32 @@ +// +// PropBool.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +PropItem { + Global { id: global } + id: root + + property alias valueVar : checkboxControl.checked + + Component.onCompleted: { + valueVar = root.valueVarGetter(); + } + + PropCheckBox { + id: checkboxControl + + anchors.left: root.splitter.right + anchors.verticalCenter: root.verticalCenter + width: root.width * global.valueAreaWidthScale + + onCheckedChanged: { root.valueVarSetter(checked); } + } +} \ No newline at end of file diff --git a/scripts/developer/utilities/lib/prop/PropColor.qml b/scripts/developer/utilities/lib/prop/PropColor.qml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/developer/utilities/lib/prop/PropEnum.qml b/scripts/developer/utilities/lib/prop/PropEnum.qml new file mode 100644 index 0000000000..87c2845c90 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropEnum.qml @@ -0,0 +1,38 @@ +// +// PropEnum.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import QtQuick.Controls 2.2 + +PropItem { + Global { id: global } + id: root + + property alias valueVar : valueCombo.currentIndex + property alias enums : valueCombo.model + + Component.onCompleted: { + // valueVar = root.valueVarGetter(); + } + + PropComboBox { + id: valueCombo + + flat: true + + anchors.left: root.splitter.right + anchors.right: root.right + anchors.verticalCenter: root.verticalCenter + height: global.slimHeight + + currentIndex: root.valueVarGetter() + onCurrentIndexChanged: { root.valueVarSetter(currentIndex); } + } +} diff --git a/scripts/developer/utilities/lib/prop/PropGroup.qml b/scripts/developer/utilities/lib/prop/PropGroup.qml new file mode 100644 index 0000000000..384b50ecad --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropGroup.qml @@ -0,0 +1,102 @@ +// +// PropGroup.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +// PropGroup is mostly reusing the look and feel of the PropFolderPanel +// It is populated by calling "updatePropItems" +// or adding manually new Items to the "propItemsPanel" +PropFolderPanel { + Global { id: global } + id: root + + property alias propItemsPanel: root.panelFrameContent + + // Prop Group is designed to author an array of ProItems, they are defined with an array of the tuplets describing each individual item: + // [ ..., PropItemInfo, ...] + // PropItemInfo { + // type: "PropXXXX", object: JSobject, property: "propName" + // } + // + function updatePropItems(propItemsContainer, propItemsModel) { + root.hasContent = false + for (var i = 0; i < propItemsModel.length; i++) { + var proItem = propItemsModel[i]; + // valid object + if (proItem['object'] !== undefined && proItem['object'] !== null ) { + // valid property + if (proItem['property'] !== undefined && proItem.object[proItem.property] !== undefined) { + // check type + if (proItem['type'] === undefined) { + proItem['type'] = typeof(proItem.object[proItem.property]) + } + switch(proItem.type) { + case 'boolean': + case 'PropBool': { + var component = Qt.createComponent("PropBool.qml"); + component.createObject(propItemsContainer, { + "label": proItem.property, + "object": proItem.object, + "property": proItem.property + }) + } break; + case 'number': + case 'PropScalar': { + var component = Qt.createComponent("PropScalar.qml"); + component.createObject(propItemsContainer, { + "label": proItem.property, + "object": proItem.object, + "property": proItem.property, + "min": (proItem["min"] !== undefined ? proItem.min : 0.0), + "max": (proItem["max"] !== undefined ? proItem.max : 1.0), + "integer": (proItem["integral"] !== undefined ? proItem.integral : false), + }) + } break; + case 'PropEnum': { + var component = Qt.createComponent("PropEnum.qml"); + component.createObject(propItemsContainer, { + "label": proItem.property, + "object": proItem.object, + "property": proItem.property, + "enums": (proItem["enums"] !== undefined ? proItem.enums : ["Undefined Enums !!!"]), + }) + } break; + case 'object': { + var component = Qt.createComponent("PropItem.qml"); + component.createObject(propItemsContainer, { + "label": proItem.property, + "object": proItem.object, + "property": proItem.property, + }) + } break; + case 'printLabel': { + var component = Qt.createComponent("PropItem.qml"); + component.createObject(propItemsContainer, { + "label": proItem.property + }) + } break; + } + root.hasContent = true + } else { + console.log('Invalid property: ' + JSON.stringify(proItem)); + } + } else if (proItem['type'] === 'printLabel') { + var component = Qt.createComponent("PropItem.qml"); + component.createObject(propItemsContainer, { + "label": proItem.label + }) + } else { + console.log('Invalid object: ' + JSON.stringify(proItem)); + } + } + } + Component.onCompleted: { + } +} diff --git a/scripts/developer/utilities/lib/prop/PropItem.qml b/scripts/developer/utilities/lib/prop/PropItem.qml new file mode 100644 index 0000000000..339ff10422 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropItem.qml @@ -0,0 +1,63 @@ +// +// PropItem.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +Item { + Global { id: global } + + id: root + + // Prop item is designed to author an object[property]: + property var object: {} + property string property: "" + + // value is accessed through the "valueVarSetter" and "valueVarGetter" + // By default, these just go get or set the value from the object[property] + // + function defaultGet() { return root.object[root.property]; } + function defaultSet(value) { root.object[root.property] = value; } + property var valueVarSetter: defaultSet + property var valueVarGetter: defaultGet + + // PropItem is stretching horizontally accross its parent + // Fixed height + anchors.left: parent.left + anchors.right: parent.right + height: global.lineHeight + anchors.leftMargin: global.horizontalMargin + anchors.rightMargin: global.horizontalMargin + + // LabelControl And SplitterControl are on the left side of the PropItem + property bool showLabel: true + property alias labelControl: labelControl + property alias label: labelControl.text + + property var labelAreaWidth: root.width * global.splitterRightWidthScale - global.splitterWidth + + PropText { + id: labelControl + text: root.label + enabled: root.showLabel + + anchors.left: root.left + anchors.verticalCenter: root.verticalCenter + width: labelAreaWidth + } + + property alias splitter: splitterControl + PropSplitter { + id: splitterControl + + anchors.left: labelControl.right + size: global.splitterWidth + } + +} diff --git a/scripts/developer/utilities/lib/prop/PropScalar.qml b/scripts/developer/utilities/lib/prop/PropScalar.qml new file mode 100644 index 0000000000..684dd4fed4 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropScalar.qml @@ -0,0 +1,70 @@ +// +// PropItem.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +import controlsUit 1.0 as HifiControls + +PropItem { + Global { id: global } + id: root + + // Scalar Prop + property bool integral: false + property var numDigits: 2 + + + property alias valueVar : sliderControl.value + property alias min: sliderControl.minimumValue + property alias max: sliderControl.maximumValue + + + + property bool showValue: true + + + signal valueChanged(real value) + + Component.onCompleted: { + valueVar = root.valueVarGetter(); + } + + PropLabel { + id: valueLabel + enabled: root.showValue + + anchors.left: root.splitter.right + anchors.verticalCenter: root.verticalCenter + width: root.width * global.valueAreaWidthScale + horizontalAlignment: global.valueTextAlign + height: global.slimHeight + + text: sliderControl.value.toFixed(root.integral ? 0 : root.numDigits) + + background: Rectangle { + color: global.color + border.color: global.colorBorderLight + border.width: global.valueBorderWidth + radius: global.valueBorderRadius + } + } + + HifiControls.Slider { + id: sliderControl + stepSize: root.integral ? 1.0 : 0.0 + anchors.left: valueLabel.right + anchors.right: root.right + anchors.verticalCenter: root.verticalCenter + + onValueChanged: { root.valueVarSetter(value) } + } + + +} diff --git a/scripts/developer/utilities/lib/prop/qmldir b/scripts/developer/utilities/lib/prop/qmldir new file mode 100644 index 0000000000..c67ab6a41a --- /dev/null +++ b/scripts/developer/utilities/lib/prop/qmldir @@ -0,0 +1,14 @@ +Module Prop +Global 1.0 style/Global.qml +PropText 1.0 style/PiText.qml +PropLabel 1.0 style/PiLabel.qml +PropSplitter 1.0 style/PiSplitter.qml +PropComboBox 1.0 style/PiComboBox.qml +PropCanvasIcon 1.0 style/PiCanvasIcon.qml +PropCheckBox 1.0 style/PiCheckBox.qml +PropFolderPanel 1.0 style/PiFolderPanel.qml + +PropItem 1.0 PropItem.qml +PropScalar 1.0 PropScalar.qml +PropEnum 1.0 PropEnum.qml +PropColor 1.0 PropColor.qml diff --git a/scripts/developer/utilities/lib/prop/style/Global.qml b/scripts/developer/utilities/lib/prop/style/Global.qml new file mode 100644 index 0000000000..4cdee70244 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/style/Global.qml @@ -0,0 +1,54 @@ +// +// Prop/style/Global.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls + + +Item { + HifiConstants { id: hifi } + id: root + + readonly property real lineHeight: 32 + readonly property real slimHeight: 24 + + readonly property real horizontalMargin: 4 + + readonly property color color: hifi.colors.baseGray + readonly property color colorBackShadow: hifi.colors.baseGrayShadow + readonly property color colorBackHighlight: hifi.colors.baseGrayHighlight + readonly property color colorBorderLight: hifi.colors.lightGray + readonly property color colorBorderHighight: hifi.colors.blueHighlight + + readonly property color colorOrangeAccent: "#FF6309" + readonly property color colorRedAccent: "#C62147" + readonly property color colorGreenHighlight: "#1ac567" + + readonly property real fontSize: 12 + readonly property var fontFamily: "Raleway" + readonly property var fontWeight: Font.DemiBold + readonly property color fontColor: hifi.colors.faintGray + + readonly property var splitterRightWidthScale: 0.45 + readonly property real splitterWidth: 8 + + readonly property real iconWidth: fontSize + readonly property real iconHeight: fontSize + + readonly property var labelTextAlign: Text.AlignRight + readonly property var labelTextElide: Text.ElideMiddle + + readonly property var valueAreaWidthScale: 0.3 * (1.0 - splitterRightWidthScale) + readonly property var valueTextAlign: Text.AlignHCenter + readonly property real valueBorderWidth: 1 + readonly property real valueBorderRadius: 2 +} \ No newline at end of file diff --git a/scripts/developer/utilities/lib/prop/style/PiCanvasIcon.qml b/scripts/developer/utilities/lib/prop/style/PiCanvasIcon.qml new file mode 100644 index 0000000000..2fc3ed10ec --- /dev/null +++ b/scripts/developer/utilities/lib/prop/style/PiCanvasIcon.qml @@ -0,0 +1,99 @@ +// +// Prop/style/PiFoldCanvas.qml +// +// Created by Sam Gateau on 3/9/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + + import QtQuick 2.7 +import QtQuick.Controls 2.2 + +Canvas { + Global { id: global } + + width: global.iconWidth + height: global.iconHeight + + property var icon: 0 + + onIconChanged: function () { //console.log("Icon changed to: " + icon ); + requestPaint() + } + + property var filled: true + onFilledChanged: function () { //console.log("Filled changed to: " + filled ); + requestPaint() + } + + property var fillColor: global.colorBorderHighight + onFillColorChanged: function () { //console.log("fillColor changed to: " + filled ); + requestPaint() + } + + property alias iconMouseArea: mousearea + + contextType: "2d" + onPaint: { + context.reset(); + switch (icon) { + case false: + case 0:{ // Right Arrow + context.moveTo(width * 0.25, 0); + context.lineTo(width * 0.25, height); + context.lineTo(width, height / 2); + context.closePath(); + } break; + case true: + case 1:{ // Up Arrow + context.moveTo(0, height * 0.25); + context.lineTo(width, height * 0.25); + context.lineTo(width / 2, height); + context.closePath(); + } break; + case 2:{ // Down Arrow + context.moveTo(0, height * 0.75); + context.lineTo(width, height* 0.75); + context.lineTo(width / 2, 0); + context.closePath(); + } break; + case 3:{ // Left Arrow + context.moveTo(width * 0.75, 0); + context.lineTo(width * 0.75, height); + context.lineTo(0, height / 2); + context.closePath(); + } break; + case 4:{ // Square + context.moveTo(0,0); + context.lineTo(0, height); + context.lineTo(width, height); + context.lineTo(width, 0); + context.closePath(); + } break; + case 5:{ // Small Square + context.moveTo(width * 0.25, height * 0.25); + context.lineTo(width * 0.25, height * 0.75); + context.lineTo(width * 0.75, height * 0.75); + context.lineTo(width * 0.75, height * 0.25); + context.closePath(); + } break; + default: { + + } + } + if (filled) { + context.fillStyle = fillColor; + context.fill(); + } else { + context.strokeStyle = fillColor; + context.stroke(); + } + } + + MouseArea{ + id: mousearea + anchors.fill: parent + } +} \ No newline at end of file diff --git a/scripts/developer/utilities/lib/prop/style/PiCheckBox.qml b/scripts/developer/utilities/lib/prop/style/PiCheckBox.qml new file mode 100644 index 0000000000..1e1f03669b --- /dev/null +++ b/scripts/developer/utilities/lib/prop/style/PiCheckBox.qml @@ -0,0 +1,23 @@ +// +// Prop/style/PiCheckBox.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import controlsUit 1.0 as HifiControls + +HifiControls.CheckBox { + Global { id: global } + + color: global.fontColor + + //anchors.left: root.splitter.right + //anchors.verticalCenter: root.verticalCenter + //width: root.width * global.valueAreaWidthScale + height: global.slimHeight +} \ No newline at end of file diff --git a/scripts/developer/utilities/lib/prop/style/PiComboBox.qml b/scripts/developer/utilities/lib/prop/style/PiComboBox.qml new file mode 100644 index 0000000000..d9e029b702 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/style/PiComboBox.qml @@ -0,0 +1,86 @@ +// +// Prop/style/PiComboBox.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import QtQuick.Controls 2.2 + + +ComboBox { + id: valueCombo + + height: global.slimHeight + + + // look + flat: true + delegate: ItemDelegate { + width: valueCombo.width + height: valueCombo.height + contentItem: PiText { + text: modelData + horizontalAlignment: global.valueTextAlign + } + background: Rectangle { + color:highlighted?global.colorBackHighlight:global.color; + } + highlighted: valueCombo.highlightedIndex === index + } + + indicator: PiCanvasIcon { + id: canvas + x: valueCombo.width - width - valueCombo.rightPadding + y: valueCombo.topPadding + (valueCombo.availableHeight - height) / 2 + + icon: 1 + /*Connections { + target: valueCombo + onPressedChanged: { canvas.icon = control.down + 1 } + }*/ + } + + contentItem: PiText { + leftPadding: 0 + rightPadding: valueCombo.indicator.width + valueCombo.spacing + + text: valueCombo.displayText + horizontalAlignment: global.valueTextAlign + } + + background: Rectangle { + implicitWidth: 120 + implicitHeight: 40 + color: global.color + border.color: valueCombo.popup.visible ? global.colorBorderHighight : global.colorBorderLight + border.width: global.valueBorderWidth + radius: global.valueBorderRadius + } + + popup: Popup { + y: valueCombo.height - 1 + width: valueCombo.width + implicitHeight: contentItem.implicitHeight + 2 + padding: 1 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: valueCombo.popup.visible ? valueCombo.delegateModel : null + currentIndex: valueCombo.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator { } + } + + background: Rectangle { + color: global.color + border.color: global.colorBorderHighight + radius: global.valueBorderRadius + } + } +} diff --git a/scripts/developer/utilities/lib/prop/style/PiFolderPanel.qml b/scripts/developer/utilities/lib/prop/style/PiFolderPanel.qml new file mode 100644 index 0000000000..d66c0708d3 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/style/PiFolderPanel.qml @@ -0,0 +1,123 @@ +// +// Prop/style/PiFoldedPanel.qml +// +// Created by Sam Gateau on 4/17/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +Item { + Global { id: global } + id: root + + property var label: "panel" + + property alias isUnfold: headerFolderIcon.icon + property alias hasContent: headerFolderIcon.filled + property var indentDepth: 0 + + // Panel Header Data Component + property Component panelHeaderData: defaultPanelHeaderData + Component { // default is a Label + id: defaultPanelHeaderData + PiLabel { + text: root.label + horizontalAlignment: Text.AlignHCenter + } + } + + // Panel Frame Data Component + property Component panelFrameData: defaultPanelFrameData + Component { // default is a column + id: defaultPanelFrameData + Column { + } + } + + property alias panelFrameContent: _panelFrameData.item + + // Header Item + Rectangle { + id: header + height: global.slimHeight + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 0 + + color: global.colorBackShadow // header of group is darker + + // First in the header, some indentation spacer + Item { + id: indentSpacer + width: (headerFolderIcon.width * root.indentDepth) + global.horizontalMargin // Must be non-zero + height: parent.height + + anchors.verticalCenter: parent.verticalCenter + } + + // Second, the folder button / indicator + Item { + id: headerFolder + anchors.left: indentSpacer.right + width: headerFolderIcon.width * 2 + anchors.verticalCenter: header.verticalCenter + height: parent.height + + PiCanvasIcon { + id: headerFolderIcon + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + fillColor: global.colorOrangeAccent + iconMouseArea.onClicked: { root.isUnfold = !root.isUnfold } + } + } + + // Next the header container + // by default containing a Label showing the root.label + Loader { + sourceComponent: panelHeaderData + anchors.left: headerFolder.right + anchors.right: header.right + anchors.rightMargin: global.horizontalMargin * 2 + anchors.verticalCenter: header.verticalCenter + height: parent.height + } + } + + // The Panel container + Rectangle { + id: frame + visible: root.isUnfold + + color: "transparent" + border.color: global.colorBorderLight + border.width: global.valueBorderWidth + radius: global.valueBorderRadius + + anchors.margins: 0 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + anchors.bottom: root.bottom + + // Next the panel frame data + Loader { + id: _panelFrameData + sourceComponent: panelFrameData + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 0 + anchors.rightMargin: 0 + clip: true + } + } + + height: header.height + isUnfold * _panelFrameData.item.height + anchors.margins: 0 + anchors.left: parent.left + anchors.right: parent.right +} \ No newline at end of file diff --git a/scripts/developer/utilities/lib/prop/style/PiLabel.qml b/scripts/developer/utilities/lib/prop/style/PiLabel.qml new file mode 100644 index 0000000000..fac9397004 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/style/PiLabel.qml @@ -0,0 +1,25 @@ +// +// Prop/style/PsLabel.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import QtQuick.Controls 2.2 + +Label { + Global { id: global } + + color: global.fontColor + font.pixelSize: global.fontSize + font.family: global.fontFamily + font.weight: global.fontWeight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: global.labelTextAlign + elide: global.labelTextElide +} + diff --git a/scripts/developer/utilities/lib/prop/style/PiSplitter.qml b/scripts/developer/utilities/lib/prop/style/PiSplitter.qml new file mode 100644 index 0000000000..360860d56f --- /dev/null +++ b/scripts/developer/utilities/lib/prop/style/PiSplitter.qml @@ -0,0 +1,21 @@ +// +// Prop/style/Splitter.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +Item { + id: root + property real size + + width: size // Must be non-zero + height: size + + anchors.verticalCenter: parent.verticalCenter +} diff --git a/scripts/developer/utilities/lib/prop/style/PiText.qml b/scripts/developer/utilities/lib/prop/style/PiText.qml new file mode 100644 index 0000000000..a3d85e2354 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/style/PiText.qml @@ -0,0 +1,24 @@ +// +// Prop/style/PsText.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +Text { + Global { id: global } + + color: global.fontColor + font.pixelSize: global.fontSize + font.family: global.fontFamily + font.weight: global.fontWeight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: global.labelTextAlign + elide: global.labelTextElide +} + diff --git a/scripts/developer/utilities/render/antialiasing.js b/scripts/developer/utilities/render/antialiasing.js index e915d75e93..5484565b36 100644 --- a/scripts/developer/utilities/render/antialiasing.js +++ b/scripts/developer/utilities/render/antialiasing.js @@ -13,7 +13,7 @@ (function() { var TABLET_BUTTON_NAME = "TAA"; - var QMLAPP_URL = Script.resolvePath("./antialiasing.qml"); + var QMLAPP_URL = Script.resolvePath("./luci/Antialiasing.qml"); var onLuciScreen = false; diff --git a/scripts/developer/utilities/render/antialiasing.qml b/scripts/developer/utilities/render/antialiasing.qml deleted file mode 100644 index 5abfd30935..0000000000 --- a/scripts/developer/utilities/render/antialiasing.qml +++ /dev/null @@ -1,182 +0,0 @@ -// -// Antialiasing.qml -// -// Created by Sam Gateau on 8/14/2017 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.7 -import QtQuick.Controls 1.4 -import QtQuick.Layouts 1.3 - -import stylesUit 1.0 -import controlsUit 1.0 as HifiControls - -import "configSlider" -import "../lib/plotperf" - -Item { -Rectangle { - id: root; - - HifiConstants { id: hifi; } - color: hifi.colors.baseGray; - - Column { - id: antialiasing - spacing: 20 - padding: 10 - - Column{ - spacing: 10 - - Row { - spacing: 10 - id: fxaaOnOff - property bool debugFXAA: false - HifiControls.Button { - function getTheText() { - if (Render.getConfig("RenderMainView.Antialiasing").fxaaOnOff) { - return "FXAA" - } else { - return "TAA" - } - } - text: getTheText() - onClicked: { - var onOff = !Render.getConfig("RenderMainView.Antialiasing").fxaaOnOff; - if (onOff) { - Render.getConfig("RenderMainView.JitterCam").none(); - Render.getConfig("RenderMainView.Antialiasing").fxaaOnOff = true; - } else { - Render.getConfig("RenderMainView.JitterCam").play(); - Render.getConfig("RenderMainView.Antialiasing").fxaaOnOff = false; - } - - } - } - } - Separator {} - Row { - spacing: 10 - - HifiControls.Button { - text: { - var state = 2 - (Render.getConfig("RenderMainView.JitterCam").freeze * 1 - Render.getConfig("RenderMainView.JitterCam").stop * 2); - if (state === 2) { - return "Jitter" - } else if (state === 1) { - return "Paused at " + Render.getConfig("RenderMainView.JitterCam").index + "" - } else { - return "No Jitter" - } - } - onClicked: { Render.getConfig("RenderMainView.JitterCam").cycleStopPauseRun(); } - } - HifiControls.Button { - text: "<" - onClicked: { Render.getConfig("RenderMainView.JitterCam").prev(); } - } - HifiControls.Button { - text: ">" - onClicked: { Render.getConfig("RenderMainView.JitterCam").next(); } - } - } - Separator {} - HifiControls.CheckBox { - boxSize: 20 - text: "Constrain color" - checked: Render.getConfig("RenderMainView.Antialiasing")["constrainColor"] - onCheckedChanged: { Render.getConfig("RenderMainView.Antialiasing")["constrainColor"] = checked } - } - ConfigSlider { - label: qsTr("Covariance gamma") - integral: false - config: Render.getConfig("RenderMainView.Antialiasing") - property: "covarianceGamma" - max: 1.5 - min: 0.5 - height: 38 - } - Separator {} - HifiControls.CheckBox { - boxSize: 20 - text: "Feedback history color" - checked: Render.getConfig("RenderMainView.Antialiasing")["feedbackColor"] - onCheckedChanged: { Render.getConfig("RenderMainView.Antialiasing")["feedbackColor"] = checked } - } - - ConfigSlider { - label: qsTr("Source blend") - integral: false - config: Render.getConfig("RenderMainView.Antialiasing") - property: "blend" - max: 1.0 - min: 0.0 - height: 38 - } - - ConfigSlider { - label: qsTr("Post sharpen") - integral: false - config: Render.getConfig("RenderMainView.Antialiasing") - property: "sharpen" - max: 1.0 - min: 0.0 - } - Separator {} - Row { - - spacing: 10 - HifiControls.CheckBox { - boxSize: 20 - text: "Debug" - checked: Render.getConfig("RenderMainView.Antialiasing")["debug"] - onCheckedChanged: { Render.getConfig("RenderMainView.Antialiasing")["debug"] = checked } - } - HifiControls.CheckBox { - boxSize: 20 - text: "Show Debug Cursor" - checked: Render.getConfig("RenderMainView.Antialiasing")["showCursorPixel"] - onCheckedChanged: { Render.getConfig("RenderMainView.Antialiasing")["showCursorPixel"] = checked } - } - } - ConfigSlider { - label: qsTr("Debug Region <") - integral: false - config: Render.getConfig("RenderMainView.Antialiasing") - property: "debugX" - max: 1.0 - min: 0.0 - } - HifiControls.CheckBox { - boxSize: 20 - text: "Closest Fragment" - checked: Render.getConfig("RenderMainView.Antialiasing")["showClosestFragment"] - onCheckedChanged: { Render.getConfig("RenderMainView.Antialiasing")["showClosestFragment"] = checked } - } - ConfigSlider { - label: qsTr("Debug Velocity Threshold [pix]") - integral: false - config: Render.getConfig("RenderMainView.Antialiasing") - property: "debugShowVelocityThreshold" - max: 50 - min: 0.0 - height: 38 - } - ConfigSlider { - label: qsTr("Debug Orb Zoom") - integral: false - config: Render.getConfig("RenderMainView.Antialiasing") - property: "debugOrbZoom" - max: 32.0 - min: 1.0 - height: 38 - } - } - } -} -} diff --git a/scripts/developer/utilities/render/debugCulling.js b/scripts/developer/utilities/render/debugCulling.js index 788c7cb4a0..e7c68fd717 100644 --- a/scripts/developer/utilities/render/debugCulling.js +++ b/scripts/developer/utilities/render/debugCulling.js @@ -10,7 +10,7 @@ // // Set up the qml ui -var qml = Script.resolvePath('culling.qml'); +var qml = Script.resolvePath('luci/Culling.qml'); var window = new OverlayWindow({ title: 'Render Draws', source: qml, diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index d147585212..80ca8b09e1 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -14,6 +14,7 @@ import QtQuick.Layouts 1.3 import stylesUit 1.0 import controlsUit 1.0 as HifiControls import "configSlider" +import "luci" Rectangle { HifiConstants { id: hifi;} @@ -28,125 +29,16 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.margins: hifi.dimensions.contentMargin.x - //padding: hifi.dimensions.contentMargin.x + + HifiControls.Label { text: "Shading" } - Row { - anchors.left: parent.left - anchors.right: parent.right - - spacing: 5 - Column { - spacing: 5 - // padding: 10 - Repeater { - model: [ - "Unlit:LightingModel:enableUnlit", - "Emissive:LightingModel:enableEmissive", - "Lightmap:LightingModel:enableLightmap", - "Background:LightingModel:enableBackground", - "Haze:LightingModel:enableHaze", - "AO:LightingModel:enableAmbientOcclusion", - "Textures:LightingModel:enableMaterialTexturing" - ] - HifiControls.CheckBox { - boxSize: 20 - text: modelData.split(":")[0] - checked: render.mainViewTask.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] - onCheckedChanged: { render.mainViewTask.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } - } - } - } + ShadingModel {} + Separator {} + ToneMapping {} - Column { - spacing: 5 - Repeater { - model: [ - "Obscurance:LightingModel:enableObscurance", - "Scattering:LightingModel:enableScattering", - "Diffuse:LightingModel:enableDiffuse", - "Specular:LightingModel:enableSpecular", - "Albedo:LightingModel:enableAlbedo", - "Wireframe:LightingModel:enableWireframe", - "Skinning:LightingModel:enableSkinning", - "Blendshape:LightingModel:enableBlendshape" - ] - HifiControls.CheckBox { - boxSize: 20 - text: modelData.split(":")[0] - checked: render.mainViewTask.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] - onCheckedChanged: { render.mainViewTask.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } - } - } - } - - Column { - spacing: 5 - Repeater { - model: [ - "Ambient:LightingModel:enableAmbientLight", - "Directional:LightingModel:enableDirectionalLight", - "Point:LightingModel:enablePointLight", - "Spot:LightingModel:enableSpotLight", - "Light Contour:LightingModel:showLightContour", - "Zone Stack:DrawZoneStack:enabled", - "Shadow:LightingModel:enableShadow" - ] - HifiControls.CheckBox { - boxSize: 20 - text: modelData.split(":")[0] - checked: render.mainViewTask.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] - onCheckedChanged: { render.mainViewTask.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } - } - } - } - } - Separator {} - Column { - anchors.left: parent.left - anchors.right: parent.right - spacing: 5 - Repeater { - model: [ "Tone Mapping Exposure:ToneMapping:exposure:5.0:-5.0" - ] - ConfigSlider { - label: qsTr(modelData.split(":")[0]) - integral: false - config: render.mainViewTask.getConfig(modelData.split(":")[1]) - property: modelData.split(":")[2] - max: modelData.split(":")[3] - min: modelData.split(":")[4] - - anchors.left: parent.left - anchors.right: parent.right - } - } - Item { - height: childrenRect.height - anchors.left: parent.left - anchors.right: parent.right - - HifiControls.Label { - text: "Tone Mapping Curve" - anchors.left: parent.left - } - - ComboBox { - anchors.right: parent.right - currentIndex: 1 - model: [ - "RGB", - "SRGB", - "Reinhard", - "Filmic", - ] - width: 200 - onCurrentIndexChanged: { render.mainViewTask.getConfig("ToneMapping")["curve"] = currentIndex; } - } - } - } Separator {} Column { anchors.left: parent.left @@ -169,133 +61,11 @@ Rectangle { } } Separator {} + Framebuffer {} - Item { - height: childrenRect.height - anchors.left: parent.left - anchors.right: parent.right - - id: framebuffer - - HifiControls.Label { - text: "Debug Framebuffer" - anchors.left: parent.left - } - - property var config: render.mainViewTask.getConfig("DebugDeferredBuffer") - - function setDebugMode(mode) { - framebuffer.config.enabled = (mode != 0); - framebuffer.config.mode = mode; - } - - ComboBox { - anchors.right: parent.right - currentIndex: 0 - model: ListModel { - id: cbItemsFramebuffer - ListElement { text: "Off"; color: "Yellow" } - ListElement { text: "Depth"; color: "Green" } - ListElement { text: "Albedo"; color: "Yellow" } - ListElement { text: "Normal"; color: "White" } - ListElement { text: "Roughness"; color: "White" } - ListElement { text: "Metallic"; color: "White" } - ListElement { text: "Emissive"; color: "White" } - ListElement { text: "Unlit"; color: "White" } - ListElement { text: "Occlusion"; color: "White" } - ListElement { text: "Lightmap"; color: "White" } - ListElement { text: "Scattering"; color: "White" } - ListElement { text: "Lighting"; color: "White" } - ListElement { text: "Shadow Cascade 0"; color: "White" } - ListElement { text: "Shadow Cascade 1"; color: "White" } - ListElement { text: "Shadow Cascade 2"; color: "White" } - ListElement { text: "Shadow Cascade 3"; color: "White" } - ListElement { text: "Shadow Cascade Indices"; color: "White" } - ListElement { text: "Linear Depth"; color: "White" } - ListElement { text: "Half Linear Depth"; color: "White" } - ListElement { text: "Half Normal"; color: "White" } - ListElement { text: "Mid Curvature"; color: "White" } - ListElement { text: "Mid Normal"; color: "White" } - ListElement { text: "Low Curvature"; color: "White" } - ListElement { text: "Low Normal"; color: "White" } - ListElement { text: "Curvature Occlusion"; color: "White" } - ListElement { text: "Debug Scattering"; color: "White" } - ListElement { text: "Ambient Occlusion"; color: "White" } - ListElement { text: "Ambient Occlusion Blurred"; color: "White" } - ListElement { text: "Ambient Occlusion Normal"; color: "White" } - ListElement { text: "Velocity"; color: "White" } - ListElement { text: "Custom"; color: "White" } - } - width: 200 - onCurrentIndexChanged: { framebuffer.setDebugMode(currentIndex) } - } - } - Separator {} - Row { - spacing: 5 - Column { - spacing: 5 + BoundingBoxes { - HifiControls.CheckBox { - boxSize: 20 - text: "Opaques" - checked: render.mainViewTask.getConfig("DrawOpaqueBounds")["enabled"] - onCheckedChanged: { render.mainViewTask.getConfig("DrawOpaqueBounds")["enabled"] = checked } - } - HifiControls.CheckBox { - boxSize: 20 - text: "Transparents" - checked: render.mainViewTask.getConfig("DrawTransparentBounds")["enabled"] - onCheckedChanged: { render.mainViewTask.getConfig("DrawTransparentBounds")["enabled"] = checked } - } - HifiControls.CheckBox { - boxSize: 20 - text: "Opaques in Front" - checked: render.mainViewTask.getConfig("DrawOverlayInFrontOpaqueBounds")["enabled"] - onCheckedChanged: { render.mainViewTask.getConfig("DrawOverlayInFrontOpaqueBounds")["enabled"] = checked } - } - HifiControls.CheckBox { - boxSize: 20 - text: "Transparents in Front" - checked: render.mainViewTask.getConfig("DrawOverlayInFrontTransparentBounds")["enabled"] - onCheckedChanged: { render.mainViewTask.getConfig("DrawOverlayInFrontTransparentBounds")["enabled"] = checked } - } - HifiControls.CheckBox { - boxSize: 20 - text: "Opaques in HUD" - checked: render.mainViewTask.getConfig("DrawOverlayHUDOpaqueBounds")["enabled"] - onCheckedChanged: { render.mainViewTask.getConfig("DrawOverlayHUDOpaqueBounds")["enabled"] = checked } - } - - } - Column { - spacing: 5 - HifiControls.CheckBox { - boxSize: 20 - text: "Metas" - checked: render.mainViewTask.getConfig("DrawMetaBounds")["enabled"] - onCheckedChanged: { render.mainViewTask.getConfig("DrawMetaBounds")["enabled"] = checked } - } - HifiControls.CheckBox { - boxSize: 20 - text: "Lights" - checked: render.mainViewTask.getConfig("DrawLightBounds")["enabled"] - onCheckedChanged: { render.mainViewTask.getConfig("DrawLightBounds")["enabled"] = checked; } - } - HifiControls.CheckBox { - boxSize: 20 - text: "Zones" - checked: render.mainViewTask.getConfig("DrawZones")["enabled"] - onCheckedChanged: { render.mainViewTask.getConfig("ZoneRenderer")["enabled"] = checked; render.mainViewTask.getConfig("DrawZones")["enabled"] = checked; } - } - HifiControls.CheckBox { - boxSize: 20 - text: "Transparents in HUD" - checked: render.mainViewTask.getConfig("DrawOverlayHUDTransparentBounds")["enabled"] - onCheckedChanged: { render.mainViewTask.getConfig("DrawOverlayHUDTransparentBounds")["enabled"] = checked } - } - } } Separator {} Row { diff --git a/scripts/developer/utilities/render/luci.js b/scripts/developer/utilities/render/luci.js index bae5c4646d..fd84f55e65 100644 --- a/scripts/developer/utilities/render/luci.js +++ b/scripts/developer/utilities/render/luci.js @@ -10,10 +10,13 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // + + (function() { var AppUi = Script.require('appUi'); var MaterialInspector = Script.require('./materialInspector.js'); + var Page = Script.require('./luci/Page.js'); var moveDebugCursor = false; var onMousePressEvent = function (e) { @@ -43,83 +46,12 @@ Render.getConfig("RenderMainView").getConfig("DebugDeferredBuffer").size = { x: nx, y: ny, z: 1.0, w: 1.0 }; } - function Page(title, qmlurl, width, height, handleWindowFunc) { - this.title = title; - this.qml = qmlurl; - this.width = width; - this.height = height; - this.handleWindowFunc = handleWindowFunc; - this.window; - - print("Page: New Page:" + JSON.stringify(this)); - } - - Page.prototype.killView = function () { - print("Page: Kill window for page:" + JSON.stringify(this)); - if (this.window) { - print("Page: Kill window for page:" + this.title); - //this.window.closed.disconnect(function () { - // this.killView(); - //}); - this.window.close(); - this.window = false; - } - }; - - Page.prototype.createView = function () { - var that = this; - if (!this.window) { - print("Page: New window for page:" + this.title); - this.window = Desktop.createWindow(Script.resolvePath(this.qml), { - title: this.title, - presentationMode: Desktop.PresentationMode.NATIVE, - size: {x: this.width, y: this.height} - }); - this.handleWindowFunc(this.window); - this.window.closed.connect(function () { - that.killView(); - this.handleWindowFunc(undefined); - }); - } - }; - - - var Pages = function () { - this._pages = {}; - }; - - Pages.prototype.addPage = function (command, title, qmlurl, width, height, handleWindowFunc) { - if (handleWindowFunc === undefined) { - // Workaround for bad linter - handleWindowFunc = function(window){}; - } - this._pages[command] = new Page(title, qmlurl, width, height, handleWindowFunc); - }; - - Pages.prototype.open = function (command) { - print("Pages: command = " + command); - if (!this._pages[command]) { - print("Pages: unknown command = " + command); - return; - } - this._pages[command].createView(); - }; - - Pages.prototype.clear = function () { - for (var p in this._pages) { - print("Pages: kill page: " + p); - this._pages[p].killView(); - delete this._pages[p]; - } - this._pages = {}; - }; var pages = new Pages(); - pages.addPage('openEngineView', 'Render Engine', 'engineInspector.qml', 300, 400); - pages.addPage('openEngineLODView', 'Render LOD', 'lod.qml', 300, 400); - pages.addPage('openCullInspectorView', 'Cull Inspector', 'culling.qml', 300, 400); - pages.addPage('openMaterialInspectorView', 'Material Inspector', 'materialInspector.qml', 300, 400, MaterialInspector.setWindow); + pages.addPage('openEngineLODView', 'Render LOD', '../lod.qml', 300, 400); + pages.addPage('openCullInspectorView', 'Cull Inspector', '../luci/Culling.qml', 300, 400); + pages.addPage('openMaterialInspectorView', 'Material Inspector', '../materialInspector.qml', 300, 400, MaterialInspector.setWindow); function fromQml(message) { if (pages.open(message.method)) { @@ -132,7 +64,7 @@ ui = new AppUi({ buttonName: "LUCI", home: Script.resolvePath("deferredLighting.qml"), - additionalAppScreens: Script.resolvePath("engineInspector.qml"), + additionalAppScreens : Script.resolvePath("engineInspector.qml"), onMessage: fromQml, normalButton: Script.resolvePath("../../../system/assets/images/luci-i.svg"), activeButton: Script.resolvePath("../../../system/assets/images/luci-a.svg") @@ -144,8 +76,5 @@ Controller.mouseReleaseEvent.disconnect(onMouseReleaseEvent); Controller.mouseMoveEvent.disconnect(onMouseMoveEvent); pages.clear(); - // killEngineInspectorView(); - // killCullInspectorView(); - // killEngineLODWindow(); }); }()); diff --git a/scripts/developer/utilities/render/luci.qml b/scripts/developer/utilities/render/luci.qml new file mode 100644 index 0000000000..a904ec52fc --- /dev/null +++ b/scripts/developer/utilities/render/luci.qml @@ -0,0 +1,100 @@ +// +// luci.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + +import controlsUit 1.0 as HifiControls + +import "../lib/prop" as Prop +import "../lib/jet/qml" as Jet +import "luci" + +Rectangle { + anchors.fill: parent + id: render; + property var mainViewTask: Render.getConfig("RenderMainView") + + Prop.Global { id: global;} + color: global.color + + ScrollView { + id: control + anchors.fill: parent + clip: true + + Column { + width: render.width + Prop.PropFolderPanel { + label: "Shading Model" + panelFrameData: Component { + ShadingModel {} + } + } + Prop.PropFolderPanel { + label: "Bounding Boxes" + panelFrameData: Component { + BoundingBoxes {} + } + } + Prop.PropFolderPanel { + label: "Framebuffer" + panelFrameData: Component { + Framebuffer {} + } + } + Prop.PropFolderPanel { + label: "Tone Mapping" + panelFrameData: Component { + ToneMapping {} + } + } + Prop.PropFolderPanel { + label: "Antialiasing" + panelFrameData: Component { + Antialiasing {} + } + } + Prop.PropFolderPanel { + label: "Culling" + panelFrameData: Component { + Culling {} + } + } + Prop.PropFolderPanel { + label: "Tools" + panelFrameData: Component { + Row { + HifiControls.Button { + text: "LOD" + onClicked: { + sendToScript({method: "openEngineLODView"}); + } + } + HifiControls.Button { + text: "Material" + onClicked: { + sendToScript({method: "openMaterialInspectorView"}); + } + } + } + } + } + Jet.TaskPropView { + id: "le" + jobPath: "" + label: "Le Render Engine" + + // anchors.left: parent.left + // anchors.right: parent.right + } + } + } +} \ No newline at end of file diff --git a/scripts/developer/utilities/render/luci/Antialiasing.qml b/scripts/developer/utilities/render/luci/Antialiasing.qml new file mode 100644 index 0000000000..e29bca43eb --- /dev/null +++ b/scripts/developer/utilities/render/luci/Antialiasing.qml @@ -0,0 +1,176 @@ +// +// Antialiasing.qml +// +// Created by Sam Gateau on 8/14/2017 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.3 + +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls + +import "../configSlider" +import "../../lib/plotperf" + + +Column{ + HifiConstants { id: hifi; } + + id: antialiasing + padding: 10 + anchors.left: parent.left + anchors.right: parent.right + + spacing: 10 + + Row { + spacing: 10 + id: fxaaOnOff + property bool debugFXAA: false + HifiControls.Button { + function getTheText() { + if (Render.getConfig("RenderMainView.Antialiasing").fxaaOnOff) { + return "FXAA" + } else { + return "TAA" + } + } + text: getTheText() + onClicked: { + var onOff = !Render.getConfig("RenderMainView.Antialiasing").fxaaOnOff; + if (onOff) { + Render.getConfig("RenderMainView.JitterCam").none(); + Render.getConfig("RenderMainView.Antialiasing").fxaaOnOff = true; + } else { + Render.getConfig("RenderMainView.JitterCam").play(); + Render.getConfig("RenderMainView.Antialiasing").fxaaOnOff = false; + } + + } + } + } + Separator {} + Row { + spacing: 10 + + HifiControls.Button { + text: { + var state = 2 - (Render.getConfig("RenderMainView.JitterCam").freeze * 1 - Render.getConfig("RenderMainView.JitterCam").stop * 2); + if (state === 2) { + return "Jitter" + } else if (state === 1) { + return "Paused at " + Render.getConfig("RenderMainView.JitterCam").index + "" + } else { + return "No Jitter" + } + } + onClicked: { Render.getConfig("RenderMainView.JitterCam").cycleStopPauseRun(); } + } + HifiControls.Button { + text: "<" + onClicked: { Render.getConfig("RenderMainView.JitterCam").prev(); } + } + HifiControls.Button { + text: ">" + onClicked: { Render.getConfig("RenderMainView.JitterCam").next(); } + } + } + Separator {} + HifiControls.CheckBox { + boxSize: 20 + text: "Constrain color" + checked: Render.getConfig("RenderMainView.Antialiasing")["constrainColor"] + onCheckedChanged: { Render.getConfig("RenderMainView.Antialiasing")["constrainColor"] = checked } + } + ConfigSlider { + label: qsTr("Covariance gamma") + integral: false + config: Render.getConfig("RenderMainView.Antialiasing") + property: "covarianceGamma" + max: 1.5 + min: 0.5 + height: 38 + } + Separator {} + HifiControls.CheckBox { + boxSize: 20 + text: "Feedback history color" + checked: Render.getConfig("RenderMainView.Antialiasing")["feedbackColor"] + onCheckedChanged: { Render.getConfig("RenderMainView.Antialiasing")["feedbackColor"] = checked } + } + + ConfigSlider { + label: qsTr("Source blend") + integral: false + config: Render.getConfig("RenderMainView.Antialiasing") + property: "blend" + max: 1.0 + min: 0.0 + height: 38 + } + + ConfigSlider { + label: qsTr("Post sharpen") + integral: false + config: Render.getConfig("RenderMainView.Antialiasing") + property: "sharpen" + max: 1.0 + min: 0.0 + } + Separator {} + Row { + + spacing: 10 + HifiControls.CheckBox { + boxSize: 20 + text: "Debug" + checked: Render.getConfig("RenderMainView.Antialiasing")["debug"] + onCheckedChanged: { Render.getConfig("RenderMainView.Antialiasing")["debug"] = checked } + } + HifiControls.CheckBox { + boxSize: 20 + text: "Show Debug Cursor" + checked: Render.getConfig("RenderMainView.Antialiasing")["showCursorPixel"] + onCheckedChanged: { Render.getConfig("RenderMainView.Antialiasing")["showCursorPixel"] = checked } + } + } + ConfigSlider { + label: qsTr("Debug Region <") + integral: false + config: Render.getConfig("RenderMainView.Antialiasing") + property: "debugX" + max: 1.0 + min: 0.0 + } + HifiControls.CheckBox { + boxSize: 20 + text: "Closest Fragment" + checked: Render.getConfig("RenderMainView.Antialiasing")["showClosestFragment"] + onCheckedChanged: { Render.getConfig("RenderMainView.Antialiasing")["showClosestFragment"] = checked } + } + ConfigSlider { + label: qsTr("Debug Velocity Threshold [pix]") + integral: false + config: Render.getConfig("RenderMainView.Antialiasing") + property: "debugShowVelocityThreshold" + max: 50 + min: 0.0 + height: 38 + } + ConfigSlider { + label: qsTr("Debug Orb Zoom") + integral: false + config: Render.getConfig("RenderMainView.Antialiasing") + property: "debugOrbZoom" + max: 32.0 + min: 1.0 + height: 38 + } +} + diff --git a/scripts/developer/utilities/render/luci/BoundingBoxes.qml b/scripts/developer/utilities/render/luci/BoundingBoxes.qml new file mode 100644 index 0000000000..636267729c --- /dev/null +++ b/scripts/developer/utilities/render/luci/BoundingBoxes.qml @@ -0,0 +1,84 @@ +// +// BoundingBoxes.qml +// +// Created by Sam Gateau on 4/18/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +import "../../lib/prop" as Prop + +Column { + + id: root; + + property var mainViewTask: Render.getConfig("RenderMainView") + + spacing: 5 + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: hifi.dimensions.contentMargin.x + + Row { + anchors.left: parent.left + anchors.right: parent.right + + spacing: 5 + Column { + spacing: 5 + + Prop.PropCheckBox { + text: "Opaques" + checked: root.mainViewTask.getConfig("DrawOpaqueBounds")["enabled"] + onCheckedChanged: { root.mainViewTask.getConfig("DrawOpaqueBounds")["enabled"] = checked } + } + Prop.PropCheckBox { + text: "Transparents" + checked: root.mainViewTask.getConfig("DrawTransparentBounds")["enabled"] + onCheckedChanged: { root.mainViewTask.getConfig("DrawTransparentBounds")["enabled"] = checked } + } + Prop.PropCheckBox { + text: "Metas" + checked: root.mainViewTask.getConfig("DrawMetaBounds")["enabled"] + onCheckedChanged: { root.mainViewTask.getConfig("DrawMetaBounds")["enabled"] = checked } + } + Prop.PropCheckBox { + text: "Lights" + checked: root.mainViewTask.getConfig("DrawLightBounds")["enabled"] + onCheckedChanged: { root.mainViewTask.getConfig("DrawLightBounds")["enabled"] = checked; } + } + Prop.PropCheckBox { + text: "Zones" + checked: root.mainViewTask.getConfig("DrawZones")["enabled"] + onCheckedChanged: { root.mainViewTask.getConfig("ZoneRenderer")["enabled"] = checked; root.mainViewTask.getConfig("DrawZones")["enabled"] = checked; } + } + } + Column { + spacing: 5 + Prop.PropCheckBox { + text: "Opaques in Front" + checked: root.mainViewTask.getConfig("DrawOverlayInFrontOpaqueBounds")["enabled"] + onCheckedChanged: { root.mainViewTask.getConfig("DrawOverlayInFrontOpaqueBounds")["enabled"] = checked } + } + Prop.PropCheckBox { + text: "Transparents in Front" + checked: root.mainViewTask.getConfig("DrawOverlayInFrontTransparentBounds")["enabled"] + onCheckedChanged: { root.mainViewTask.getConfig("DrawOverlayInFrontTransparentBounds")["enabled"] = checked } + } + Prop.PropCheckBox { + text: "Opaques in HUD" + checked: root.mainViewTask.getConfig("DrawOverlayHUDOpaqueBounds")["enabled"] + onCheckedChanged: { root.mainViewTask.getConfig("DrawOverlayHUDOpaqueBounds")["enabled"] = checked } + } + Prop.PropCheckBox { + text: "Transparents in HUD" + checked: root.mainViewTask.getConfig("DrawOverlayHUDTransparentBounds")["enabled"] + onCheckedChanged: { root.mainViewTask.getConfig("DrawOverlayHUDTransparentBounds")["enabled"] = checked } + } + } + } +} diff --git a/scripts/developer/utilities/render/culling.qml b/scripts/developer/utilities/render/luci/Culling.qml similarity index 86% rename from scripts/developer/utilities/render/culling.qml rename to scripts/developer/utilities/render/luci/Culling.qml index 801cb5b573..d881ddf7a6 100644 --- a/scripts/developer/utilities/render/culling.qml +++ b/scripts/developer/utilities/render/luci/Culling.qml @@ -9,11 +9,16 @@ // import QtQuick 2.5 import QtQuick.Controls 1.4 -import "configSlider" + +import "../../lib/prop" as Prop Column { id: root spacing: 8 + + anchors.left: parent.left; + anchors.right: parent.right; + property var sceneOctree: Render.getConfig("RenderMainView.DrawSceneOctree"); property var itemSelection: Render.getConfig("RenderMainView.DrawItemSelection"); @@ -36,6 +41,10 @@ Column { GroupBox { title: "Culling" + + anchors.left: parent.left; + anchors.right: parent.right; + Row { spacing: 8 Column { @@ -91,6 +100,7 @@ Column { } } } + } GroupBox { @@ -103,13 +113,14 @@ Column { anchors.right: parent.right; Repeater { model: [ "Opaque:RenderMainView.DrawOpaqueDeferred", "Transparent:RenderMainView.DrawTransparentDeferred", "Light:RenderMainView.DrawLight", - "Opaque Overlays:RenderMainView.DrawOverlay3DOpaque", "Transparent Overlays:RenderMainView.DrawOverlay3DTransparent" ] - ConfigSlider { + "Opaque InFront:RenderMainView.DrawInFrontOpaque", "Transparent InFront:RenderMainView.DrawInFrontTransparent", + "Opaque HUD:RenderMainView.DrawHUDOpaque", "Transparent HUD:RenderMainView.DrawHUDTransparent" ] + Prop.PropScalar { label: qsTr(modelData.split(":")[0]) integral: true - config: Render.getConfig(modelData.split(":")[1]) + object: Render.getConfig(modelData.split(":")[1]) property: "maxDrawn" - max: config.numDrawn + max: object.numDrawn min: -1 } } diff --git a/scripts/developer/utilities/render/luci/Framebuffer.qml b/scripts/developer/utilities/render/luci/Framebuffer.qml new file mode 100644 index 0000000000..89d5e59002 --- /dev/null +++ b/scripts/developer/utilities/render/luci/Framebuffer.qml @@ -0,0 +1,69 @@ +// +// Framebuffer.qml +// +// Created by Sam Gateau on 4/18/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +import "../../lib/prop" as Prop + +Column { + anchors.left: parent.left + anchors.right: parent.right + + id: framebuffer + + property var config: Render.getConfig("RenderMainView.DebugDeferredBuffer") + + function setDebugMode(mode) { + framebuffer.config.enabled = (mode != 0); + framebuffer.config.mode = mode; + } + + Prop.PropEnum { + label: "Debug Buffer" + object: config + property: "mode" + valueVar: 0 + enums: [ + "Off", + "Depth", + "Albedo", + "Normal", + "Roughness", + "Metallic", + "Emissive", + "Unlit", + "Occlusion", + "Lightmap", + "Scattering", + "Lighting", + "Shadow Cascade 0", + "Shadow Cascade 1", + "Shadow Cascade 2", + "Shadow Cascade 3", + "Shadow Cascade Indices", + "Linear Depth", + "Half Linear Depth", + "Half Normal", + "Mid Curvature", + "Mid Normal", + "Low Curvature", + "Low Normal", + "Curvature Occlusion", + "Debug Scattering", + "Ambient Occlusion", + "Ambient Occlusion Blurred", + "Ambient Occlusion Normal", + "Velocity", + "Custom", + ] + + valueVarSetter: function (mode) { framebuffer.setDebugMode(mode) } + } +} \ No newline at end of file diff --git a/scripts/developer/utilities/render/luci/Page.js b/scripts/developer/utilities/render/luci/Page.js new file mode 100644 index 0000000000..06c9704abf --- /dev/null +++ b/scripts/developer/utilities/render/luci/Page.js @@ -0,0 +1,90 @@ +// +// Page.js +// +// Sam Gateau, created on 4/19/2019 +// Copyright 2019 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 +// +"use strict"; + +(function() { +function Page(title, qmlurl, width, height, onViewCreated, onViewClosed) { + this.title = title; + this.qml = qmlurl; + this.width = width; + this.height = height; + this.onViewCreated = onViewCreated; + this.onViewClosed = onViewClosed; + + this.window; + + print("Page: New Page:" + JSON.stringify(this)); +} + +Page.prototype.killView = function () { + print("Page: Kill window for page:" + JSON.stringify(this)); + if (this.window) { + print("Page: Kill window for page:" + this.title); + //this.window.closed.disconnect(function () { + // this.killView(); + //}); + this.window.close(); + this.window = false; + } +}; + +Page.prototype.createView = function () { + var that = this; + if (!this.window) { + print("Page: New window for page:" + this.title); + this.window = Desktop.createWindow(Script.resolvePath(this.qml), { + title: this.title, + presentationMode: Desktop.PresentationMode.NATIVE, + size: {x: this.width, y: this.height} + }); + this.onViewCreated(this.window); + this.window.closed.connect(function () { + that.killView(); + that.onViewClosed(); + }); + } +}; + + +Pages = function () { + this._pages = {}; +}; + +Pages.prototype.addPage = function (command, title, qmlurl, width, height, onViewCreated, onViewClosed) { + if (onViewCreated === undefined) { + // Workaround for bad linter + onViewCreated = function(window) {}; + } + if (onViewClosed === undefined) { + // Workaround for bad linter + onViewClosed = function() {}; + } + this._pages[command] = new Page(title, qmlurl, width, height, onViewCreated, onViewClosed); +}; + +Pages.prototype.open = function (command) { + print("Pages: command = " + command); + if (!this._pages[command]) { + print("Pages: unknown command = " + command); + return; + } + this._pages[command].createView(); +}; + +Pages.prototype.clear = function () { + for (var p in this._pages) { + print("Pages: kill page: " + p); + this._pages[p].killView(); + delete this._pages[p]; + } + this._pages = {}; +}; + +}()); diff --git a/scripts/developer/utilities/render/luci/ShadingModel.qml b/scripts/developer/utilities/render/luci/ShadingModel.qml new file mode 100644 index 0000000000..78ca7f1740 --- /dev/null +++ b/scripts/developer/utilities/render/luci/ShadingModel.qml @@ -0,0 +1,92 @@ +// +// ShadingModel.qml +// +// Created by Sam Gateau on 4/17/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +import "../../lib/prop" as Prop + +Column { + + id: shadingModel; + + property var mainViewTask: Render.getConfig("RenderMainView") + + spacing: 5 + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: hifi.dimensions.contentMargin.x + Row { + anchors.left: parent.left + anchors.right: parent.right + + spacing: 5 + Column { + spacing: 5 + Repeater { + model: [ + "Unlit:LightingModel:enableUnlit", + "Emissive:LightingModel:enableEmissive", + "Lightmap:LightingModel:enableLightmap", + "Background:LightingModel:enableBackground", + "Haze:LightingModel:enableHaze", + "AO:LightingModel:enableAmbientOcclusion", + "Textures:LightingModel:enableMaterialTexturing" + ] + Prop.PropCheckBox { + text: modelData.split(":")[0] + checked: shadingModel.mainViewTask.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] + onCheckedChanged: { shadingModel.mainViewTask.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } + } + } + } + + + Column { + spacing: 5 + Repeater { + model: [ + "Obscurance:LightingModel:enableObscurance", + "Scattering:LightingModel:enableScattering", + "Diffuse:LightingModel:enableDiffuse", + "Specular:LightingModel:enableSpecular", + "Albedo:LightingModel:enableAlbedo", + "Wireframe:LightingModel:enableWireframe", + "Skinning:LightingModel:enableSkinning", + "Blendshape:LightingModel:enableBlendshape" + ] + Prop.PropCheckBox { + text: modelData.split(":")[0] + checked: shadingModel.mainViewTask.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] + onCheckedChanged: { shadingModel.mainViewTask.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } + } + } + } + + Column { + spacing: 5 + Repeater { + model: [ + "Ambient:LightingModel:enableAmbientLight", + "Directional:LightingModel:enableDirectionalLight", + "Point:LightingModel:enablePointLight", + "Spot:LightingModel:enableSpotLight", + "Light Contour:LightingModel:showLightContour", + "Zone Stack:DrawZoneStack:enabled", + "Shadow:LightingModel:enableShadow" + ] + Prop.PropCheckBox { + text: modelData.split(":")[0] + checked: shadingModel.mainViewTask.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] + onCheckedChanged: { shadingModel.mainViewTask.getConfig(modelData.split(":")[1])[modelData.split(":")[2]] = checked } + } + } + } + } +} diff --git a/scripts/developer/utilities/render/luci/ToneMapping.qml b/scripts/developer/utilities/render/luci/ToneMapping.qml new file mode 100644 index 0000000000..a76990e37c --- /dev/null +++ b/scripts/developer/utilities/render/luci/ToneMapping.qml @@ -0,0 +1,40 @@ +// +// ToneMapping.qml +// +// Created by Sam Gateau on 4/17/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +import "../../lib/prop" as Prop + +Column { + anchors.left: parent.left + anchors.right: parent.right + Prop.PropScalar { + label: "Exposure" + object: Render.getConfig("RenderMainView.ToneMapping") + property: "exposure" + min: -4 + max: 4 + anchors.left: parent.left + anchors.right: parent.right + } + Prop.PropEnum { + label: "Tone Curve" + object: Render.getConfig("RenderMainView.ToneMapping") + property: "curve" + enums: [ + "RGB", + "SRGB", + "Reinhard", + "Filmic", + ] + anchors.left: parent.left + anchors.right: parent.right + } +} diff --git a/scripts/developer/utilities/render/luci/qmldir b/scripts/developer/utilities/render/luci/qmldir new file mode 100644 index 0000000000..7a7d6a8ca6 --- /dev/null +++ b/scripts/developer/utilities/render/luci/qmldir @@ -0,0 +1,7 @@ + +ShadingModel 1.0 ShadingModel.qml +ToneMapping 1.0 ToneMapping.qml +BoundingBoxes 1.0 BoundingBoxes.qml +Framebuffer 1.0 Framebuffer.qml +Antialiasing 1.0 Antialiasing.qml +Culling 1.0 Culling.qml \ No newline at end of file diff --git a/scripts/developer/utilities/render/luci2.js b/scripts/developer/utilities/render/luci2.js new file mode 100644 index 0000000000..a34cf88415 --- /dev/null +++ b/scripts/developer/utilities/render/luci2.js @@ -0,0 +1,83 @@ + + +var MaterialInspector = Script.require('./materialInspector.js'); +var Page = Script.require('./luci/Page.js'); + + +function openView() { + //window.closed.connect(function() { Script.stop(); }); + + + var pages = new Pages(); + function fromQml(message) { + if (pages.open(message.method)) { + return; + } + } + + var luciWindow + function openLuciWindow(window) { + if (luciWindow !== undefined) { + activeWindow.fromQml.disconnect(fromQml); + } + if (window !== undefined) { + window.fromQml.connect(fromQml); + } + luciWindow = window; + + + var moveDebugCursor = false; + var onMousePressEvent = function (e) { + if (e.isMiddleButton) { + moveDebugCursor = true; + setDebugCursor(e.x, e.y); + } + }; + Controller.mousePressEvent.connect(onMousePressEvent); + + var onMouseReleaseEvent = function () { + moveDebugCursor = false; + }; + Controller.mouseReleaseEvent.connect(onMouseReleaseEvent); + + var onMouseMoveEvent = function (e) { + if (moveDebugCursor) { + setDebugCursor(e.x, e.y); + } + }; + Controller.mouseMoveEvent.connect(onMouseMoveEvent); + + function setDebugCursor(x, y) { + var nx = 2.0 * (x / Window.innerWidth) - 1.0; + var ny = 1.0 - 2.0 * ((y) / (Window.innerHeight)); + + Render.getConfig("RenderMainView").getConfig("DebugDeferredBuffer").size = { x: nx, y: ny, z: 1.0, w: 1.0 }; + } + + } + + function closeLuciWindow() { + if (luciWindow !== undefined) { + activeWindow.fromQml.disconnect(fromQml); + } + luciWindow = {}; + + Controller.mousePressEvent.disconnect(onMousePressEvent); + Controller.mouseReleaseEvent.disconnect(onMouseReleaseEvent); + Controller.mouseMoveEvent.disconnect(onMouseMoveEvent); + pages.clear(); + } + + pages.addPage('Luci', 'Luci', '../luci.qml', 300, 420, openLuciWindow, closeLuciWindow); + pages.addPage('openEngineLODView', 'Render LOD', '../lod.qml', 300, 400); + pages.addPage('openMaterialInspectorView', 'Material Inspector', '../materialInspector.qml', 300, 400, MaterialInspector.setWindow, MaterialInspector.setWindow); + + pages.open('Luci'); + + + return pages; +} + + +openView(); + diff --git a/scripts/developer/utilities/render/materialInspector.js b/scripts/developer/utilities/render/materialInspector.js index 76e5da5cd0..98d9f769fb 100644 --- a/scripts/developer/utilities/render/materialInspector.js +++ b/scripts/developer/utilities/render/materialInspector.js @@ -109,7 +109,9 @@ function mouseReleaseEvent(event) { } function killWindow() { - setWindow(undefined); + activeWindow = undefined; + + // setWindow(undefined); } function toQml(message) { @@ -138,14 +140,14 @@ function setSelectedObject(id, type) { function setWindow(window) { if (activeWindow !== undefined) { setSelectedObject(Uuid.NULL, ""); - activeWindow.closed.disconnect(killWindow); + // activeWindow.closed.disconnect(killWindow); activeWindow.fromQml.disconnect(fromQml); Controller.mousePressEvent.disconnect(mousePressEvent); Controller.mouseReleaseEvent.disconnect(mouseReleaseEvent); activeWindow.close(); } if (window !== undefined) { - window.closed.connect(killWindow); + // window.closed.connect(killWindow); window.fromQml.connect(fromQml); Controller.mousePressEvent.connect(mousePressEvent); Controller.mouseReleaseEvent.connect(mouseReleaseEvent); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 92cd103c85..69cf278ab3 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -2363,41 +2363,62 @@ var PropertiesTool = function (opts) { } var i, properties, dY, diff, newPosition; if (data.type === "update") { - if (selectionManager.selections.length > 1) { - for (i = 0; i < selectionManager.selections.length; i++) { - Entities.editEntity(selectionManager.selections[i], data.properties); + + if (data.properties || data.propertiesMap) { + var propertiesMap = data.propertiesMap; + if (propertiesMap === undefined) { + propertiesMap = [{ + entityIDs: data.ids, + properties: data.properties, + }]; } - } else if (data.properties) { - if (data.properties.dynamic === false) { - // this object is leaving dynamic, so we zero its velocities - data.properties.localVelocity = Vec3.ZERO; - data.properties.localAngularVelocity = Vec3.ZERO; - } - if (data.properties.rotation !== undefined) { - data.properties.rotation = Quat.fromVec3Degrees(data.properties.rotation); - } - if (data.properties.localRotation !== undefined) { - data.properties.localRotation = Quat.fromVec3Degrees(data.properties.localRotation); - } - if (data.properties.emitOrientation !== undefined) { - data.properties.emitOrientation = Quat.fromVec3Degrees(data.properties.emitOrientation); - } - if (data.properties.keyLight !== undefined && data.properties.keyLight.direction !== undefined) { - var currentKeyLightDirection = Vec3.toPolar(Entities.getEntityProperties(selectionManager.selections[0], ['keyLight.direction']).keyLight.direction); - if (data.properties.keyLight.direction.x === undefined) { - data.properties.keyLight.direction.x = currentKeyLightDirection.x; + + var sendListUpdate = false; + propertiesMap.forEach(function(propertiesObject) { + var properties = propertiesObject.properties; + var updateEntityIDs = propertiesObject.entityIDs; + if (properties.dynamic === false) { + // this object is leaving dynamic, so we zero its velocities + properties.localVelocity = Vec3.ZERO; + properties.localAngularVelocity = Vec3.ZERO; } - if (data.properties.keyLight.direction.y === undefined) { - data.properties.keyLight.direction.y = currentKeyLightDirection.y; + if (properties.rotation !== undefined) { + properties.rotation = Quat.fromVec3Degrees(properties.rotation); } - data.properties.keyLight.direction = Vec3.fromPolar(data.properties.keyLight.direction.x, data.properties.keyLight.direction.y); - } - Entities.editEntity(selectionManager.selections[0], data.properties); - if (data.properties.name !== undefined || data.properties.modelURL !== undefined || data.properties.materialURL !== undefined || - data.properties.visible !== undefined || data.properties.locked !== undefined) { + if (properties.localRotation !== undefined) { + properties.localRotation = Quat.fromVec3Degrees(properties.localRotation); + } + if (properties.emitOrientation !== undefined) { + properties.emitOrientation = Quat.fromVec3Degrees(properties.emitOrientation); + } + if (properties.keyLight !== undefined && properties.keyLight.direction !== undefined) { + var currentKeyLightDirection = Vec3.toPolar(Entities.getEntityProperties(selectionManager.selections[0], ['keyLight.direction']).keyLight.direction); + if (properties.keyLight.direction.x === undefined) { + properties.keyLight.direction.x = currentKeyLightDirection.x; + } + if (properties.keyLight.direction.y === undefined) { + properties.keyLight.direction.y = currentKeyLightDirection.y; + } + properties.keyLight.direction = Vec3.fromPolar(properties.keyLight.direction.x, properties.keyLight.direction.y); + } + + updateEntityIDs.forEach(function (entityID) { + Entities.editEntity(entityID, properties); + }); + + if (properties.name !== undefined || properties.modelURL !== undefined || properties.materialURL !== undefined || + properties.visible !== undefined || properties.locked !== undefined) { + + sendListUpdate = true; + } + + }); + if (sendListUpdate) { entityListTool.sendUpdate(); } } + + if (data.onlyUpdateEntities) { blockPropertyUpdates = true; } else { @@ -2407,9 +2428,9 @@ var PropertiesTool = function (opts) { selectionManager._update(false, this); blockPropertyUpdates = false; } else if (data.type === 'saveUserData' || data.type === 'saveMaterialData') { - //the event bridge and json parsing handle our avatar id string differently. - var actualID = data.id.split('"')[1]; - Entities.editEntity(actualID, data.properties); + data.ids.forEach(function(entityID) { + Entities.editEntity(entityID, data.properties); + }); } else if (data.type === "showMarketplace") { showMarketplace(); } else if (data.type === "action") { diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index 2d4a2d3e97..858b93ef1e 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -40,9 +40,6 @@ function updateControllerDisplay() { var button; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); -// Independent and Entity mode make people sick; disable them in hmd. -var desktopOnlyViews = ['Independent Mode', 'Entity Mode']; - var switchToVR = "ENTER VR"; var switchToDesktop = "EXIT VR"; @@ -59,9 +56,6 @@ function onHmdChanged(isHmd) { text: switchToVR }); } - desktopOnlyViews.forEach(function (view) { - Menu.setMenuEnabled("View>" + view, !isHmd); - }); updateControllerDisplay(); } diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index d6a281b0c4..470e57ad6d 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -193,8 +193,8 @@ td { } td.hidden { - padding-left: 0px; - padding-right: 0px; + padding-left: 0; + padding-right: 0; } td.url { @@ -262,6 +262,42 @@ input[type="text"] { width: 100%; } +input.multi-diff:not(:focus) + span.multi-diff, +textarea.multi-diff:not(:focus) + span.multi-diff, +.draggable-number.multi-diff>input:not(:focus)+span.multi-diff, +dl>dt.multi-diff:not(:focus) + span.multi-diff { + visibility: visible; + position: absolute; + display: inline-block; + z-index: 2; + top: 7.5px; + left: 20px; + max-width: 50px; + min-width: 10px; + width: 50%; + height: 13px; + background-image: linear-gradient(transparent 0%, transparent 10%, #afafaf 10%, #afafaf 20%, transparent 20%, transparent 45%, #afafaf 45%, #afafaf 55%, transparent 55%, transparent 80%, #afafaf 80%, #afafaf 90%, transparent 90%, transparent 100%); + background-repeat: no-repeat; + pointer-events: none; +} + +input.multi-diff:not(:focus)::-webkit-input-placeholder, input.multi-diff:not(:focus) { + color: transparent; +} + +.draggable-number.multi-diff .text { + color: transparent; +} + +.dropdown > span.multi-diff { + top: 5px; + left: 10px; +} + +.text, .url, .texture, .textarea { + position: relative; +} + input[type="search"] { height: 28px; width: 100%; @@ -333,7 +369,7 @@ input[type=range]::-webkit-slider-thumb { input[type=range]::-webkit-slider-thumb:hover { background-color: white; } -input[type=range]:focus { /*#252525*/ +input[type=range]:focus { outline: none; } @@ -443,6 +479,21 @@ input[type=checkbox]:checked + label { input[type=checkbox]:checked + label:hover { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEySURBVDhPnZLPSsNAEMa/XVPBCE0RhNy0OarP4Av4AD6JB0GwVBA8efBBxHsgh4CQswcRoUIpiIpVAm3zZ5M4szFSbQPBH3xkJvNNZskOer2eLIriKM/ze1JOcS1UHmdZduF5ngEKjr/fN4Z6+oKerwA2gxC4HAFPEWVLsAzgZAvYt3Q6Enw6jg7uBAaTFMNwhpnKdbXCkAJdy8ROu4XrXW2HTJIErHcFDD6nC02Mom8PwymeE2gvS0ZRBBaTlsOXEmdlrfLLOI7Bakrl/zWxCT8T/904f9QW/b06qtrCUdtFCqdjYs2Q2jAPX8c2XQd7Kr/wfV8vwIPs4Ga1ixe5Xrr/YFLTYfKIvWzM6ZtwXZdX7lxXG0L+sxXHcW5t254opRzawQ0S72+dPmjTroIgOP0CQSMt5LDn1T8AAAAASUVORK5CYII=); } +input.multi-diff[type=checkbox] + label { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFbSURBVDhPY2xoaGD68+dPMSMjY9L////VgTQjAw4AlH8PxLOPHj1azWxjY1MBVNsBFBfBpwkEgNKcQGwtJyfHyALkF4IE34gqM9zU9WT4wicG4mIA1l/fGIyOL2EQeP8EZEAiC5AQBUlcMQ5ieMXIwfDo9SeG73/+gRXDAAsTI4Pd9wdgTVAgw/Tv3z8GEP7Jwctw78M3DE0goPr6BoPludVgdTAM1wgCv//9B9PIQOPNDYaAGxtRNIEw03+gYhDGBtSBNgVc3wiWR8dM//4DTQBidKD++jqD//X1YDlsGMWpMKD26jqD79V1GM5DxihOZQWGntqrawy+V9ZiOA0dw21k/f6JwerzHQbvS2swTMeGGfPz8l8BLRP9KizDwP0WHk+EwGum/3//94M8y/nmEdZAwIb//vs7g/nk6ZPHzE3NvwITrxLDfwYhIAamZpz4PcM/hslXrl6pBwAmfz5iaAlAuAAAAABJRU5ErkJggg==); +} +input.multi-diff[type=checkbox] + label:hover { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFPSURBVDhPjZJBSwJBHMXfrG6rWEkl1MFDGOihDn2JIIrqc3QJunbyFhUkRieD+hYepWteuxctXiJ1Q5xmdmZ3bWZTUHezfvAu/3lv3n+HRblcTrbb7fN+v/8eBMFgFpxz13Gcu3q9bqHb7V4M5/9GhatE3cIsy0o99YBKC3jliCWbBK43gK0MoDI9otfTB/vPBC9Uwu4xMC8IzSOSBsFxIYNqMTGcAIYQAlodD3j5/IqENIc5gqt1P/SNZKhaXR0a5E/5BEcrwH1xEHrGZbiuC604DpZ81AoiPJ/WROM4e4sSt3kaaRopNrg7z1FZdSLmcU2saqrX20lTXC5/RFabFmk2m+GLnBnbWJMOThJv4SV/QRqNBjNNM9UiGeQHdDiejZSSG5TSG71zjnVivyVOKlNLlEqlx+xCds7zvU31G6Z938dvEq4QjLMH27ZPvwHFVYQr3h7uHwAAAABJRU5ErkJggg==); +} + +.rgb.fstuple .color-picker.multi-diff:after { + width: 20px; + height: 20px; + content: ' '; + background: darkgray; + display: flex; + clip-path: polygon(0 0, 0 100%, 100% 100%); +} .icon-input input { position: relative; @@ -535,7 +586,6 @@ input[type=checkbox]:checked + label:hover { div.section-header, hr { display: flex; flex-flow: row nowrap; - padding: 10px 16px; font-family: Raleway-Regular; font-size: 12px; @@ -731,8 +781,6 @@ span.indented { .dropdown dl { clear: both; cursor: pointer; -} -.dropdown dl { font-family: FiraSans-SemiBold; font-size: 15px; width: 292px; @@ -741,7 +789,10 @@ span.indented { color: #afafaf; background: #575757; position: relative; + display: flex; + align-items: center; } + .dropdown dl[dropped="true"] { color: #404040; background: linear-gradient(#afafaf, #afafaf); @@ -878,6 +929,8 @@ div.refresh { div.refresh input[type="button"] { float: right; margin-right: -44px; + position: relative; + left: 10px; } .color-picker { @@ -930,6 +983,8 @@ div.refresh input[type="button"] { position: relative; height: 28px; flex: 0 1 124px; + display: flex; + align-items: center; } .draggable-number .text { @@ -1735,3 +1790,7 @@ input[type=number].hide-spinner::-webkit-inner-spin-button { -webkit-appearance: none; visibility: hidden; } + +div.jsoneditor-menu a.jsoneditor-poweredBy { + display: none; +} diff --git a/scripts/system/html/js/draggableNumber.js b/scripts/system/html/js/draggableNumber.js index 0d6af01ebd..3c7b74290c 100644 --- a/scripts/system/html/js/draggableNumber.js +++ b/scripts/system/html/js/draggableNumber.js @@ -13,6 +13,7 @@ function DraggableNumber(min, max, step, decimals, dragStart, dragEnd) { this.min = min; this.max = max; this.step = step !== undefined ? step : 1; + this.multiDiffModeEnabled = false; this.decimals = decimals; this.dragStartFunction = dragStart; this.dragEndFunction = dragEnd; @@ -20,6 +21,7 @@ function DraggableNumber(min, max, step, decimals, dragStart, dragEnd) { this.initialMouseEvent = null; this.lastMouseEvent = null; this.valueChangeFunction = null; + this.multiDiffStepFunction = null; this.initialize(); } @@ -70,22 +72,22 @@ DraggableNumber.prototype = { this.lastMouseEvent = event; } if (this.dragging && this.lastMouseEvent) { - let initialValue = this.elInput.value; - let dx = event.clientX - this.lastMouseEvent.clientX; - let changeValue = dx !== 0; - if (changeValue) { - while (dx !== 0) { - if (dx > 0) { - this.elInput.stepUp(); - --dx; - } else { - this.elInput.stepDown(); - ++dx; + let dragDelta = event.clientX - this.lastMouseEvent.clientX; + if (dragDelta !== 0) { + if (this.multiDiffModeEnabled) { + if (this.multiDiffStepFunction) { + this.multiDiffStepFunction(dragDelta * this.step); + } + } else { + if (dragDelta > 0) { + this.elInput.stepUp(dragDelta); + } else { + this.elInput.stepDown(-dragDelta); + } + this.inputChange(); + if (this.valueChangeFunction) { + this.valueChangeFunction(); } - } - this.inputChange(); - if (this.valueChangeFunction) { - this.valueChangeFunction(); } } this.lastMouseEvent = event; @@ -106,25 +108,46 @@ DraggableNumber.prototype = { stepUp: function() { if (!this.isDisabled()) { - this.elInput.value = parseFloat(this.elInput.value) + this.step; - this.inputChange(); - if (this.valueChangeFunction) { - this.valueChangeFunction(); + if (this.multiDiffModeEnabled) { + if (this.multiDiffStepFunction) { + this.multiDiffStepFunction(this.step, true); + } + } else { + this.elInput.value = parseFloat(this.elInput.value) + this.step; + this.inputChange(); + if (this.valueChangeFunction) { + this.valueChangeFunction(); + } } } }, stepDown: function() { if (!this.isDisabled()) { - this.elInput.value = parseFloat(this.elInput.value) - this.step; - this.inputChange(); - if (this.valueChangeFunction) { - this.valueChangeFunction(); + if (this.multiDiffModeEnabled) { + if (this.multiDiffStepFunction) { + this.multiDiffStepFunction(-this.step, true); + } + } else { + this.elInput.value = parseFloat(this.elInput.value) - this.step; + this.inputChange(); + if (this.valueChangeFunction) { + this.valueChangeFunction(); + } } } }, - setValue: function(newValue) { + setValue: function(newValue, isMultiDiff) { + if (isMultiDiff !== undefined) { + this.setMultiDiff(isMultiDiff); + } + + if (isNaN(newValue)) { + console.error("DraggableNumber.setValue() > " + newValue + " is not a number."); + return; + } + if (newValue !== "" && this.decimals !== undefined) { this.elInput.value = parseFloat(newValue).toFixed(this.decimals); } else { @@ -133,11 +156,24 @@ DraggableNumber.prototype = { this.elText.firstChild.data = this.elInput.value; }, + setMultiDiff: function(isMultiDiff) { + this.multiDiffModeEnabled = isMultiDiff; + if (isMultiDiff) { + this.elDiv.classList.add('multi-diff'); + } else { + this.elDiv.classList.remove('multi-diff'); + } + }, + setValueChangeFunction: function(valueChangeFunction) { this.valueChangeFunction = valueChangeFunction.bind(this.elInput); this.elInput.addEventListener("change", this.valueChangeFunction); }, - + + setMultiDiffStepFunction: function (multiDiffStepFunction) { + this.multiDiffStepFunction = multiDiffStepFunction; + }, + inputChange: function() { let value = this.elInput.value; if (this.max !== undefined) { @@ -155,6 +191,9 @@ DraggableNumber.prototype = { keyPress: function(event) { if (event.keyCode === ENTER_KEY) { + if (this.valueChangeFunction) { + this.valueChangeFunction(); + } this.inputBlur(); } }, @@ -203,7 +242,10 @@ DraggableNumber.prototype = { this.elRightArrow.className = 'right-arrow'; this.elRightArrow.innerHTML = 'D'; this.elRightArrow.addEventListener("click", this.onStepUp); - + + this.elMultiDiff = document.createElement('span'); + this.elMultiDiff.className = 'multi-diff'; + this.elInput = document.createElement('input'); this.elInput.className = "input"; this.elInput.setAttribute("type", "number"); @@ -220,6 +262,7 @@ DraggableNumber.prototype = { this.elDiv.appendChild(this.elLeftArrow); this.elDiv.appendChild(this.elText); this.elDiv.appendChild(this.elInput); + this.elDiv.appendChild(this.elMultiDiff); this.elDiv.appendChild(this.elRightArrow); } }; diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 021a27152a..d7800ada5d 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -8,17 +8,34 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html /* global alert, augmentSpinButtons, clearTimeout, console, document, Element, - EventBridge, JSONEditor, openEventBridge, setTimeout, window, _ $ */ + EventBridge, JSONEditor, openEventBridge, setTimeout, window, _, $ */ const DEGREES_TO_RADIANS = Math.PI / 180.0; const NO_SELECTION = ","; -const PROPERTY_SPACE_MODE = { +const PROPERTY_SPACE_MODE = Object.freeze({ ALL: 0, LOCAL: 1, WORLD: 2 -}; +}); + +const PROPERTY_SELECTION_VISIBILITY = Object.freeze({ + SINGLE_SELECTION: 1, + MULTIPLE_SELECTIONS: 2, + MULTI_DIFF_SELECTIONS: 4, + ANY_SELECTIONS: 7, /* SINGLE_SELECTION | MULTIPLE_SELECTIONS | MULTI_DIFF_SELECTIONS */ +}); + +// Multiple-selection behavior +const PROPERTY_MULTI_DISPLAY_MODE = Object.freeze({ + DEFAULT: 0, + /** + * Comma separated values + * Limited for properties with type "string" or "textarea" and readOnly enabled + */ + COMMA_SEPARATED_VALUES: 1, +}); const GROUPS = [ { @@ -45,6 +62,7 @@ const GROUPS = [ placeholder: "ID", readOnly: true, replaceID: "placeholder-property-id", + multiDisplayMode: PROPERTY_MULTI_DISPLAY_MODE.COMMA_SEPARATED_VALUES, }, { label: "Description", @@ -708,6 +726,7 @@ const GROUPS = [ type: "dynamic-multiselect", propertyUpdate: materialTargetPropertyUpdate, propertyID: "parentMaterialName", + selectionVisibility: PROPERTY_SELECTION_VISIBILITY.SINGLE_SELECTION, }, { label: "Priority", @@ -1340,6 +1359,7 @@ const GROUPS = [ type: "placeholder", indentedLabel: true, propertyID: "serverScriptStatus", + selectionVisibility: PROPERTY_SELECTION_VISIBILITY.SINGLE_SELECTION, }, { label: "Lifetime", @@ -1528,7 +1548,6 @@ const GROUPS_PER_TYPE = { }; const EDITOR_TIMEOUT_DURATION = 1500; -const IMAGE_DEBOUNCE_TIMEOUT = 250; const DEBOUNCE_TIMEOUT = 125; const COLOR_MIN = 0; @@ -1553,7 +1572,7 @@ const ENABLE_DISABLE_SELECTOR = "input, textarea, span, .dropdown dl, .color-pic const PROPERTY_NAME_DIVISION = { GROUP: 0, PROPERTY: 1, - SUBPROPERTY: 2, + SUB_PROPERTY: 2, }; const RECT_ELEMENTS = { @@ -1588,17 +1607,22 @@ let properties = {}; let propertyRangeRequests = []; let colorPickers = {}; let particlePropertyUpdates = {}; -let selectedEntityProperties; -let lastEntityID = null; +let selectedEntityIDs = new Set(); +let currentSelections = []; let createAppTooltip = new CreateAppTooltip(); let currentSpaceMode = PROPERTY_SPACE_MODE.LOCAL; + function createElementFromHTML(htmlString) { let elTemplate = document.createElement('template'); elTemplate.innerHTML = htmlString.trim(); return elTemplate.content.firstChild; } +function isFlagSet(value, flag) { + return (value & flag) === flag; +} + /** * GENERAL PROPERTY/GROUP FUNCTIONS */ @@ -1680,8 +1704,11 @@ function disableProperties() { } function showPropertyElement(propertyID, show) { - let elProperty = properties[propertyID].elContainer; - elProperty.style.display = show ? "" : "none"; + setPropertyVisibility(properties[propertyID], show); +} + +function setPropertyVisibility(property, visible) { + property.elContainer.style.display = visible ? null : "none"; } function resetProperties() { @@ -1692,6 +1719,7 @@ function resetProperties() { switch (propertyData.type) { case 'number': case 'string': { + property.elInput.classList.remove('multi-diff'); if (propertyData.defaultValue !== undefined) { property.elInput.value = propertyData.defaultValue; } else { @@ -1700,46 +1728,49 @@ function resetProperties() { break; } case 'bool': { + property.elInput.classList.remove('multi-diff'); property.elInput.checked = false; break; } case 'number-draggable': { if (propertyData.defaultValue !== undefined) { - property.elNumber.setValue(propertyData.defaultValue); + property.elNumber.setValue(propertyData.defaultValue, false); } else { - property.elNumber.setValue(""); + property.elNumber.setValue("", false); } break; } case 'rect': { - property.elNumberX.setValue(""); - property.elNumberY.setValue(""); - property.elNumberWidth.setValue(""); - property.elNumberHeight.setValue(""); + property.elNumberX.setValue("", false); + property.elNumberY.setValue("", false); + property.elNumberWidth.setValue("", false); + property.elNumberHeight.setValue("", false); break; } case 'vec3': case 'vec2': { - property.elNumberX.setValue(""); - property.elNumberY.setValue(""); + property.elNumberX.setValue("", false); + property.elNumberY.setValue("", false); if (property.elNumberZ !== undefined) { - property.elNumberZ.setValue(""); + property.elNumberZ.setValue("", false); } break; } case 'color': { property.elColorPicker.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - property.elNumberR.setValue(""); - property.elNumberG.setValue(""); - property.elNumberB.setValue(""); + property.elNumberR.setValue("", false); + property.elNumberG.setValue("", false); + property.elNumberB.setValue("", false); break; } case 'dropdown': { + property.elInput.classList.remove('multi-diff'); property.elInput.value = ""; setDropdownText(property.elInput); break; } case 'textarea': { + property.elInput.classList.remove('multi-diff'); property.elInput.value = ""; setTextareaScrolling(property.elInput); break; @@ -1749,6 +1780,7 @@ function resetProperties() { break; } case 'texture': { + property.elInput.classList.remove('multi-diff'); property.elInput.value = ""; property.elInput.imageLoad(property.elInput.value); break; @@ -1766,7 +1798,11 @@ function resetProperties() { } } } - + + resetServerScriptStatus(); +} + +function resetServerScriptStatus() { let elServerScriptError = document.getElementById("property-serverScripts-error"); let elServerScriptStatus = document.getElementById("property-serverScripts-status"); elServerScriptError.parentElement.style.display = "none"; @@ -1775,43 +1811,194 @@ function resetProperties() { function showGroupsForType(type) { if (type === "Box" || type === "Sphere") { - type = "Shape"; + showGroupsForTypes(["Shape"]); + return; } - - let typeGroups = GROUPS_PER_TYPE[type]; + showGroupsForTypes([type]); +} - for (let groupKey in elGroups) { - let elGroup = elGroups[groupKey]; - if (typeGroups && typeGroups.indexOf(groupKey) > -1) { +function getGroupsForTypes(types) { + return Object.keys(elGroups).filter((groupKey) => { + return types.map(type => GROUPS_PER_TYPE[type].includes(groupKey)).every(function (hasGroup) { + return hasGroup; + }); + }); +} + +function showGroupsForTypes(types) { + Object.entries(elGroups).forEach(([groupKey, elGroup]) => { + if (types.map(type => GROUPS_PER_TYPE[type].includes(groupKey)).every(function (hasGroup) { return hasGroup; })) { elGroup.style.display = "block"; } else { elGroup.style.display = "none"; } - } + }); } -function getPropertyValue(originalPropertyName) { - // if this is a compound property name (i.e. animation.running) +function getFirstSelectedID() { + if (selectedEntityIDs.size === 0) { + return null; + } + return selectedEntityIDs.values().next().value; +} + +/** + * Returns true when the user is currently dragging the numeric slider control of the property + * @param propertyName - name of property + * @returns {boolean} currentlyDragging + */ +function isCurrentlyDraggingProperty(propertyName) { + return properties[propertyName] && properties[propertyName].dragging === true; +} + +const SUPPORTED_FALLBACK_TYPES = ['number', 'number-draggable', 'rect', 'vec3', 'vec2', 'color']; + +function getMultiplePropertyValue(originalPropertyName) { + // if this is a compound property name (i.e. animation.running) // then split it by . up to 3 times to find property value - let propertyValue; + + let propertyData = null; + if (properties[originalPropertyName] !== undefined) { + propertyData = properties[originalPropertyName].data; + } + + let propertyValues = []; let splitPropertyName = originalPropertyName.split('.'); if (splitPropertyName.length > 1) { let propertyGroupName = splitPropertyName[PROPERTY_NAME_DIVISION.GROUP]; let propertyName = splitPropertyName[PROPERTY_NAME_DIVISION.PROPERTY]; - let groupProperties = selectedEntityProperties[propertyGroupName]; - if (groupProperties === undefined || groupProperties[propertyName] === undefined) { - return undefined; - } - if (splitPropertyName.length === PROPERTY_NAME_DIVISION.SUBPROPERTY + 1) { - let subPropertyName = splitPropertyName[PROPERTY_NAME_DIVISION.SUBPROPERTY]; - propertyValue = groupProperties[propertyName][subPropertyName]; - } else { - propertyValue = groupProperties[propertyName]; - } + propertyValues = currentSelections.map(selection => { + let groupProperties = selection.properties[propertyGroupName]; + if (groupProperties === undefined || groupProperties[propertyName] === undefined) { + return undefined; + } + if (splitPropertyName.length === PROPERTY_NAME_DIVISION.SUB_PROPERTY + 1) { + let subPropertyName = splitPropertyName[PROPERTY_NAME_DIVISION.SUB_PROPERTY]; + return groupProperties[propertyName][subPropertyName]; + } else { + return groupProperties[propertyName]; + } + }); } else { - propertyValue = selectedEntityProperties[originalPropertyName]; + propertyValues = currentSelections.map(selection => selection.properties[originalPropertyName]); + } + + if (propertyData !== null && propertyData.fallbackProperty !== undefined && + SUPPORTED_FALLBACK_TYPES.includes(propertyData.type)) { + + let fallbackMultiValue = null; + + for (let i = 0; i < propertyValues.length; ++i) { + let isPropertyNotNumber = false; + let propertyValue = propertyValues[i]; + if (propertyValue === undefined) { + continue; + } + switch (propertyData.type) { + case 'number': + case 'number-draggable': + isPropertyNotNumber = isNaN(propertyValue) || propertyValue === null; + break; + case 'rect': + case 'vec3': + case 'vec2': + isPropertyNotNumber = isNaN(propertyValue.x) || propertyValue.x === null; + break; + case 'color': + isPropertyNotNumber = isNaN(propertyValue.red) || propertyValue.red === null; + break; + } + if (isPropertyNotNumber) { + if (fallbackMultiValue === null) { + fallbackMultiValue = getMultiplePropertyValue(propertyData.fallbackProperty); + } + propertyValues[i] = fallbackMultiValue.values[i]; + } + } + } + + const firstValue = propertyValues[0]; + const isMultiDiffValue = !propertyValues.every((x) => deepEqual(firstValue, x)); + + if (isMultiDiffValue) { + return { + value: undefined, + values: propertyValues, + isMultiDiffValue: true + } + } + + return { + value: propertyValues[0], + values: propertyValues, + isMultiDiffValue: false + }; +} + +/** + * Retrieve more detailed info for differing Numeric MultiplePropertyValue + * @param multiplePropertyValue - input multiplePropertyValue + * @param propertyData + * @returns {{keys: *[], propertyComponentDiff, averagePerPropertyComponent}} + */ +function getDetailedNumberMPVDiff(multiplePropertyValue, propertyData) { + let detailedValues = {}; + // Fixed numbers can't be easily averaged since they're strings, so lets keep an array of unmodified numbers + let unmodifiedValues = {}; + const DEFAULT_KEY = 0; + let uniqueKeys = new Set([]); + multiplePropertyValue.values.forEach(function(propertyValue) { + if (typeof propertyValue === "object") { + Object.entries(propertyValue).forEach(function([key, value]) { + if (!uniqueKeys.has(key)) { + uniqueKeys.add(key); + detailedValues[key] = []; + unmodifiedValues[key] = []; + } + detailedValues[key].push(applyInputNumberPropertyModifiers(value, propertyData)); + unmodifiedValues[key].push(value); + }); + } else { + if (!uniqueKeys.has(DEFAULT_KEY)) { + uniqueKeys.add(DEFAULT_KEY); + detailedValues[DEFAULT_KEY] = []; + unmodifiedValues[DEFAULT_KEY] = []; + } + detailedValues[DEFAULT_KEY].push(applyInputNumberPropertyModifiers(propertyValue, propertyData)); + unmodifiedValues[DEFAULT_KEY].push(propertyValue); + } + }); + let keys = [...uniqueKeys]; + + let propertyComponentDiff = {}; + Object.entries(detailedValues).forEach(function([key, value]) { + propertyComponentDiff[key] = [...new Set(value)].length > 1; + }); + + let averagePerPropertyComponent = {}; + Object.entries(unmodifiedValues).forEach(function([key, value]) { + let average = value.reduce((a, b) => a + b) / value.length; + averagePerPropertyComponent[key] = applyInputNumberPropertyModifiers(average, propertyData); + }); + + return { + keys, + propertyComponentDiff, + averagePerPropertyComponent, + }; +} + +function getDetailedSubPropertyMPVDiff(multiplePropertyValue, subPropertyName) { + let isChecked = false; + let checkedValues = multiplePropertyValue.values.map((value) => value.split(",").includes(subPropertyName)); + let isMultiDiff = !checkedValues.every(value => value === checkedValues[0]); + if (!isMultiDiff) { + isChecked = checkedValues[0]; + } + return { + isChecked, + isMultiDiff } - return propertyValue; } function updateVisibleSpaceModeProperties() { @@ -1829,12 +2016,11 @@ function updateVisibleSpaceModeProperties() { } } - /** * PROPERTY UPDATE FUNCTIONS */ -function updateProperty(originalPropertyName, propertyValue, isParticleProperty) { +function createPropertyUpdateObject(originalPropertyName, propertyValue) { let propertyUpdate = {}; // if this is a compound property name (i.e. animation.running) then split it by . up to 3 times let splitPropertyName = originalPropertyName.split('.'); @@ -1842,8 +2028,8 @@ function updateProperty(originalPropertyName, propertyValue, isParticleProperty) let propertyGroupName = splitPropertyName[PROPERTY_NAME_DIVISION.GROUP]; let propertyName = splitPropertyName[PROPERTY_NAME_DIVISION.PROPERTY]; propertyUpdate[propertyGroupName] = {}; - if (splitPropertyName.length === PROPERTY_NAME_DIVISION.SUBPROPERTY + 1) { - let subPropertyName = splitPropertyName[PROPERTY_NAME_DIVISION.SUBPROPERTY]; + if (splitPropertyName.length === PROPERTY_NAME_DIVISION.SUB_PROPERTY + 1) { + let subPropertyName = splitPropertyName[PROPERTY_NAME_DIVISION.SUB_PROPERTY]; propertyUpdate[propertyGroupName][propertyName] = {}; propertyUpdate[propertyGroupName][propertyName][subPropertyName] = propertyValue; } else { @@ -1852,6 +2038,12 @@ function updateProperty(originalPropertyName, propertyValue, isParticleProperty) } else { propertyUpdate[originalPropertyName] = propertyValue; } + return propertyUpdate; +} + +function updateProperty(originalPropertyName, propertyValue, isParticleProperty) { + let propertyUpdate = createPropertyUpdateObject(originalPropertyName, propertyValue); + // queue up particle property changes with the debounced sync to avoid // causing particle emitting to reset excessively with each value change if (isParticleProperty) { @@ -1861,9 +2053,9 @@ function updateProperty(originalPropertyName, propertyValue, isParticleProperty) particleSyncDebounce(); } else { // only update the entity property value itself if in the middle of dragging - // prevent undo command push, saving new property values, and property update + // prevent undo command push, saving new property values, and property update // callback until drag is complete (additional update sent via dragEnd callback) - let onlyUpdateEntity = properties[originalPropertyName] && properties[originalPropertyName].dragging === true; + let onlyUpdateEntity = isCurrentlyDraggingProperty(originalPropertyName); updateProperties(propertyUpdate, onlyUpdateEntity); } } @@ -1878,15 +2070,27 @@ function updateProperties(propertiesToUpdate, onlyUpdateEntity) { onlyUpdateEntity = false; } EventBridge.emitWebEvent(JSON.stringify({ - id: lastEntityID, + ids: [...selectedEntityIDs], type: "update", properties: propertiesToUpdate, onlyUpdateEntities: onlyUpdateEntity })); } +function updateMultiDiffProperties(propertiesMapToUpdate, onlyUpdateEntity) { + if (onlyUpdateEntity === undefined) { + onlyUpdateEntity = false; + } + EventBridge.emitWebEvent(JSON.stringify({ + type: "update", + propertiesMap: propertiesMapToUpdate, + onlyUpdateEntities: onlyUpdateEntity + })); +} + function createEmitTextPropertyUpdateFunction(property) { return function() { + property.elInput.classList.remove('multi-diff'); updateProperty(property.name, this.value, property.isParticleProperty); }; } @@ -1906,60 +2110,70 @@ function createDragStartFunction(property) { function createDragEndFunction(property) { return function() { property.dragging = false; - // send an additional update post-dragging to consider whole property change from dragStart to dragEnd to be 1 action - this.valueChangeFunction(); + + if (this.multiDiffModeEnabled) { + let propertyMultiValue = getMultiplePropertyValue(property.name); + let updateObjects = []; + const selectedEntityIDsArray = [...selectedEntityIDs]; + + for (let i = 0; i < selectedEntityIDsArray.length; ++i) { + let entityID = selectedEntityIDsArray[i]; + updateObjects.push({ + entityIDs: [entityID], + properties: createPropertyUpdateObject(property.name, propertyMultiValue.values[i]), + }); + } + + // send a full updateMultiDiff post-dragging to count as an action in the undo stack + updateMultiDiffProperties(updateObjects); + } else { + // send an additional update post-dragging to consider whole property change from dragStart to dragEnd to be 1 action + this.valueChangeFunction(); + } }; } function createEmitNumberPropertyUpdateFunction(property) { return function() { - let multiplier = property.data.multiplier; - if (multiplier === undefined) { - multiplier = 1; - } - let value = parseFloat(this.value) * multiplier; + let value = parseFloat(applyOutputNumberPropertyModifiers(parseFloat(this.value), property.data)); updateProperty(property.name, value, property.isParticleProperty); }; } -function createEmitVec2PropertyUpdateFunction(property) { - return function () { - let multiplier = property.data.multiplier; - if (multiplier === undefined) { - multiplier = 1; - } - let newValue = { - x: property.elNumberX.elInput.value * multiplier, - y: property.elNumberY.elInput.value * multiplier - }; - updateProperty(property.name, newValue, property.isParticleProperty); - }; -} - -function createEmitVec3PropertyUpdateFunction(property) { +function createEmitNumberPropertyComponentUpdateFunction(property, propertyComponent) { return function() { - let multiplier = property.data.multiplier; - if (multiplier === undefined) { - multiplier = 1; - } - let newValue = { - x: property.elNumberX.elInput.value * multiplier, - y: property.elNumberY.elInput.value * multiplier, - z: property.elNumberZ.elInput.value * multiplier - }; - updateProperty(property.name, newValue, property.isParticleProperty); - }; -} + let propertyMultiValue = getMultiplePropertyValue(property.name); + let value = parseFloat(applyOutputNumberPropertyModifiers(parseFloat(this.value), property.data)); -function createEmitRectPropertyUpdateFunction(property) { - return function() { - let newValue = { - x: property.elNumberX.elInput.value, - y: property.elNumberY.elInput.value, - width: property.elNumberWidth.elInput.value, - height: property.elNumberHeight.elInput.value, - }; - updateProperty(property.name, newValue, property.isParticleProperty); + if (propertyMultiValue.isMultiDiffValue) { + let updateObjects = []; + const selectedEntityIDsArray = [...selectedEntityIDs]; + + for (let i = 0; i < selectedEntityIDsArray.length; ++i) { + let entityID = selectedEntityIDsArray[i]; + + let propertyObject = propertyMultiValue.values[i]; + propertyObject[propertyComponent] = value; + + let updateObject = createPropertyUpdateObject(property.name, propertyObject); + updateObjects.push({ + entityIDs: [entityID], + properties: updateObject, + }); + + mergeDeep(currentSelections[i].properties, updateObject); + } + + // only update the entity property value itself if in the middle of dragging + // prevent undo command push, saving new property values, and property update + // callback until drag is complete (additional update sent via dragEnd callback) + let onlyUpdateEntity = isCurrentlyDraggingProperty(property.name); + updateMultiDiffProperties(updateObjects, onlyUpdateEntity); + } else { + let propertyValue = propertyMultiValue.value; + propertyValue[propertyComponent] = value; + updateProperty(property.name, propertyValue, property.isParticleProperty); + } }; } @@ -1979,16 +2193,34 @@ function emitColorPropertyUpdate(propertyName, red, green, blue, isParticlePrope updateProperty(propertyName, newValue, isParticleProperty); } -function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElement, subPropertyString, isParticleProperty) { - if (subPropertyElement.checked) { - if (propertyValue.indexOf(subPropertyString)) { - propertyValue += subPropertyString + ','; - } - } else { - // We've unchecked, so remove - propertyValue = propertyValue.replace(subPropertyString + ",", ""); +function toggleBooleanCSV(inputCSV, property, enable) { + let values = inputCSV.split(","); + if (enable && !values.includes(property)) { + values.push(property); + } else if (!enable && values.includes(property)) { + values = values.filter(value => value !== property); + } + return values.join(","); +} + +function updateCheckedSubProperty(propertyName, propertyMultiValue, subPropertyElement, subPropertyString, isParticleProperty) { + if (propertyMultiValue.isMultiDiffValue) { + let updateObjects = []; + const selectedEntityIDsArray = [...selectedEntityIDs]; + + for (let i = 0; i < selectedEntityIDsArray.length; ++i) { + let newValue = toggleBooleanCSV(propertyMultiValue.values[i], subPropertyString, subPropertyElement.checked); + updateObjects.push({ + entityIDs: [selectedEntityIDsArray[i]], + properties: createPropertyUpdateObject(propertyName, newValue), + }); + } + + updateMultiDiffProperties(updateObjects); + } else { + updateProperty(propertyName, toggleBooleanCSV(propertyMultiValue.value, subPropertyString, subPropertyElement.checked), + isParticleProperty); } - updateProperty(propertyName, propertyValue, isParticleProperty); } /** @@ -2005,7 +2237,7 @@ function createStringProperty(property, elProperty) { + ${propertyData.readOnly ? 'readonly' : ''}/> `); @@ -2014,7 +2246,12 @@ function createStringProperty(property, elProperty) { elInput.addEventListener('change', propertyData.onChange); } + + let elMultiDiff = document.createElement('span'); + elMultiDiff.className = "multi-diff"; + elProperty.appendChild(elInput); + elProperty.appendChild(elMultiDiff); if (propertyData.buttons !== undefined) { addButtons(elProperty, elementID, propertyData.buttons, false); @@ -2047,7 +2284,10 @@ function createBoolProperty(property, elProperty) { let subPropertyOf = propertyData.subPropertyOf; if (subPropertyOf !== undefined) { elInput.addEventListener('change', function() { - updateCheckedSubProperty(subPropertyOf, selectedEntityProperties[subPropertyOf], + let subPropertyMultiValue = getMultiplePropertyValue(subPropertyOf); + + updateCheckedSubProperty(subPropertyOf, + subPropertyMultiValue, elInput, propertyName, property.isParticleProperty); }); } else { @@ -2068,8 +2308,8 @@ function createNumberProperty(property, elProperty) { class='hide-spinner' type="number" ${propertyData.placeholder ? 'placeholder="' + propertyData.placeholder + '"' : ''} - ${propertyData.readOnly ? 'readonly' : ''}> - `) + ${propertyData.readOnly ? 'readonly' : ''}/> + `); if (propertyData.min !== undefined) { elInput.setAttribute("min", propertyData.min); @@ -2086,7 +2326,11 @@ function createNumberProperty(property, elProperty) { elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(property)); + let elMultiDiff = document.createElement('span'); + elMultiDiff.className = "multi-diff"; + elProperty.appendChild(elInput); + elProperty.appendChild(elMultiDiff); if (propertyData.buttons !== undefined) { addButtons(elProperty, elementID, propertyData.buttons, false); @@ -2107,24 +2351,79 @@ function updateNumberMinMax(property) { } } +/** + * + * @param {object} property - property update on step + * @param {string} [propertyComponent] - propertyComponent to update on step (e.g. enter 'x' to just update position.x) + * @returns {Function} + */ +function createMultiDiffStepFunction(property, propertyComponent) { + return function(step, shouldAddToUndoHistory) { + if (shouldAddToUndoHistory === undefined) { + shouldAddToUndoHistory = false; + } + + let propertyMultiValue = getMultiplePropertyValue(property.name); + if (!propertyMultiValue.isMultiDiffValue) { + console.log("setMultiDiffStepFunction is only supposed to be called in MultiDiff mode."); + return; + } + + let multiplier = property.data.multiplier !== undefined ? property.data.multiplier : 1; + + let applyDelta = step * multiplier; + + if (selectedEntityIDs.size !== propertyMultiValue.values.length) { + console.log("selectedEntityIDs and propertyMultiValue got out of sync."); + return; + } + let updateObjects = []; + const selectedEntityIDsArray = [...selectedEntityIDs]; + + for (let i = 0; i < selectedEntityIDsArray.length; ++i) { + let entityID = selectedEntityIDsArray[i]; + + let updatedValue; + if (propertyComponent !== undefined) { + let objectToUpdate = propertyMultiValue.values[i]; + objectToUpdate[propertyComponent] += applyDelta; + updatedValue = objectToUpdate; + } else { + updatedValue = propertyMultiValue.values[i] + applyDelta; + } + let propertiesUpdate = createPropertyUpdateObject(property.name, updatedValue); + updateObjects.push({ + entityIDs: [entityID], + properties: propertiesUpdate + }); + // We need to store these so that we can send a full update on the dragEnd + mergeDeep(currentSelections[i].properties, propertiesUpdate); + } + + updateMultiDiffProperties(updateObjects, !shouldAddToUndoHistory); + } +} + function createNumberDraggableProperty(property, elProperty) { let elementID = property.elementID; let propertyData = property.data; - + elProperty.className += " draggable-number-container"; let dragStartFunction = createDragStartFunction(property); let dragEndFunction = createDragEndFunction(property); let elDraggableNumber = new DraggableNumber(propertyData.min, propertyData.max, propertyData.step, propertyData.decimals, dragStartFunction, dragEndFunction); - - let defaultValue = propertyData.defaultValue; - if (defaultValue !== undefined) { - elDraggableNumber.elInput.value = defaultValue; + + let defaultValue = propertyData.defaultValue; + if (defaultValue !== undefined) { + elDraggableNumber.elInput.value = defaultValue; } let valueChangeFunction = createEmitNumberPropertyUpdateFunction(property); elDraggableNumber.setValueChangeFunction(valueChangeFunction); + + elDraggableNumber.setMultiDiffStepFunction(createMultiDiffStepFunction(property)); elDraggableNumber.elInput.setAttribute("id", elementID); elProperty.appendChild(elDraggableNumber.elDiv); @@ -2165,11 +2464,15 @@ function createRectProperty(property, elProperty) { elWidthHeightRow.appendChild(elNumberWidth.elDiv); elWidthHeightRow.appendChild(elNumberHeight.elDiv); - let valueChangeFunction = createEmitRectPropertyUpdateFunction(property); - elNumberX.setValueChangeFunction(valueChangeFunction); - elNumberY.setValueChangeFunction(valueChangeFunction); - elNumberWidth.setValueChangeFunction(valueChangeFunction); - elNumberHeight.setValueChangeFunction(valueChangeFunction); + elNumberX.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'x')); + elNumberY.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'y')); + elNumberWidth.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'width')); + elNumberHeight.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'height')); + + elNumberX.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'x')); + elNumberY.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'y')); + elNumberX.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'width')); + elNumberY.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'height')); let elResult = []; elResult[RECT_ELEMENTS.X_NUMBER] = elNumberX; @@ -2199,11 +2502,14 @@ function createVec3Property(property, elProperty) { elProperty.appendChild(elNumberX.elDiv); elProperty.appendChild(elNumberY.elDiv); elProperty.appendChild(elNumberZ.elDiv); - - let valueChangeFunction = createEmitVec3PropertyUpdateFunction(property); - elNumberX.setValueChangeFunction(valueChangeFunction); - elNumberY.setValueChangeFunction(valueChangeFunction); - elNumberZ.setValueChangeFunction(valueChangeFunction); + + elNumberX.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'x')); + elNumberY.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'y')); + elNumberZ.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'z')); + + elNumberX.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'x')); + elNumberY.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'y')); + elNumberZ.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'z')); let elResult = []; elResult[VECTOR_ELEMENTS.X_NUMBER] = elNumberX; @@ -2226,10 +2532,12 @@ function createVec2Property(property, elProperty) { let elNumberY = createTupleNumberInput(property, propertyData.subLabels[VECTOR_ELEMENTS.Y_NUMBER]); elProperty.appendChild(elNumberX.elDiv); elProperty.appendChild(elNumberY.elDiv); - - let valueChangeFunction = createEmitVec2PropertyUpdateFunction(property); - elNumberX.setValueChangeFunction(valueChangeFunction); - elNumberY.setValueChangeFunction(valueChangeFunction); + + elNumberX.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'x')); + elNumberY.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'y')); + + elNumberX.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'x')); + elNumberY.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'y')); let elResult = []; elResult[VECTOR_ELEMENTS.X_NUMBER] = elNumberX; @@ -2293,7 +2601,6 @@ function createColorProperty(property, elProperty) { color: '000000', submit: false, // We don't want to have a submission button onShow: function(colpick) { - console.log("Showing"); // The original color preview within the picker needs to be updated on show because // prior to the picker being shown we don't have access to the selections' starting color. colorPickers[colorPickerID].colpickSetColor({ @@ -2342,7 +2649,7 @@ function createDropdownProperty(property, propertyID, elProperty) { } elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(property)); - + elProperty.appendChild(elInput); return elInput; @@ -2361,8 +2668,12 @@ function createTextareaProperty(property, elProperty) { } elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(property)); - + + let elMultiDiff = document.createElement('span'); + elMultiDiff.className = "multi-diff"; + elProperty.appendChild(elInput); + elProperty.appendChild(elMultiDiff); if (propertyData.buttons !== undefined) { addButtons(elProperty, elementID, propertyData.buttons, true); @@ -2373,8 +2684,7 @@ function createTextareaProperty(property, elProperty) { function createIconProperty(property, elProperty) { let elementID = property.elementID; - let propertyData = property.data; - + elProperty.className = "value"; let elSpan = document.createElement('span'); @@ -2401,6 +2711,7 @@ function createTextureProperty(property, elProperty) { elInput.setAttribute("type", "text"); let imageLoad = function(url) { + elDiv.style.display = null; if (url.slice(0, 5).toLowerCase() === "atp:/") { elImage.src = ""; elImage.style.display = "none"; @@ -2422,12 +2733,18 @@ function createTextureProperty(property, elProperty) { } }; elInput.imageLoad = imageLoad; + elInput.setMultipleValues = function() { + elDiv.style.display = "none"; + }; elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(property)); elInput.addEventListener('change', function(ev) { imageLoad(ev.target.value); }); elProperty.appendChild(elInput); + let elMultiDiff = document.createElement('span'); + elMultiDiff.className = "multi-diff"; + elProperty.appendChild(elMultiDiff); elProperty.appendChild(elDiv); let elResult = []; @@ -2457,7 +2774,7 @@ function createDynamicMultiselectProperty(property, elProperty) { let elDivOptions = document.createElement('div'); elDivOptions.setAttribute("id", elementID + "-options"); - elDivOptions.style = "overflow-y:scroll;max-height:160px;" + elDivOptions.style = "overflow-y:scroll;max-height:160px;"; let elDivButtons = document.createElement('div'); elDivButtons.setAttribute("id", elDivOptions.getAttribute("id") + "-buttons"); @@ -2633,7 +2950,7 @@ function createProperty(propertyData, propertyElementID, propertyName, propertyI */ function parentIDChanged() { - if (selectedEntityProperties.type === "Material") { + if (currentSelections.length === 1 && currentSelections[0].properties.type === "Material") { requestMaterialTarget(); } } @@ -2710,6 +3027,7 @@ function clearUserData() { } function newJSONEditor() { + getPropertyInputElement("userData").classList.remove('multi-diff'); deleteJSONEditor(); createJSONEditor(); let data = {}; @@ -2719,8 +3037,11 @@ function newJSONEditor() { showSaveUserDataButton(); } -function saveUserData() { - saveJSONUserData(true); +/** + * @param {Set.} [entityIDsToUpdate] Entity IDs to update userData for. + */ +function saveUserData(entityIDsToUpdate) { + saveJSONUserData(true, entityIDsToUpdate); } function setJSONError(property, isError) { @@ -2730,11 +3051,14 @@ function setJSONError(property, isError) { $propertyUserDataEditorStatus.text(isError ? 'Invalid JSON code - look for red X in your code' : ''); } -function setUserDataFromEditor(noUpdate) { - let json = null; +/** + * @param {boolean} noUpdate - don't update the UI, but do send a property update. + * @param {Set.} [entityIDsToUpdate] - Entity IDs to update userData for. + */ +function setUserDataFromEditor(noUpdate, entityIDsToUpdate) { let errorFound = false; try { - json = editor.get(); + editor.get(); } catch (e) { errorFound = true; } @@ -2749,7 +3073,7 @@ function setUserDataFromEditor(noUpdate) { if (noUpdate) { EventBridge.emitWebEvent( JSON.stringify({ - id: lastEntityID, + ids: [...entityIDsToUpdate], type: "saveUserData", properties: { userData: text @@ -2761,61 +3085,6 @@ function setUserDataFromEditor(noUpdate) { } } -function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults, removeKeys) { - let propertyUpdate = {}; - let parsedData = {}; - let keysToBeRemoved = removeKeys ? removeKeys : []; - try { - if ($('#property-userData-editor').css('height') !== "0px") { - // if there is an expanded, we want to use its json. - parsedData = getEditorJSON(); - } else { - parsedData = JSON.parse(userDataElement.value); - } - } catch (e) { - // TODO: Should an alert go here? - } - - if (!(groupName in parsedData)) { - parsedData[groupName] = {}; - } - let keys = Object.keys(updateKeyPair); - keys.forEach(function (key) { - if (updateKeyPair[key] !== null && updateKeyPair[key] !== "null") { - if (updateKeyPair[key] instanceof Element) { - if (updateKeyPair[key].type === "checkbox") { - parsedData[groupName][key] = updateKeyPair[key].checked; - } else { - let val = isNaN(updateKeyPair[key].value) ? updateKeyPair[key].value : parseInt(updateKeyPair[key].value); - parsedData[groupName][key] = val; - } - } else { - parsedData[groupName][key] = updateKeyPair[key]; - } - } else if (defaults[key] !== null && defaults[key] !== "null") { - parsedData[groupName][key] = defaults[key]; - } - }); - keysToBeRemoved.forEach(function(key) { - if (parsedData[groupName].hasOwnProperty(key)) { - delete parsedData[groupName][key]; - } - }); - - if (Object.keys(parsedData[groupName]).length === 0) { - delete parsedData[groupName]; - } - if (Object.keys(parsedData).length > 0) { - propertyUpdate.userData = JSON.stringify(parsedData); - } else { - propertyUpdate.userData = ''; - } - - userDataElement.value = propertyUpdate.userData; - - updateProperties(propertyUpdate, false); -} - let editor = null; function createJSONEditor() { @@ -2825,9 +3094,6 @@ function createJSONEditor() { mode: 'tree', modes: ['code', 'tree'], name: 'userData', - onModeChange: function() { - $('.jsoneditor-poweredBy').remove(); - }, onError: function(e) { alert('JSON editor:' + e); }, @@ -2838,8 +3104,6 @@ function createJSONEditor() { return; } $('#property-userData-button-save').attr('disabled', false); - - } }; editor = new JSONEditor(container, options); @@ -2897,10 +3161,6 @@ function setEditorJSON(json) { } } -function getEditorJSON() { - return editor.get(); -} - function deleteJSONEditor() { if (editor !== null) { setJSONError('userData', false); @@ -2911,8 +3171,12 @@ function deleteJSONEditor() { let savedJSONTimer = null; -function saveJSONUserData(noUpdate) { - setUserDataFromEditor(noUpdate); +/** + * @param {boolean} noUpdate - don't update the UI, but do send a property update. + * @param {Set.} [entityIDsToUpdate] Entity IDs to update userData for + */ +function saveJSONUserData(noUpdate, entityIDsToUpdate) { + setUserDataFromEditor(noUpdate, entityIDsToUpdate ? entityIDsToUpdate : selectedEntityIDs); $('#property-userData-saved').show(); $('#property-userData-button-save').attr('disabled', true); if (savedJSONTimer !== null) { @@ -2939,6 +3203,7 @@ function clearMaterialData() { } function newJSONMaterialEditor() { + getPropertyInputElement("materialData").classList.remove('multi-diff'); deleteJSONMaterialEditor(); createJSONMaterialEditor(); let data = {}; @@ -2952,11 +3217,14 @@ function saveMaterialData() { saveJSONMaterialData(true); } -function setMaterialDataFromEditor(noUpdate) { - let json = null; +/** + * @param {boolean} noUpdate - don't update the UI, but do send a property update. + * @param {Set.} [entityIDsToUpdate] - Entity IDs to update materialData for. + */ +function setMaterialDataFromEditor(noUpdate, entityIDsToUpdate) { let errorFound = false; try { - json = materialEditor.get(); + materialEditor.get(); } catch (e) { errorFound = true; } @@ -2970,7 +3238,7 @@ function setMaterialDataFromEditor(noUpdate) { if (noUpdate) { EventBridge.emitWebEvent( JSON.stringify({ - id: lastEntityID, + ids: [...entityIDsToUpdate], type: "saveMaterialData", properties: { materialData: text @@ -2991,9 +3259,6 @@ function createJSONMaterialEditor() { mode: 'tree', modes: ['code', 'tree'], name: 'materialData', - onModeChange: function() { - $('.jsoneditor-poweredBy').remove(); - }, onError: function(e) { alert('JSON editor:' + e); }, @@ -3004,8 +3269,6 @@ function createJSONMaterialEditor() { return; } $('#property-materialData-button-save').attr('disabled', false); - - } }; materialEditor = new JSONEditor(container, options); @@ -3063,10 +3326,6 @@ function setMaterialEditorJSON(json) { } } -function getMaterialEditorJSON() { - return materialEditor.get(); -} - function deleteJSONMaterialEditor() { if (materialEditor !== null) { setJSONError('materialData', false); @@ -3077,8 +3336,12 @@ function deleteJSONMaterialEditor() { let savedMaterialJSONTimer = null; -function saveJSONMaterialData(noUpdate) { - setMaterialDataFromEditor(noUpdate); +/** + * @param {boolean} noUpdate - don't update the UI, but do send a property update. + * @param {Set.} [entityIDsToUpdate] - Entity IDs to update materialData for. + */ +function saveJSONMaterialData(noUpdate, entityIDsToUpdate) { + setMaterialDataFromEditor(noUpdate, entityIDsToUpdate ? entityIDsToUpdate : selectedEntityIDs); $('#property-materialData-saved').show(); $('#property-materialData-button-save').attr('disabled', true); if (savedMaterialJSONTimer !== null) { @@ -3101,13 +3364,12 @@ function bindAllNonJSONEditorElements() { if (e.target.id === "property-userData-button-edit" || e.target.id === "property-userData-button-clear" || e.target.id === "property-materialData-button-edit" || e.target.id === "property-materialData-button-clear") { return; - } else { - if ($('#property-userData-editor').css('height') !== "0px") { - saveUserData(); - } - if ($('#property-materialData-editor').css('height') !== "0px") { - saveMaterialData(); - } + } + if ($('#property-userData-editor').css('height') !== "0px") { + saveUserData(); + } + if ($('#property-materialData-editor').css('height') !== "0px") { + saveMaterialData(); } }); } @@ -3140,14 +3402,14 @@ function toggleDropdown(event) { } function closeAllDropdowns() { - elDropdowns = document.querySelectorAll("div.dropdown > dl"); + let elDropdowns = document.querySelectorAll("div.dropdown > dl"); for (let i = 0; i < elDropdowns.length; ++i) { elDropdowns[i].setAttribute('dropped', 'false'); } } function setDropdownValue(event) { - let dt = event.target.parentNode.parentNode.previousSibling; + let dt = event.target.parentNode.parentNode.previousSibling.previousSibling; dt.value = event.target.getAttribute("value"); dt.firstChild.textContent = event.target.textContent; @@ -3174,7 +3436,10 @@ function setTextareaScrolling(element) { */ function requestMaterialTarget() { - EventBridge.emitWebEvent(JSON.stringify({ type: 'materialTargetRequest', entityID: selectedEntityProperties.id })); + EventBridge.emitWebEvent(JSON.stringify({ + type: 'materialTargetRequest', + entityID: getFirstSelectedID(), + })); } function setMaterialTargetData(materialTargetData) { @@ -3268,7 +3533,7 @@ function sendMaterialTargetProperty() { if (elInput.checked) { let targetID = elInput.getAttribute("targetID"); if (elInput.getAttribute("isMaterialName") === "true") { - materialTargetList.push("mat::" + targetID); + materialTargetList.push(MATERIAL_PREFIX_STRING + targetID); } else { materialTargetList.push(targetID); } @@ -3300,7 +3565,7 @@ function materialTargetPropertyUpdate(propertyValue) { let targetID = elInput.getAttribute("targetID"); let materialTargetName = targetID; if (elInput.getAttribute("isMaterialName") === "true") { - materialTargetName = "mat::" + targetID; + materialTargetName = MATERIAL_PREFIX_STRING + targetID; } elInput.checked = materialTargets.indexOf(materialTargetName) >= 0; } @@ -3308,12 +3573,358 @@ function materialTargetPropertyUpdate(propertyValue) { elDivOptions.propertyValue = propertyValue; } +function roundAndFixNumber(number, propertyData) { + let result = number; + if (propertyData.round !== undefined) { + result = Math.round(result * propertyData.round) / propertyData.round; + } + if (propertyData.decimals !== undefined) { + return result.toFixed(propertyData.decimals) + } + return result; +} + +function applyInputNumberPropertyModifiers(number, propertyData) { + const multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; + return roundAndFixNumber(number / multiplier, propertyData); +} + +function applyOutputNumberPropertyModifiers(number, propertyData) { + const multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; + return roundAndFixNumber(number * multiplier, propertyData); +} + +const areSetsEqual = (a, b) => a.size === b.size && [...a].every(value => b.has(value)); + + +function handleEntitySelectionUpdate(selections, isPropertiesToolUpdate) { + const previouslySelectedEntityIDs = selectedEntityIDs; + currentSelections = selections; + selectedEntityIDs = new Set(selections.map(selection => selection.id)); + const multipleSelections = currentSelections.length > 1; + const hasSelectedEntityChanged = !areSetsEqual(selectedEntityIDs, previouslySelectedEntityIDs); + + if (selections.length === 0) { + deleteJSONEditor(); + deleteJSONMaterialEditor(); + + resetProperties(); + showGroupsForType("None"); + + let elIcon = properties.type.elSpan; + elIcon.innerText = NO_SELECTION; + elIcon.style.display = 'inline-block'; + + getPropertyInputElement("userData").value = ""; + showUserDataTextArea(); + showSaveUserDataButton(); + showNewJSONEditorButton(); + + getPropertyInputElement("materialData").value = ""; + showMaterialDataTextArea(); + showSaveMaterialDataButton(); + showNewJSONMaterialEditorButton(); + + disableProperties(); + } else { + if (!isPropertiesToolUpdate && !hasSelectedEntityChanged && document.hasFocus()) { + // in case the selection has not changed and we still have focus on the properties page, + // we will ignore the event. + return; + } + + if (hasSelectedEntityChanged) { + if (!multipleSelections) { + resetServerScriptStatus(); + } + } + + const doSelectElement = !hasSelectedEntityChanged; + + // Get unique entity types, and convert the types Sphere and Box to Shape + const shapeTypes = ["Sphere", "Box"]; + const entityTypes = [...new Set(currentSelections.map(a => + shapeTypes.includes(a.properties.type) ? "Shape" : a.properties.type))]; + + const shownGroups = getGroupsForTypes(entityTypes); + showGroupsForTypes(entityTypes); + + const lockedMultiValue = getMultiplePropertyValue('locked'); + + if (lockedMultiValue.isMultiDiffValue || lockedMultiValue.value) { + disableProperties(); + getPropertyInputElement('locked').removeAttribute('disabled'); + } else { + enableProperties(); + disableSaveUserDataButton(); + disableSaveMaterialDataButton() + } + + const certificateIDMultiValue = getMultiplePropertyValue('certificateID'); + const hasCertifiedInSelection = certificateIDMultiValue.isMultiDiffValue || certificateIDMultiValue.value !== ""; + + Object.entries(properties).forEach(function([propertyID, property]) { + const propertyData = property.data; + const propertyName = property.name; + let propertyMultiValue = getMultiplePropertyValue(propertyName); + let isMultiDiffValue = propertyMultiValue.isMultiDiffValue; + let propertyValue = propertyMultiValue.value; + + if (propertyData.selectionVisibility !== undefined) { + let visibility = propertyData.selectionVisibility; + let propertyVisible = true; + if (!multipleSelections) { + propertyVisible = isFlagSet(visibility, PROPERTY_SELECTION_VISIBILITY.SINGLE_SELECTION); + } else if (isMultiDiffValue) { + propertyVisible = isFlagSet(visibility, PROPERTY_SELECTION_VISIBILITY.MULTI_DIFF_SELECTIONS); + } else { + propertyVisible = isFlagSet(visibility, PROPERTY_SELECTION_VISIBILITY.MULTIPLE_SELECTIONS); + } + setPropertyVisibility(property, propertyVisible); + } + + const isSubProperty = propertyData.subPropertyOf !== undefined; + if (propertyValue === undefined && !isMultiDiffValue && !isSubProperty) { + return; + } + + if (!shownGroups.includes(property.group_id)) { + const WANT_DEBUG_SHOW_HIDDEN_FROM_GROUPS = false; + if (WANT_DEBUG_SHOW_HIDDEN_FROM_GROUPS) { + console.log("Skipping property " + property.data.label + " [" + property.name + + "] from hidden group " + property.group_id); + } + return; + } + + if (propertyData.hideIfCertified && hasCertifiedInSelection) { + propertyValue = "** Certified **"; + property.elInput.disabled = true; + } + + if (propertyName === "type") { + propertyValue = entityTypes.length > 1 ? "Multiple" : propertyMultiValue.values[0]; + } + + switch (propertyData.type) { + case 'string': { + if (isMultiDiffValue) { + if (propertyData.readOnly && propertyData.multiDisplayMode + && propertyData.multiDisplayMode === PROPERTY_MULTI_DISPLAY_MODE.COMMA_SEPARATED_VALUES) { + property.elInput.value = propertyMultiValue.values.join(", "); + } else { + property.elInput.classList.add('multi-diff'); + property.elInput.value = ""; + } + } else { + property.elInput.classList.remove('multi-diff'); + property.elInput.value = propertyValue; + } + break; + } + case 'bool': { + const inverse = propertyData.inverse !== undefined ? propertyData.inverse : false; + if (isSubProperty) { + let subPropertyMultiValue = getMultiplePropertyValue(propertyData.subPropertyOf); + let propertyValue = subPropertyMultiValue.value; + isMultiDiffValue = subPropertyMultiValue.isMultiDiffValue; + if (isMultiDiffValue) { + let detailedSubProperty = getDetailedSubPropertyMPVDiff(subPropertyMultiValue, propertyName); + property.elInput.checked = detailedSubProperty.isChecked; + property.elInput.classList.toggle('multi-diff', detailedSubProperty.isMultiDiff); + } else { + let subProperties = propertyValue.split(","); + let subPropertyValue = subProperties.indexOf(propertyName) > -1; + property.elInput.checked = inverse ? !subPropertyValue : subPropertyValue; + property.elInput.classList.remove('multi-diff'); + } + + } else { + if (isMultiDiffValue) { + property.elInput.checked = false; + } else { + property.elInput.checked = inverse ? !propertyValue : propertyValue; + } + property.elInput.classList.toggle('multi-diff', isMultiDiffValue); + } + + break; + } + case 'number': { + property.elInput.value = isMultiDiffValue ? "" : propertyValue; + property.elInput.classList.toggle('multi-diff', isMultiDiffValue); + break; + } + case 'number-draggable': { + let detailedNumberDiff = getDetailedNumberMPVDiff(propertyMultiValue, propertyData); + property.elNumber.setValue(detailedNumberDiff.averagePerPropertyComponent[0], detailedNumberDiff.propertyComponentDiff[0]); + break; + } + case 'rect': { + let detailedNumberDiff = getDetailedNumberMPVDiff(propertyMultiValue, propertyData); + property.elNumberX.setValue(detailedNumberDiff.averagePerPropertyComponent.x, detailedNumberDiff.propertyComponentDiff.x); + property.elNumberY.setValue(detailedNumberDiff.averagePerPropertyComponent.y, detailedNumberDiff.propertyComponentDiff.y); + property.elNumberWidth.setValue(detailedNumberDiff.averagePerPropertyComponent.width, detailedNumberDiff.propertyComponentDiff.width); + property.elNumberHeight.setValue(detailedNumberDiff.averagePerPropertyComponent.height, detailedNumberDiff.propertyComponentDiff.height); + break; + } + case 'vec3': + case 'vec2': { + let detailedNumberDiff = getDetailedNumberMPVDiff(propertyMultiValue, propertyData); + property.elNumberX.setValue(detailedNumberDiff.averagePerPropertyComponent.x, detailedNumberDiff.propertyComponentDiff.x); + property.elNumberY.setValue(detailedNumberDiff.averagePerPropertyComponent.y, detailedNumberDiff.propertyComponentDiff.y); + if (property.elNumberZ !== undefined) { + property.elNumberZ.setValue(detailedNumberDiff.averagePerPropertyComponent.z, detailedNumberDiff.propertyComponentDiff.z); + } + break; + } + case 'color': { + let displayColor = propertyMultiValue.isMultiDiffValue ? propertyMultiValue.values[0] : propertyValue; + property.elColorPicker.style.backgroundColor = "rgb(" + displayColor.red + "," + + displayColor.green + "," + + displayColor.blue + ")"; + property.elColorPicker.classList.toggle('multi-diff', propertyMultiValue.isMultiDiffValue); + + if (hasSelectedEntityChanged && $(property.elColorPicker).attr('active') === 'true') { + // Set the color picker inactive before setting the color, + // otherwise an update will be sent directly after setting it here. + $(property.elColorPicker).attr('active', 'false'); + colorPickers['#' + property.elementID].colpickSetColor({ + "r": displayColor.red, + "g": displayColor.green, + "b": displayColor.blue + }); + $(property.elColorPicker).attr('active', 'true'); + } + + property.elNumberR.setValue(displayColor.red); + property.elNumberG.setValue(displayColor.green); + property.elNumberB.setValue(displayColor.blue); + break; + } + case 'dropdown': { + property.elInput.classList.toggle('multi-diff', isMultiDiffValue); + property.elInput.value = isMultiDiffValue ? "" : propertyValue; + setDropdownText(property.elInput); + break; + } + case 'textarea': { + property.elInput.value = propertyValue; + setTextareaScrolling(property.elInput); + break; + } + case 'icon': { + property.elSpan.innerHTML = propertyData.icons[propertyValue]; + property.elSpan.style.display = "inline-block"; + break; + } + case 'texture': { + property.elInput.value = isMultiDiffValue ? "" : propertyValue; + property.elInput.classList.toggle('multi-diff', isMultiDiffValue); + if (isMultiDiffValue) { + property.elInput.setMultipleValues(); + } else { + property.elInput.imageLoad(property.elInput.value); + } + break; + } + case 'dynamic-multiselect': { + if (!isMultiDiffValue && property.data.propertyUpdate) { + property.data.propertyUpdate(propertyValue); + } + break; + } + } + + let showPropertyRules = property.showPropertyRules; + if (showPropertyRules !== undefined) { + for (let propertyToShow in showPropertyRules) { + let showIfThisPropertyValue = showPropertyRules[propertyToShow]; + let show = String(propertyValue) === String(showIfThisPropertyValue); + showPropertyElement(propertyToShow, show); + } + } + }); + + updateVisibleSpaceModeProperties(); + + let userDataMultiValue = getMultiplePropertyValue("userData"); + let userDataTextArea = getPropertyInputElement("userData"); + let json = null; + if (!userDataMultiValue.isMultiDiffValue) { + try { + json = JSON.parse(userDataMultiValue.value); + } catch (e) { + + } + } + if (json !== null) { + if (editor === null) { + createJSONEditor(); + } + userDataTextArea.classList.remove('multi-diff'); + setEditorJSON(json); + showSaveUserDataButton(); + hideUserDataTextArea(); + hideNewJSONEditorButton(); + hideUserDataSaved(); + } else { + // normal text + deleteJSONEditor(); + userDataTextArea.classList.toggle('multi-diff', userDataMultiValue.isMultiDiffValue); + userDataTextArea.value = userDataMultiValue.isMultiDiffValue ? "" : userDataMultiValue.value; + + showUserDataTextArea(); + showNewJSONEditorButton(); + hideSaveUserDataButton(); + hideUserDataSaved(); + } + + let materialDataMultiValue = getMultiplePropertyValue("materialData"); + let materialDataTextArea = getPropertyInputElement("materialData"); + let materialJson = null; + if (!materialDataMultiValue.isMultiDiffValue) { + try { + materialJson = JSON.parse(materialDataMultiValue.value); + } catch (e) { + + } + } + if (materialJson !== null) { + if (materialEditor === null) { + createJSONMaterialEditor(); + } + materialDataTextArea.classList.remove('multi-diff'); + setMaterialEditorJSON(materialJson); + showSaveMaterialDataButton(); + hideMaterialDataTextArea(); + hideNewJSONMaterialEditorButton(); + hideMaterialDataSaved(); + } else { + // normal text + deleteJSONMaterialEditor(); + materialDataTextArea.classList.toggle('multi-diff', materialDataMultiValue.isMultiDiffValue); + materialDataTextArea.value = materialDataMultiValue.isMultiDiffValue ? "" : materialDataMultiValue.value; + showMaterialDataTextArea(); + showNewJSONMaterialEditorButton(); + hideSaveMaterialDataButton(); + hideMaterialDataSaved(); + } + + if (hasSelectedEntityChanged && selections.length === 1 && entityTypes[0] === "Material") { + requestMaterialTarget(); + } + + let activeElement = document.activeElement; + if (doSelectElement && typeof activeElement.select !== "undefined") { + activeElement.select(); + } + } +} function loaded() { openEventBridge(function() { let elPropertiesList = document.getElementById("properties-list"); - - let templatePropertyRow = document.getElementById('property-row'); GROUPS.forEach(function(group) { let elGroup; @@ -3420,6 +4031,7 @@ function loaded() { property.isParticleProperty = group.id.includes("particles"); property.elContainer = elContainer; property.spaceMode = propertySpaceMode; + property.group_id = group.id; let elLabel = createElementFromHTML(`
${innerPropertyData.label}
`); createAppTooltip.registerTooltipElement(elLabel, propertyID, propertyName); @@ -3438,6 +4050,7 @@ function loaded() { property.isParticleProperty = group.id.includes("particles"); property.elContainer = elContainer; property.spaceMode = propertySpaceMode; + property.group_id = group.id; if (property.type !== 'placeholder') { properties[propertyID] = property; @@ -3473,7 +4086,7 @@ function loaded() { if (window.EventBridge !== undefined) { EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); - if (data.type === "server_script_status") { + if (data.type === "server_script_status" && selectedEntityIDs.size === 1) { let elServerScriptError = document.getElementById("property-serverScripts-error"); let elServerScriptStatus = document.getElementById("property-serverScripts-status"); elServerScriptError.value = data.errorInfo; @@ -3492,324 +4105,7 @@ function loaded() { if (data.spaceMode !== undefined) { currentSpaceMode = data.spaceMode === "local" ? PROPERTY_SPACE_MODE.LOCAL : PROPERTY_SPACE_MODE.WORLD; } - if (data.selections.length === 0) { - if (lastEntityID !== null) { - if (editor !== null) { - saveUserData(); - deleteJSONEditor(); - } - if (materialEditor !== null) { - saveMaterialData(); - deleteJSONMaterialEditor(); - } - } - lastEntityID = null; - - resetProperties(); - showGroupsForType("None"); - - let elIcon = properties.type.elSpan; - elIcon.innerText = NO_SELECTION; - elIcon.style.display = 'inline-block'; - - deleteJSONEditor(); - getPropertyInputElement("userData").value = ""; - showUserDataTextArea(); - showSaveUserDataButton(); - showNewJSONEditorButton(); - - deleteJSONMaterialEditor(); - getPropertyInputElement("materialData").value = ""; - showMaterialDataTextArea(); - showSaveMaterialDataButton(); - showNewJSONMaterialEditorButton(); - - disableProperties(); - } else if (data.selections.length > 1) { - deleteJSONEditor(); - deleteJSONMaterialEditor(); - - let selections = data.selections; - - let ids = []; - let types = {}; - let numTypes = 0; - - for (let i = 0; i < selections.length; ++i) { - ids.push(selections[i].id); - let currentSelectedType = selections[i].properties.type; - if (types[currentSelectedType] === undefined) { - types[currentSelectedType] = 0; - numTypes += 1; - } - types[currentSelectedType]++; - } - - let type = "Multiple"; - if (numTypes === 1) { - type = selections[0].properties.type; - } - - resetProperties(); - showGroupsForType(type); - - let typeProperty = properties["type"]; - typeProperty.elSpan.innerHTML = typeProperty.data.icons[type]; - typeProperty.elSpan.style.display = "inline-block"; - - disableProperties(); - } else { - selectedEntityProperties = data.selections[0].properties; - - if (lastEntityID !== '"' + selectedEntityProperties.id + '"' && lastEntityID !== null) { - if (editor !== null) { - saveUserData(); - } - if (materialEditor !== null) { - saveMaterialData(); - } - } - - let hasSelectedEntityChanged = lastEntityID !== '"' + selectedEntityProperties.id + '"'; - - if (!data.isPropertiesToolUpdate && !hasSelectedEntityChanged && document.hasFocus()) { - // in case the selection has not changed and we still have focus on the properties page, - // we will ignore the event. - return; - } - - let doSelectElement = !hasSelectedEntityChanged; - - // the event bridge and json parsing handle our avatar id string differently. - lastEntityID = '"' + selectedEntityProperties.id + '"'; - - showGroupsForType(selectedEntityProperties.type); - - if (selectedEntityProperties.locked) { - disableProperties(); - getPropertyInputElement("locked").removeAttribute('disabled'); - } else { - enableProperties(); - disableSaveUserDataButton(); - disableSaveMaterialDataButton() - } - - for (let propertyID in properties) { - let property = properties[propertyID]; - let propertyData = property.data; - let propertyName = property.name; - let propertyValue = getPropertyValue(propertyName); - - let isSubProperty = propertyData.subPropertyOf !== undefined; - if (propertyValue === undefined && !isSubProperty) { - continue; - } - - if (propertyData.hideIfCertified) { - let shouldHide = selectedEntityProperties.certificateID !== ""; - if (shouldHide) { - propertyValue = "** Certified **"; - property.elInput.disabled = true; - } - } - - let isPropertyNotNumber = false; - switch (propertyData.type) { - case 'number': - case 'number-draggable': - isPropertyNotNumber = isNaN(propertyValue) || propertyValue === null; - break; - case 'rect': - case 'vec3': - case 'vec2': - isPropertyNotNumber = isNaN(propertyValue.x) || propertyValue.x === null; - break; - case 'color': - isPropertyNotNumber = isNaN(propertyValue.red) || propertyValue.red === null; - break; - } - if (isPropertyNotNumber && propertyData.fallbackProperty !== undefined) { - propertyValue = getPropertyValue(propertyData.fallbackProperty); - } - - switch (propertyData.type) { - case 'string': { - property.elInput.value = propertyValue; - break; - } - case 'bool': { - let inverse = propertyData.inverse !== undefined ? propertyData.inverse : false; - if (isSubProperty) { - let propertyValue = selectedEntityProperties[propertyData.subPropertyOf]; - let subProperties = propertyValue.split(","); - let subPropertyValue = subProperties.indexOf(propertyName) > -1; - property.elInput.checked = inverse ? !subPropertyValue : subPropertyValue; - } else { - property.elInput.checked = inverse ? !propertyValue : propertyValue; - } - break; - } - case 'number': { - property.elInput.value = propertyValue; - break; - } - case 'number-draggable': { - let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; - let value = propertyValue / multiplier; - if (propertyData.round !== undefined) { - value = Math.round(value.round) / propertyData.round; - } - property.elNumber.setValue(value); - break; - } - case 'rect': - property.elNumberX.setValue(propertyValue.x); - property.elNumberY.setValue(propertyValue.y); - property.elNumberWidth.setValue(propertyValue.width); - property.elNumberHeight.setValue(propertyValue.height); - break; - case 'vec3': - case 'vec2': { - let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; - let valueX = propertyValue.x / multiplier; - let valueY = propertyValue.y / multiplier; - let valueZ = propertyValue.z / multiplier; - if (propertyData.round !== undefined) { - valueX = Math.round(valueX * propertyData.round) / propertyData.round; - valueY = Math.round(valueY * propertyData.round) / propertyData.round; - valueZ = Math.round(valueZ * propertyData.round) / propertyData.round; - } - if (propertyData.decimals !== undefined) { - property.elNumberX.setValue(valueX.toFixed(propertyData.decimals)); - property.elNumberY.setValue(valueY.toFixed(propertyData.decimals)); - if (property.elNumberZ !== undefined) { - property.elNumberZ.setValue(valueZ.toFixed(propertyData.decimals)); - } - } else { - property.elNumberX.setValue(valueX); - property.elNumberY.setValue(valueY); - if (property.elNumberZ !== undefined) { - property.elNumberZ.setValue(valueZ); - } - } - break; - } - case 'color': { - property.elColorPicker.style.backgroundColor = "rgb(" + propertyValue.red + "," + - propertyValue.green + "," + - propertyValue.blue + ")"; - if (hasSelectedEntityChanged && $(property.elColorPicker).attr('active') === 'true') { - // Set the color picker inactive before setting the color, - // otherwise an update will be sent directly after setting it here. - $(property.elColorPicker).attr('active', 'false'); - colorPickers['#' + property.elementID].colpickSetColor({ - "r": propertyValue.red, - "g": propertyValue.green, - "b": propertyValue.blue - }); - $(property.elColorPicker).attr('active', 'true'); - } - - property.elNumberR.setValue(propertyValue.red); - property.elNumberG.setValue(propertyValue.green); - property.elNumberB.setValue(propertyValue.blue); - break; - } - case 'dropdown': { - property.elInput.value = propertyValue; - setDropdownText(property.elInput); - break; - } - case 'textarea': { - property.elInput.value = propertyValue; - setTextareaScrolling(property.elInput); - break; - } - case 'icon': { - property.elSpan.innerHTML = propertyData.icons[propertyValue]; - property.elSpan.style.display = "inline-block"; - break; - } - case 'texture': { - property.elInput.value = propertyValue; - property.elInput.imageLoad(property.elInput.value); - break; - } - case 'dynamic-multiselect': { - if (property.data.propertyUpdate) { - property.data.propertyUpdate(propertyValue); - } - break; - } - } - - let showPropertyRules = property.showPropertyRules; - if (showPropertyRules !== undefined) { - for (let propertyToShow in showPropertyRules) { - let showIfThisPropertyValue = showPropertyRules[propertyToShow]; - let show = String(propertyValue) === String(showIfThisPropertyValue); - showPropertyElement(propertyToShow, show); - } - } - } - - updateVisibleSpaceModeProperties(); - - let json = null; - try { - json = JSON.parse(selectedEntityProperties.userData); - } catch (e) { - // normal text - deleteJSONEditor(); - getPropertyInputElement("userData").value = selectedEntityProperties.userData; - showUserDataTextArea(); - showNewJSONEditorButton(); - hideSaveUserDataButton(); - hideUserDataSaved(); - } - if (json !== null) { - if (editor === null) { - createJSONEditor(); - } - setEditorJSON(json); - showSaveUserDataButton(); - hideUserDataTextArea(); - hideNewJSONEditorButton(); - hideUserDataSaved(); - } - - let materialJson = null; - try { - materialJson = JSON.parse(selectedEntityProperties.materialData); - } catch (e) { - // normal text - deleteJSONMaterialEditor(); - getPropertyInputElement("materialData").value = selectedEntityProperties.materialData; - showMaterialDataTextArea(); - showNewJSONMaterialEditorButton(); - hideSaveMaterialDataButton(); - hideMaterialDataSaved(); - } - if (materialJson !== null) { - if (materialEditor === null) { - createJSONMaterialEditor(); - } - setMaterialEditorJSON(materialJson); - showSaveMaterialDataButton(); - hideMaterialDataTextArea(); - hideNewJSONMaterialEditorButton(); - hideMaterialDataSaved(); - } - - if (hasSelectedEntityChanged && selectedEntityProperties.type === "Material") { - requestMaterialTarget(); - } - - let activeElement = document.activeElement; - if (doSelectElement && typeof activeElement.select !== "undefined") { - activeElement.select(); - } - } + handleEntitySelectionUpdate(data.selections, data.isPropertiesToolUpdate); } else if (data.type === 'tooltipsReply') { createAppTooltip.setIsEnabled(!data.hmdActive); createAppTooltip.setTooltipData(data.tooltips); @@ -3825,13 +4121,13 @@ function loaded() { if (propertyRange !== undefined) { let propertyData = properties[property].data; let multiplier = propertyData.multiplier; - if (propertyData.min === undefined && propertyRange.minimum != "") { + if (propertyData.min === undefined && propertyRange.minimum !== "") { propertyData.min = propertyRange.minimum; if (multiplier !== undefined) { propertyData.min /= multiplier; } } - if (propertyData.max === undefined && propertyRange.maximum != "") { + if (propertyData.max === undefined && propertyRange.maximum !== "") { propertyData.max = propertyRange.maximum; if (multiplier !== undefined) { propertyData.max /= multiplier; @@ -3855,7 +4151,7 @@ function loaded() { } } } else if (data.type === 'materialTargetReply') { - if (data.entityID === selectedEntityProperties.id) { + if (data.entityID === getFirstSelectedID()) { setMaterialTargetData(data.materialTargetData); } } @@ -3877,7 +4173,7 @@ function loaded() { // Server Script Error let elServerScripts = getPropertyInputElement("serverScripts"); - elDiv = document.createElement('div'); + let elDiv = document.createElement('div'); elDiv.className = "property"; let elServerScriptError = document.createElement('textarea'); let serverScriptErrorElementID = 'property-serverScripts-error'; @@ -3994,6 +4290,10 @@ function loaded() { dt.addEventListener("click", toggleDropdown, true); dl.appendChild(dt); + let elMultiDiff = document.createElement('span'); + elMultiDiff.className = "multi-diff"; + dl.appendChild(elMultiDiff); + let span = document.createElement("span"); span.setAttribute("value", options[selectedOption].value); span.textContent = options[selectedOption].firstChild.textContent; diff --git a/scripts/system/html/js/utils.js b/scripts/system/html/js/utils.js index d61b4d1762..9556856089 100644 --- a/scripts/system/html/js/utils.js +++ b/scripts/system/html/js/utils.js @@ -25,3 +25,70 @@ function disableDragDrop() { event.preventDefault(); }, false); } + +// mergeDeep function from https://stackoverflow.com/a/34749873 +/** + * Simple object check. + * @param item + * @returns {boolean} + */ +function mergeDeepIsObject(item) { + return (item && typeof item === 'object' && !Array.isArray(item)); +} + +/** + * Deep merge two objects. + * @param target + * @param sources + */ +function mergeDeep(target, ...sources) { + if (!sources.length) { + return target; + } + const source = sources.shift(); + + if (mergeDeepIsObject(target) && mergeDeepIsObject(source)) { + for (const key in source) { + if (!source.hasOwnProperty(key)) { + continue; + } + if (mergeDeepIsObject(source[key])) { + if (!target[key]) { + Object.assign(target, { [key]: {} }); + } + mergeDeep(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + + return mergeDeep(target, ...sources); +} + +function deepEqual(a, b) { + if (a === b) { + return true; + } + + if (typeof(a) !== "object" || typeof(b) !== "object") { + return false; + } + + if (Object.keys(a).length !== Object.keys(b).length) { + return false; + } + + for (let property in a) { + if (!a.hasOwnProperty(property)) { + continue; + } + if (!b.hasOwnProperty(property)) { + return false; + } + if (!deepEqual(a[property], b[property])) { + return false; + } + } + return true; +} diff --git a/scripts/system/libraries/gridTool.js b/scripts/system/libraries/gridTool.js index 1fd3cbfbaa..233ae3a3e4 100644 --- a/scripts/system/libraries/gridTool.js +++ b/scripts/system/libraries/gridTool.js @@ -25,7 +25,8 @@ Grid = function() { color: gridColor, alpha: gridAlpha, minorGridEvery: minorGridEvery, - majorGridEvery: majorGridEvery + majorGridEvery: majorGridEvery, + ignorePickIntersection: true }); that.visible = false; diff --git a/tools/nitpick/AppDataHighFidelity/Interface.json b/tools/nitpick/AppDataHighFidelity/Interface.json index ca91a092c8..7a8a15e002 100644 --- a/tools/nitpick/AppDataHighFidelity/Interface.json +++ b/tools/nitpick/AppDataHighFidelity/Interface.json @@ -253,9 +253,7 @@ "UserActivityLoggerDisabled": false, "View/Center Player In View": true, "View/Enter First Person Mode in HMD": true, - "View/Entity Mode": false, "View/First Person": true, - "View/Independent Mode": false, "View/Mirror": false, "View/Third Person": false, "WindowGeometry": "@Rect(0 0 1920 1080)", diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index b6adee28dc..70db77673a 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include "Gzip.h" @@ -132,10 +131,10 @@ void DomainBaker::loadLocalFile() { } // read the file contents to a JSON document - auto jsonDocument = QJsonDocument::fromJson(fileContents); + _json = QJsonDocument::fromJson(fileContents); // grab the entities object from the root JSON object - _entities = jsonDocument.object()[ENTITIES_OBJECT_KEY].toArray(); + _entities = _json.object()[ENTITIES_OBJECT_KEY].toArray(); if (_entities.isEmpty()) { // add an error to our list stating that the models file was empty @@ -171,7 +170,7 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, con // move the baker to the baker thread // and kickoff the bake baker->moveToThread(Oven::instance().getNextWorkerThread()); - QMetaObject::invokeMethod(baker.data(), "bake"); + QMetaObject::invokeMethod(baker.data(), "bake", Qt::QueuedConnection); // keep track of the total number of baking entities ++_totalNumberOfSubBakes; @@ -212,7 +211,7 @@ void DomainBaker::addTextureBaker(const QString& property, const QString& url, i // move the baker to a worker thread and kickoff the bake textureBaker->moveToThread(Oven::instance().getNextWorkerThread()); - QMetaObject::invokeMethod(textureBaker.data(), "bake"); + QMetaObject::invokeMethod(textureBaker.data(), "bake", Qt::QueuedConnection); // keep track of the total number of baking entities ++_totalNumberOfSubBakes; @@ -247,7 +246,7 @@ void DomainBaker::addScriptBaker(const QString& property, const QString& url, co // move the baker to a worker thread and kickoff the bake scriptBaker->moveToThread(Oven::instance().getNextWorkerThread()); - QMetaObject::invokeMethod(scriptBaker.data(), "bake"); + QMetaObject::invokeMethod(scriptBaker.data(), "bake", Qt::QueuedConnection); // keep track of the total number of baking entities ++_totalNumberOfSubBakes; @@ -272,7 +271,7 @@ void DomainBaker::addMaterialBaker(const QString& property, const QString& data, // setup a baker for this material QSharedPointer materialBaker { - new MaterialBaker(data, isURL, _contentOutputPath, destinationPath), + new MaterialBaker(materialData, isURL, _contentOutputPath, destinationPath), &MaterialBaker::deleteLater }; @@ -284,7 +283,7 @@ void DomainBaker::addMaterialBaker(const QString& property, const QString& data, // move the baker to a worker thread and kickoff the bake materialBaker->moveToThread(Oven::instance().getNextWorkerThread()); - QMetaObject::invokeMethod(materialBaker.data(), "bake"); + QMetaObject::invokeMethod(materialBaker.data(), "bake", Qt::QueuedConnection); // keep track of the total number of baking entities ++_totalNumberOfSubBakes; @@ -410,7 +409,10 @@ void DomainBaker::enumerateEntities() { // Materials if (entity.contains(MATERIAL_URL_KEY)) { - addMaterialBaker(MATERIAL_URL_KEY, entity[MATERIAL_URL_KEY].toString(), true, *it); + QString materialURL = entity[MATERIAL_URL_KEY].toString(); + if (!materialURL.startsWith("materialData")) { + addMaterialBaker(MATERIAL_URL_KEY, materialURL, true, *it); + } } if (entity.contains(MATERIAL_DATA_KEY)) { addMaterialBaker(MATERIAL_DATA_KEY, entity[MATERIAL_DATA_KEY].toString(), false, *it, _destinationPath); @@ -749,15 +751,10 @@ void DomainBaker::writeNewEntitiesFile() { // time to write out a main models.json.gz file // first setup a document with the entities array below the entities key - QJsonDocument entitiesDocument; - - QJsonObject rootObject; - rootObject[ENTITIES_OBJECT_KEY] = _entities; - - entitiesDocument.setObject(rootObject); + _json.object()[ENTITIES_OBJECT_KEY] = _entities; // turn that QJsonDocument into a byte array ready for compression - QByteArray jsonByteArray = entitiesDocument.toJson(); + QByteArray jsonByteArray = _json.toJson(); // compress the json byte array using gzip QByteArray compressedJson; diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index e8102ec7e8..d4414bbf99 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -12,6 +12,7 @@ #ifndef hifi_DomainBaker_h #define hifi_DomainBaker_h +#include #include #include #include @@ -59,6 +60,7 @@ private: QString _originalOutputPath; QUrl _destinationPath; + QJsonDocument _json; QJsonArray _entities; QHash> _modelBakers; diff --git a/interface/resources/icons/tablet-icons/avatar-record-a.svg b/unpublishedScripts/marketplace/record/assets/avatar-record-a.svg similarity index 100% rename from interface/resources/icons/tablet-icons/avatar-record-a.svg rename to unpublishedScripts/marketplace/record/assets/avatar-record-a.svg diff --git a/interface/resources/icons/tablet-icons/avatar-record-i.svg b/unpublishedScripts/marketplace/record/assets/avatar-record-i.svg similarity index 100% rename from interface/resources/icons/tablet-icons/avatar-record-i.svg rename to unpublishedScripts/marketplace/record/assets/avatar-record-i.svg diff --git a/unpublishedScripts/marketplace/record/record.app.json b/unpublishedScripts/marketplace/record/record.app.json new file mode 100644 index 0000000000..3b2860db01 --- /dev/null +++ b/unpublishedScripts/marketplace/record/record.app.json @@ -0,0 +1,4 @@ +{ + "scriptURL": "http://mpassets.highfidelity.com/b3da90d4-390f-4f2c-ac78-6c8e074c93d7-v1/record.js", + "homeURL": "http://mpassets.highfidelity.com/b3da90d4-390f-4f2c-ac78-6c8e074c93d7-v1/html/record.html" +} diff --git a/unpublishedScripts/marketplace/record/record.js b/unpublishedScripts/marketplace/record/record.js index 4786356fee..18bcfbf5a1 100644 --- a/unpublishedScripts/marketplace/record/record.js +++ b/unpublishedScripts/marketplace/record/record.js @@ -13,8 +13,8 @@ (function () { var APP_NAME = "RECORD", - APP_ICON_INACTIVE = "icons/tablet-icons/avatar-record-i.svg", - APP_ICON_ACTIVE = "icons/tablet-icons/avatar-record-a.svg", + APP_ICON_INACTIVE = Script.resolvePath("assets/avatar-record-i.svg"), + APP_ICON_ACTIVE = Script.resolvePath("assets/avatar-record-a.svg"), APP_URL = Script.resolvePath("html/record.html"), isDialogDisplayed = false, tablet,