diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 1b533f10f3..0a868737b0 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -39,7 +39,6 @@ #include "SendAssetTask.h" #include "UploadAssetTask.h" - static const uint8_t MIN_CORES_FOR_MULTICORE = 4; static const uint8_t CPU_AFFINITY_COUNT_HIGH = 2; static const uint8_t CPU_AFFINITY_COUNT_LOW = 1; @@ -56,7 +55,7 @@ static const QString BAKED_MODEL_SIMPLE_NAME = "asset.fbx"; static const QString BAKED_TEXTURE_SIMPLE_NAME = "texture.ktx"; static const QString BAKED_SCRIPT_SIMPLE_NAME = "asset.js"; -void AssetServer::bakeAsset(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) { +void AssetServer::bakeAsset(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath) { qDebug() << "Starting bake for: " << assetPath << assetHash; auto it = _pendingBakes.find(assetHash); if (it == _pendingBakes.end()) { @@ -74,23 +73,23 @@ void AssetServer::bakeAsset(const AssetHash& assetHash, const AssetPath& assetPa } } -QString AssetServer::getPathToAssetHash(const AssetHash& assetHash) { +QString AssetServer::getPathToAssetHash(const AssetUtils::AssetHash& assetHash) { return _filesDirectory.absoluteFilePath(assetHash); } -std::pair AssetServer::getAssetStatus(const AssetPath& path, const AssetHash& hash) { +std::pair AssetServer::getAssetStatus(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash) { auto it = _pendingBakes.find(hash); if (it != _pendingBakes.end()) { - return { (*it)->isBaking() ? Baking : Pending, "" }; + return { (*it)->isBaking() ? AssetUtils::Baking : AssetUtils::Pending, "" }; } - if (path.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { - return { Baked, "" }; + if (path.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { + return { AssetUtils::Baked, "" }; } auto dotIndex = path.lastIndexOf("."); if (dotIndex == -1) { - return { Irrelevant, "" }; + return { AssetUtils::Irrelevant, "" }; } auto extension = path.mid(dotIndex + 1); @@ -104,16 +103,16 @@ std::pair AssetServer::getAssetStatus(const AssetPath& pa } else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) { bakedFilename = BAKED_SCRIPT_SIMPLE_NAME; } else { - return { Irrelevant, "" }; + return { AssetUtils::Irrelevant, "" }; } - auto bakedPath = HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + bakedFilename; + auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + bakedFilename; auto jt = _fileMappings.find(bakedPath); if (jt != _fileMappings.end()) { if (jt->second == hash) { - return { NotBaked, "" }; + return { AssetUtils::NotBaked, "" }; } else { - return { Baked, "" }; + return { AssetUtils::Baked, "" }; } } else { bool loaded; @@ -121,11 +120,11 @@ std::pair AssetServer::getAssetStatus(const AssetPath& pa std::tie(loaded, meta) = readMetaFile(hash); if (loaded && meta.failedLastBake) { - return { Error, meta.lastBakeErrors }; + return { AssetUtils::Error, meta.lastBakeErrors }; } } - return { Pending, "" }; + return { AssetUtils::Pending, "" }; } void AssetServer::bakeAssets() { @@ -137,14 +136,14 @@ void AssetServer::bakeAssets() { } } -void AssetServer::maybeBake(const AssetPath& path, const AssetHash& hash) { +void AssetServer::maybeBake(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash) { if (needsToBeBaked(path, hash)) { qDebug() << "Queuing bake of: " << path; bakeAsset(hash, path, getPathToAssetHash(hash)); } } -void AssetServer::createEmptyMetaFile(const AssetHash& hash) { +void AssetServer::createEmptyMetaFile(const AssetUtils::AssetHash& hash) { QString metaFilePath = "atp:/" + hash + "/meta.json"; QFile metaFile { metaFilePath }; @@ -157,14 +156,14 @@ void AssetServer::createEmptyMetaFile(const AssetHash& hash) { } } -bool AssetServer::hasMetaFile(const AssetHash& hash) { - QString metaFilePath = HIDDEN_BAKED_CONTENT_FOLDER + hash + "/meta.json"; +bool AssetServer::hasMetaFile(const AssetUtils::AssetHash& hash) { + QString metaFilePath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/meta.json"; return _fileMappings.find(metaFilePath) != _fileMappings.end(); } -bool AssetServer::needsToBeBaked(const AssetPath& path, const AssetHash& assetHash) { - if (path.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { +bool AssetServer::needsToBeBaked(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& assetHash) { + if (path.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { return false; } @@ -196,7 +195,7 @@ bool AssetServer::needsToBeBaked(const AssetPath& path, const AssetHash& assetHa return false; } - auto bakedPath = HIDDEN_BAKED_CONTENT_FOLDER + assetHash + "/" + bakedFilename; + auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + assetHash + "/" + bakedFilename; return _fileMappings.find(bakedPath) == _fileMappings.end(); } @@ -235,7 +234,7 @@ AssetServer::AssetServer(ReceivedMessage& message) : ThreadedAssignment(message), _transferTaskPool(this), _bakingTaskPool(this), - _filesizeLimit(MAX_UPLOAD_SIZE) + _filesizeLimit(AssetUtils::MAX_UPLOAD_SIZE) { // store the current state of image compression so we can reset it when this assignment is complete _wasColorTextureCompressionEnabled = image::isColorTexturesCompressionEnabled(); @@ -390,7 +389,7 @@ void AssetServer::completeSetup() { // Check the asset directory to output some information about what we have auto files = _filesDirectory.entryList(QDir::Files); - QRegExp hashFileRegex { ASSET_HASH_REGEX_STRING }; + QRegExp hashFileRegex { AssetUtils::ASSET_HASH_REGEX_STRING }; auto hashedFiles = files.filter(hashFileRegex); qCInfo(asset_server) << "There are" << hashedFiles.size() << "asset files in the asset directory."; @@ -410,9 +409,9 @@ void AssetServer::completeSetup() { // get file size limit for an asset static const QString ASSETS_FILESIZE_LIMIT_OPTION = "assets_filesize_limit"; auto assetsFilesizeLimitJSONValue = assetServerObject[ASSETS_FILESIZE_LIMIT_OPTION]; - auto assetsFilesizeLimit = (uint64_t)assetsFilesizeLimitJSONValue.toInt(MAX_UPLOAD_SIZE); + auto assetsFilesizeLimit = (uint64_t)assetsFilesizeLimitJSONValue.toInt(AssetUtils::MAX_UPLOAD_SIZE); - if (assetsFilesizeLimit != 0 && assetsFilesizeLimit < MAX_UPLOAD_SIZE) { + if (assetsFilesizeLimit != 0 && assetsFilesizeLimit < AssetUtils::MAX_UPLOAD_SIZE) { _filesizeLimit = assetsFilesizeLimit * BITS_PER_MEGABITS; } @@ -421,7 +420,7 @@ void AssetServer::completeSetup() { } void AssetServer::cleanupUnmappedFiles() { - QRegExp hashFileRegex { "^[a-f0-9]{" + QString::number(SHA256_HASH_HEX_LENGTH) + "}" }; + QRegExp hashFileRegex { "^[a-f0-9]{" + QString::number(AssetUtils::SHA256_HASH_HEX_LENGTH) + "}" }; auto files = _filesDirectory.entryInfoList(QDir::Files); @@ -454,6 +453,8 @@ void AssetServer::cleanupUnmappedFiles() { } void AssetServer::handleAssetMappingOperation(QSharedPointer message, SharedNodePointer senderNode) { + using AssetMappingOperationType = AssetUtils::AssetMappingOperationType; + MessageID messageID; message->readPrimitive(&messageID); @@ -519,7 +520,7 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode if (!bakedRootFile.isEmpty()) { // we ran into an asset for which we could have a baked version, let's check if it's ready - bakedAssetPath = HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + bakedRootFile; + bakedAssetPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + bakedRootFile; auto bakedIt = _fileMappings.find(bakedAssetPath); if (bakedIt != _fileMappings.end()) { @@ -537,7 +538,7 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode } } - replyPacket.writePrimitive(AssetServerError::NoError); + replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError); if (wasRedirected) { qDebug() << "Writing re-directed hash for" << originalAssetHash << "to" << redirectedAssetHash; @@ -563,12 +564,12 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode } } } else { - replyPacket.writePrimitive(AssetServerError::AssetNotFound); + replyPacket.writePrimitive(AssetUtils::AssetServerError::AssetNotFound); } } void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) { - replyPacket.writePrimitive(AssetServerError::NoError); + replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError); uint32_t count = (uint32_t)_fileMappings.size(); @@ -580,11 +581,11 @@ void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedN replyPacket.writeString(mapping); replyPacket.write(QByteArray::fromHex(hash.toUtf8())); - BakingStatus status; + AssetUtils::BakingStatus status; QString lastBakeErrors; std::tie(status, lastBakeErrors) = getAssetStatus(mapping, hash); replyPacket.writePrimitive(status); - if (status == Error) { + if (status == AssetUtils::Error) { replyPacket.writeString(lastBakeErrors); } } @@ -594,22 +595,22 @@ void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNode if (senderNode->getCanWriteToAssetServer()) { QString assetPath = message.readString(); - auto assetHash = message.read(SHA256_HASH_LENGTH).toHex(); + auto assetHash = message.read(AssetUtils::SHA256_HASH_LENGTH).toHex(); // don't process a set mapping operation that is inside the hidden baked folder - if (assetPath.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { - qCDebug(asset_server) << "Refusing to process a set mapping operation inside" << HIDDEN_BAKED_CONTENT_FOLDER; - replyPacket.writePrimitive(AssetServerError::PermissionDenied); + if (assetPath.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { + qCDebug(asset_server) << "Refusing to process a set mapping operation inside" << AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER; + replyPacket.writePrimitive(AssetUtils::AssetServerError::PermissionDenied); } else { if (setMapping(assetPath, assetHash)) { - replyPacket.writePrimitive(AssetServerError::NoError); + replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError); } else { - replyPacket.writePrimitive(AssetServerError::MappingOperationFailed); + replyPacket.writePrimitive(AssetUtils::AssetServerError::MappingOperationFailed); } } } else { - replyPacket.writePrimitive(AssetServerError::PermissionDenied); + replyPacket.writePrimitive(AssetUtils::AssetServerError::PermissionDenied); } } @@ -623,21 +624,21 @@ void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, Shared for (int i = 0; i < numberOfDeletedMappings; ++i) { auto mapping = message.readString(); - if (!mapping.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { + if (!mapping.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { mappingsToDelete << mapping; } else { qCDebug(asset_server) << "Refusing to delete mapping" << mapping - << "that is inside" << HIDDEN_BAKED_CONTENT_FOLDER; + << "that is inside" << AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER; } } if (deleteMappings(mappingsToDelete)) { - replyPacket.writePrimitive(AssetServerError::NoError); + replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError); } else { - replyPacket.writePrimitive(AssetServerError::MappingOperationFailed); + replyPacket.writePrimitive(AssetUtils::AssetServerError::MappingOperationFailed); } } else { - replyPacket.writePrimitive(AssetServerError::PermissionDenied); + replyPacket.writePrimitive(AssetUtils::AssetServerError::PermissionDenied); } } @@ -646,20 +647,20 @@ void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedN QString oldPath = message.readString(); QString newPath = message.readString(); - if (oldPath.startsWith(HIDDEN_BAKED_CONTENT_FOLDER) || newPath.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { + if (oldPath.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER) || newPath.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { qCDebug(asset_server) << "Cannot rename" << oldPath << "to" << newPath - << "since one of the paths is inside" << HIDDEN_BAKED_CONTENT_FOLDER; - replyPacket.writePrimitive(AssetServerError::PermissionDenied); + << "since one of the paths is inside" << AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER; + replyPacket.writePrimitive(AssetUtils::AssetServerError::PermissionDenied); } else { if (renameMapping(oldPath, newPath)) { - replyPacket.writePrimitive(AssetServerError::NoError); + replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError); } else { - replyPacket.writePrimitive(AssetServerError::MappingOperationFailed); + replyPacket.writePrimitive(AssetUtils::AssetServerError::MappingOperationFailed); } } } else { - replyPacket.writePrimitive(AssetServerError::PermissionDenied); + replyPacket.writePrimitive(AssetUtils::AssetServerError::PermissionDenied); } } @@ -678,12 +679,12 @@ void AssetServer::handleSetBakingEnabledOperation(ReceivedMessage& message, Shar } if (setBakingEnabled(mappings, enabled)) { - replyPacket.writePrimitive(AssetServerError::NoError); + replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError); } else { - replyPacket.writePrimitive(AssetServerError::MappingOperationFailed); + replyPacket.writePrimitive(AssetUtils::AssetServerError::MappingOperationFailed); } } else { - replyPacket.writePrimitive(AssetServerError::PermissionDenied); + replyPacket.writePrimitive(AssetUtils::AssetServerError::PermissionDenied); } } @@ -691,15 +692,15 @@ void AssetServer::handleAssetGetInfo(QSharedPointer message, Sh QByteArray assetHash; MessageID messageID; - if (message->getSize() < qint64(SHA256_HASH_LENGTH + sizeof(messageID))) { + if (message->getSize() < qint64(AssetUtils::SHA256_HASH_LENGTH + sizeof(messageID))) { qCDebug(asset_server) << "ERROR bad file request"; return; } message->readPrimitive(&messageID); - assetHash = message->readWithoutCopy(SHA256_HASH_LENGTH); + assetHash = message->readWithoutCopy(AssetUtils::SHA256_HASH_LENGTH); - auto size = qint64(sizeof(MessageID) + SHA256_HASH_LENGTH + sizeof(AssetServerError) + sizeof(qint64)); + auto size = qint64(sizeof(MessageID) + AssetUtils::SHA256_HASH_LENGTH + sizeof(AssetUtils::AssetServerError) + sizeof(qint64)); auto replyPacket = NLPacket::create(PacketType::AssetGetInfoReply, size, true); QByteArray hexHash = assetHash.toHex(); @@ -712,11 +713,11 @@ void AssetServer::handleAssetGetInfo(QSharedPointer message, Sh if (fileInfo.exists() && fileInfo.isReadable()) { qCDebug(asset_server) << "Opening file: " << fileInfo.filePath(); - replyPacket->writePrimitive(AssetServerError::NoError); + replyPacket->writePrimitive(AssetUtils::AssetServerError::NoError); replyPacket->writePrimitive(fileInfo.size()); } else { qCDebug(asset_server) << "Asset not found: " << QString(hexHash); - replyPacket->writePrimitive(AssetServerError::AssetNotFound); + replyPacket->writePrimitive(AssetUtils::AssetServerError::AssetNotFound); } auto nodeList = DependencyManager::get(); @@ -725,7 +726,7 @@ void AssetServer::handleAssetGetInfo(QSharedPointer message, Sh void AssetServer::handleAssetGet(QSharedPointer message, SharedNodePointer senderNode) { - auto minSize = qint64(sizeof(MessageID) + SHA256_HASH_LENGTH + sizeof(DataOffset) + sizeof(DataOffset)); + auto minSize = qint64(sizeof(MessageID) + AssetUtils::SHA256_HASH_LENGTH + sizeof(AssetUtils::DataOffset) + sizeof(AssetUtils::DataOffset)); if (message->getSize() < minSize) { qCDebug(asset_server) << "ERROR bad file request"; @@ -749,14 +750,14 @@ void AssetServer::handleAssetUpload(QSharedPointer message, Sha // for now this also means it isn't allowed to add assets // so return a packet with error that indicates that - auto permissionErrorPacket = NLPacket::create(PacketType::AssetUploadReply, sizeof(MessageID) + sizeof(AssetServerError), true); + auto permissionErrorPacket = NLPacket::create(PacketType::AssetUploadReply, sizeof(MessageID) + sizeof(AssetUtils::AssetServerError), true); MessageID messageID; message->readPrimitive(&messageID); // write the message ID and a permission denied error permissionErrorPacket->writePrimitive(messageID); - permissionErrorPacket->writePrimitive(AssetServerError::PermissionDenied); + permissionErrorPacket->writePrimitive(AssetUtils::AssetServerError::PermissionDenied); // send off the packet auto nodeList = DependencyManager::get(); @@ -863,12 +864,12 @@ bool AssetServer::loadMappingsFromFile() { continue; } - if (!isValidFilePath(key)) { + if (!AssetUtils::isValidFilePath(key)) { qCWarning(asset_server) << "Will not keep mapping for" << key << "since it is not a valid path."; continue; } - if (!isValidHash(value.toString())) { + if (!AssetUtils::isValidHash(value.toString())) { qCWarning(asset_server) << "Will not keep mapping for" << key << "since it does not have a valid hash."; continue; } @@ -918,15 +919,15 @@ bool AssetServer::writeMappingsToFile() { return false; } -bool AssetServer::setMapping(AssetPath path, AssetHash hash) { +bool AssetServer::setMapping(AssetUtils::AssetPath path, AssetUtils::AssetHash hash) { path = path.trimmed(); - if (!isValidFilePath(path)) { + if (!AssetUtils::isValidFilePath(path)) { qCWarning(asset_server) << "Cannot set a mapping for invalid path:" << path << "=>" << hash; return false; } - if (!isValidHash(hash)) { + if (!AssetUtils::isValidHash(hash)) { qCWarning(asset_server) << "Cannot set a mapping for invalid hash" << path << "=>" << hash; return false; } @@ -958,23 +959,23 @@ bool AssetServer::setMapping(AssetPath path, AssetHash hash) { } } -bool pathIsFolder(const AssetPath& path) { +bool pathIsFolder(const AssetUtils::AssetPath& path) { return path.endsWith('/'); } -void AssetServer::removeBakedPathsForDeletedAsset(AssetHash hash) { +void AssetServer::removeBakedPathsForDeletedAsset(AssetUtils::AssetHash hash) { // we deleted the file with this hash // check if we had baked content for that file that should also now be removed // by calling deleteMappings for the hidden baked content folder for this hash - AssetPathList hiddenBakedFolder { HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" }; + AssetUtils::AssetPathList hiddenBakedFolder { AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" }; qCDebug(asset_server) << "Deleting baked content below" << hiddenBakedFolder << "since" << hash << "was deleted"; deleteMappings(hiddenBakedFolder); } -bool AssetServer::deleteMappings(const AssetPathList& paths) { +bool AssetServer::deleteMappings(const AssetUtils::AssetPathList& paths) { // take a copy of the current mappings in case persistence of these deletes fails auto oldMappings = _fileMappings; @@ -1060,11 +1061,11 @@ bool AssetServer::deleteMappings(const AssetPathList& paths) { } } -bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) { +bool AssetServer::renameMapping(AssetUtils::AssetPath oldPath, AssetUtils::AssetPath newPath) { oldPath = oldPath.trimmed(); newPath = newPath.trimmed(); - if (!isValidFilePath(oldPath) || !isValidFilePath(newPath)) { + if (!AssetUtils::isValidFilePath(oldPath) || !AssetUtils::isValidFilePath(newPath)) { qCWarning(asset_server) << "Cannot perform rename with invalid paths - both should have leading forward and no ending slashes:" << oldPath << "=>" << newPath; @@ -1164,8 +1165,8 @@ 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 AssetHash& hash, const QString& relativeFilePath) { - return HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + relativeFilePath; +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) { @@ -1197,7 +1198,7 @@ void AssetServer::handleCompletedBake(QString originalAssetHash, QString origina qDebug() << "File path: " << filePath; - AssetHash bakedFileHash; + AssetUtils::AssetHash bakedFileHash; if (file.open(QIODevice::ReadOnly)) { QCryptographicHash hasher(QCryptographicHash::Sha256); @@ -1290,8 +1291,8 @@ 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"; -std::pair AssetServer::readMetaFile(AssetHash hash) { - auto metaFilePath = HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + "meta.json"; +std::pair AssetServer::readMetaFile(AssetUtils::AssetHash hash) { + auto metaFilePath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + hash + "/" + "meta.json"; auto it = _fileMappings.find(metaFilePath); if (it == _fileMappings.end()) { @@ -1335,7 +1336,7 @@ std::pair AssetServer::readMetaFile(AssetHash hash) { return { false, {} }; } -bool AssetServer::writeMetaFile(AssetHash originalAssetHash, const AssetMeta& meta) { +bool AssetServer::writeMetaFile(AssetUtils::AssetHash originalAssetHash, const AssetMeta& meta) { // construct the JSON that will be in the meta file QJsonObject metaFileObject; @@ -1349,7 +1350,7 @@ bool AssetServer::writeMetaFile(AssetHash originalAssetHash, const AssetMeta& me auto metaFileJSON = metaFileDoc.toJson(); // get a hash for the contents of the meta-file - AssetHash metaFileHash = QCryptographicHash::hash(metaFileJSON, QCryptographicHash::Sha256).toHex(); + AssetUtils::AssetHash metaFileHash = QCryptographicHash::hash(metaFileJSON, QCryptographicHash::Sha256).toHex(); // create the meta file in our files folder, named by the hash of its contents QFile metaFile(_filesDirectory.absoluteFilePath(metaFileHash)); @@ -1359,7 +1360,7 @@ bool AssetServer::writeMetaFile(AssetHash originalAssetHash, const AssetMeta& me metaFile.close(); // add a mapping to the meta file so it doesn't get deleted because it is unmapped - auto metaFileMapping = HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + "meta.json"; + auto metaFileMapping = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + originalAssetHash + "/" + "meta.json"; return setMapping(metaFileMapping, metaFileHash); } else { @@ -1367,7 +1368,7 @@ bool AssetServer::writeMetaFile(AssetHash originalAssetHash, const AssetMeta& me } } -bool AssetServer::setBakingEnabled(const AssetPathList& paths, bool enabled) { +bool AssetServer::setBakingEnabled(const AssetUtils::AssetPathList& paths, bool enabled) { for (const auto& path : paths) { auto it = _fileMappings.find(path); if (it != _fileMappings.end()) { diff --git a/assignment-client/src/assets/AssetServer.h b/assignment-client/src/assets/AssetServer.h index e6393e6a98..5e7a3c1700 100644 --- a/assignment-client/src/assets/AssetServer.h +++ b/assignment-client/src/assets/AssetServer.h @@ -21,7 +21,6 @@ #include "AssetUtils.h" #include "ReceivedMessage.h" - namespace std { template <> struct hash { @@ -75,29 +74,29 @@ private: bool writeMappingsToFile(); /// Set the mapping for path to hash - bool setMapping(AssetPath path, AssetHash hash); + bool setMapping(AssetUtils::AssetPath path, AssetUtils::AssetHash hash); /// Delete mapping `path`. Returns `true` if deletion of mappings succeeds, else `false`. - bool deleteMappings(const AssetPathList& paths); + bool deleteMappings(const AssetUtils::AssetPathList& paths); /// Rename mapping from `oldPath` to `newPath`. Returns true if successful - bool renameMapping(AssetPath oldPath, AssetPath newPath); + bool renameMapping(AssetUtils::AssetPath oldPath, AssetUtils::AssetPath newPath); - bool setBakingEnabled(const AssetPathList& paths, bool enabled); + bool setBakingEnabled(const AssetUtils::AssetPathList& paths, bool enabled); /// Delete any unmapped files from the local asset directory void cleanupUnmappedFiles(); - QString getPathToAssetHash(const AssetHash& assetHash); + QString getPathToAssetHash(const AssetUtils::AssetHash& assetHash); - std::pair getAssetStatus(const AssetPath& path, const AssetHash& hash); + std::pair getAssetStatus(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash); void bakeAssets(); - void maybeBake(const AssetPath& path, const AssetHash& hash); - void createEmptyMetaFile(const AssetHash& hash); - bool hasMetaFile(const AssetHash& hash); - bool needsToBeBaked(const AssetPath& path, const AssetHash& assetHash); - void bakeAsset(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath); + void maybeBake(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash); + void createEmptyMetaFile(const AssetUtils::AssetHash& hash); + bool hasMetaFile(const AssetUtils::AssetHash& hash); + bool needsToBeBaked(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& assetHash); + 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, @@ -106,11 +105,11 @@ private: void handleAbortedBake(QString originalAssetHash, QString assetPath); /// Create meta file to describe baked content for original asset - std::pair readMetaFile(AssetHash hash); - bool writeMetaFile(AssetHash originalAssetHash, const AssetMeta& meta = AssetMeta()); + std::pair readMetaFile(AssetUtils::AssetHash hash); + bool writeMetaFile(AssetUtils::AssetHash originalAssetHash, const AssetMeta& meta = AssetMeta()); /// Remove baked paths when the original asset is deleteds - void removeBakedPathsForDeletedAsset(AssetHash originalAssetHash); + void removeBakedPathsForDeletedAsset(AssetUtils::AssetHash originalAssetHash); Mappings _fileMappings; @@ -120,7 +119,7 @@ private: /// Task pool for handling uploads and downloads of assets QThreadPool _transferTaskPool; - QHash> _pendingBakes; + QHash> _pendingBakes; QThreadPool _bakingTaskPool; bool _wasColorTextureCompressionEnabled { false }; diff --git a/assignment-client/src/assets/BakeAssetTask.cpp b/assignment-client/src/assets/BakeAssetTask.cpp index 49322ca4cb..5b9add7467 100644 --- a/assignment-client/src/assets/BakeAssetTask.cpp +++ b/assignment-client/src/assets/BakeAssetTask.cpp @@ -24,7 +24,7 @@ static const int OVEN_STATUS_CODE_ABORT { 2 }; std::once_flag registerMetaTypesFlag; -BakeAssetTask::BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) : +BakeAssetTask::BakeAssetTask(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath) : _assetHash(assetHash), _assetPath(assetPath), _filePath(filePath) diff --git a/assignment-client/src/assets/BakeAssetTask.h b/assignment-client/src/assets/BakeAssetTask.h index c73a8bff65..7b003fa1bd 100644 --- a/assignment-client/src/assets/BakeAssetTask.h +++ b/assignment-client/src/assets/BakeAssetTask.h @@ -25,7 +25,7 @@ class BakeAssetTask : public QObject, public QRunnable { Q_OBJECT public: - BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath); + BakeAssetTask(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath); bool isBaking() { return _isBaking.load(); } @@ -41,8 +41,8 @@ signals: private: std::atomic _isBaking { false }; - AssetHash _assetHash; - AssetPath _assetPath; + AssetUtils::AssetHash _assetHash; + AssetUtils::AssetPath _assetPath; QString _filePath; std::unique_ptr _ovenProcess { nullptr }; std::atomic _wasAborted { false }; diff --git a/assignment-client/src/assets/SendAssetTask.cpp b/assignment-client/src/assets/SendAssetTask.cpp index eab88e0d46..6da092357f 100644 --- a/assignment-client/src/assets/SendAssetTask.cpp +++ b/assignment-client/src/assets/SendAssetTask.cpp @@ -40,7 +40,7 @@ void SendAssetTask::run() { ByteRange byteRange; _message->readPrimitive(&messageID); - QByteArray assetHash = _message->read(SHA256_HASH_LENGTH); + QByteArray assetHash = _message->read(AssetUtils::SHA256_HASH_LENGTH); // `start` and `end` indicate the range of data to retrieve for the asset identified by `assetHash`. // `start` is inclusive, `end` is exclusive. Requesting `start` = 1, `end` = 10 will retrieve 9 bytes of data, @@ -61,7 +61,7 @@ void SendAssetTask::run() { replyPacketList->writePrimitive(messageID); if (!byteRange.isValid()) { - replyPacketList->writePrimitive(AssetServerError::InvalidByteRange); + replyPacketList->writePrimitive(AssetUtils::AssetServerError::InvalidByteRange); } else { QString filePath = _resourcesDir.filePath(QString(hexHash)); @@ -75,7 +75,7 @@ void SendAssetTask::run() { // check if we're being asked to read data that we just don't have // because of the file size if (file.size() < byteRange.fromInclusive || file.size() < byteRange.toExclusive) { - replyPacketList->writePrimitive(AssetServerError::InvalidByteRange); + replyPacketList->writePrimitive(AssetUtils::AssetServerError::InvalidByteRange); qCDebug(networking) << "Bad byte range: " << hexHash << " " << byteRange.fromInclusive << ":" << byteRange.toExclusive; } else { @@ -86,7 +86,7 @@ void SendAssetTask::run() { // this range is positive, meaning we just need to seek into the file and then read from there file.seek(byteRange.fromInclusive); - replyPacketList->writePrimitive(AssetServerError::NoError); + replyPacketList->writePrimitive(AssetUtils::AssetServerError::NoError); replyPacketList->writePrimitive(size); replyPacketList->write(file.read(size)); } else { @@ -95,7 +95,7 @@ void SendAssetTask::run() { // seek to the part of the file where the negative range begins file.seek(file.size() + byteRange.fromInclusive); - replyPacketList->writePrimitive(AssetServerError::NoError); + replyPacketList->writePrimitive(AssetUtils::AssetServerError::NoError); replyPacketList->writePrimitive(size); // first write everything from the negative range to the end of the file @@ -107,7 +107,7 @@ void SendAssetTask::run() { file.close(); } else { qCDebug(networking) << "Asset not found: " << filePath << "(" << hexHash << ")"; - replyPacketList->writePrimitive(AssetServerError::AssetNotFound); + replyPacketList->writePrimitive(AssetUtils::AssetServerError::AssetNotFound); } } diff --git a/assignment-client/src/assets/UploadAssetTask.cpp b/assignment-client/src/assets/UploadAssetTask.cpp index 5e6d59d032..68bf4db5fd 100644 --- a/assignment-client/src/assets/UploadAssetTask.cpp +++ b/assignment-client/src/assets/UploadAssetTask.cpp @@ -20,7 +20,6 @@ #include "ClientServerUtils.h" - UploadAssetTask::UploadAssetTask(QSharedPointer receivedMessage, SharedNodePointer senderNode, const QDir& resourcesDir, uint64_t filesizeLimit) : _receivedMessage(receivedMessage), @@ -50,11 +49,11 @@ void UploadAssetTask::run() { replyPacket->writePrimitive(messageID); if (fileSize > _filesizeLimit) { - replyPacket->writePrimitive(AssetServerError::AssetTooLarge); + replyPacket->writePrimitive(AssetUtils::AssetServerError::AssetTooLarge); } else { QByteArray fileData = buffer.read(fileSize); - auto hash = hashData(fileData); + auto hash = AssetUtils::hashData(fileData); auto hexHash = hash.toHex(); qDebug() << "Hash for uploaded file from" << uuidStringWithoutCurlyBraces(_senderNode->getUUID()) @@ -66,12 +65,12 @@ void UploadAssetTask::run() { if (file.exists()) { // check if the local file has the correct contents, otherwise we overwrite - if (file.open(QIODevice::ReadOnly) && hashData(file.readAll()) == hash) { + if (file.open(QIODevice::ReadOnly) && AssetUtils::hashData(file.readAll()) == hash) { qDebug() << "Not overwriting existing verified file: " << hexHash; existingCorrectFile = true; - replyPacket->writePrimitive(AssetServerError::NoError); + replyPacket->writePrimitive(AssetUtils::AssetServerError::NoError); replyPacket->write(hash); } else { qDebug() << "Overwriting an existing file whose contents did not match the expected hash: " << hexHash; @@ -84,7 +83,7 @@ void UploadAssetTask::run() { qDebug() << "Wrote file" << hexHash << "to disk. Upload complete"; file.close(); - replyPacket->writePrimitive(AssetServerError::NoError); + replyPacket->writePrimitive(AssetUtils::AssetServerError::NoError); replyPacket->write(hash); } else { qWarning() << "Failed to upload or write to file" << hexHash << " - upload failed."; @@ -96,7 +95,7 @@ void UploadAssetTask::run() { qWarning() << "Removal of failed upload file" << hexHash << "failed."; } - replyPacket->writePrimitive(AssetServerError::FileOperationFailed); + replyPacket->writePrimitive(AssetUtils::AssetServerError::FileOperationFailed); } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9379c85fdd..64e9feea02 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -68,6 +68,7 @@ #include #include #include +#include #include #include #include @@ -763,6 +764,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -1906,7 +1908,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo entityResult.distance = pickResult->distance; entityResult.surfaceNormal = pickResult->surfaceNormal; entityResult.entityID = pickResult->objectID; - entityResult.entity = DependencyManager::get()->getTree()->findEntityByID(entityResult.entityID); + entityResult.extraInfo = pickResult->extraInfo; } } return entityResult; @@ -2443,6 +2445,7 @@ void Application::initializeUi() { surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarEntitiesBookmarks", DependencyManager::get().data()); surfaceContext->setContextProperty("LocationBookmarks", DependencyManager::get().data()); // Caches @@ -5851,6 +5854,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get()->getStats().data()); scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get().data()); scriptEngine->registerGlobalObject("AvatarBookmarks", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("AvatarEntitiesBookmarks", DependencyManager::get().data()); scriptEngine->registerGlobalObject("LocationBookmarks", DependencyManager::get().data()); scriptEngine->registerGlobalObject("RayPick", DependencyManager::get().data()); diff --git a/interface/src/AvatarEntitiesBookmarks.cpp b/interface/src/AvatarEntitiesBookmarks.cpp new file mode 100644 index 0000000000..d108bf3a23 --- /dev/null +++ b/interface/src/AvatarEntitiesBookmarks.cpp @@ -0,0 +1,168 @@ +// +// AvatarEntitiesBookmarks.cpp +// interface/src +// +// Created by Dante Ruiz on 15/01/18. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MainWindow.h" +#include "Menu.h" +#include "AvatarEntitiesBookmarks.h" +#include "InterfaceLogging.h" + +#include "QVariantGLM.h" + +#include + +void addAvatarEntities(const QVariantList& avatarEntities) { + auto nodeList = DependencyManager::get(); + const QUuid myNodeID = nodeList->getSessionUUID(); + EntityTreePointer entityTree = DependencyManager::get()->getTree(); + if (!entityTree) { + return; + } + EntitySimulationPointer entitySimulation = entityTree->getSimulation(); + PhysicalEntitySimulationPointer physicalEntitySimulation = std::static_pointer_cast(entitySimulation); + EntityEditPacketSender* entityPacketSender = physicalEntitySimulation->getPacketSender(); + QScriptEngine scriptEngine; + for (int index = 0; index < avatarEntities.count(); index++) { + const QVariantMap& avatarEntityProperties = avatarEntities.at(index).toMap(); + QVariant variantProperties = avatarEntityProperties["properties"]; + QVariantMap asMap = variantProperties.toMap(); + QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine); + EntityItemProperties entityProperties; + EntityItemPropertiesFromScriptValueHonorReadOnly(scriptProperties, entityProperties); + + entityProperties.setParentID(myNodeID); + entityProperties.setClientOnly(true); + entityProperties.setOwningAvatarID(myNodeID); + entityProperties.setSimulationOwner(myNodeID, AVATAR_ENTITY_SIMULATION_PRIORITY); + entityProperties.markAllChanged(); + + EntityItemID id = EntityItemID(QUuid::createUuid()); + bool success = true; + entityTree->withWriteLock([&] { + EntityItemPointer entity = entityTree->addEntity(id, entityProperties); + if (entity) { + if (entityProperties.queryAACubeRelatedPropertyChanged()) { + // due to parenting, the server may not know where something is in world-space, so include the bounding cube. + bool success; + AACube queryAACube = entity->getQueryAACube(success); + if (success) { + entityProperties.setQueryAACube(queryAACube); + } + } + + entity->setLastBroadcast(usecTimestampNow()); + // since we're creating this object we will immediately volunteer to own its simulation + entity->flagForOwnershipBid(VOLUNTEER_SIMULATION_PRIORITY); + entityProperties.setLastEdited(entity->getLastEdited()); + } else { + qCDebug(entities) << "AvatarEntitiesBookmark failed to add new Entity to local Octree"; + success = false; + } + }); + + if (success) { + entityPacketSender->queueEditEntityMessage(PacketType::EntityAdd, entityTree, id, entityProperties); + } + } +} + +AvatarEntitiesBookmarks::AvatarEntitiesBookmarks() { + _bookmarksFilename = PathUtils::getAppDataPath() + "/" + AVATAR_ENTITIES_BOOKMARKS_FILENAME; + Bookmarks::readFromFile(); +} + +void AvatarEntitiesBookmarks::setupMenus(Menu* menubar, MenuWrapper* menu) { + auto bookmarkAction = menubar->addActionToQMenuAndActionHash(menu, MenuOption::BookmarkAvatarEntities); + QObject::connect(bookmarkAction, SIGNAL(triggered()), this, SLOT(addBookmark()), Qt::QueuedConnection); + _bookmarksMenu = menu->addMenu(MenuOption::AvatarEntitiesBookmarks); + _deleteBookmarksAction = menubar->addActionToQMenuAndActionHash(menu, MenuOption::DeleteAvatarEntitiesBookmark); + QObject::connect(_deleteBookmarksAction, SIGNAL(triggered()), this, SLOT(deleteBookmark()), Qt::QueuedConnection); + + for (auto it = _bookmarks.begin(); it != _bookmarks.end(); ++it) { + addBookmarkToMenu(menubar, it.key(), it.value()); + } + + Bookmarks::sortActions(menubar, _bookmarksMenu); +} + +void AvatarEntitiesBookmarks::applyBookmarkedAvatarEntities() { + QAction* action = qobject_cast(sender()); + auto myAvatar = DependencyManager::get()->getMyAvatar(); + + const QMap bookmark = action->data().toMap(); + + if (bookmark.value(ENTRY_VERSION) == AVATAR_ENTITIES_BOOKMARK_VERSION) { + myAvatar->removeAvatarEntities(); + const QString& avatarUrl = bookmark.value(ENTRY_AVATAR_URL, "").toString(); + myAvatar->useFullAvatarURL(avatarUrl); + const QVariantList& avatarEntities = bookmark.value(ENTRY_AVATAR_ENTITIES, QVariantList()).toList(); + addAvatarEntities(avatarEntities); + const float& avatarScale = bookmark.value(ENTRY_AVATAR_SCALE, 1.0f).toFloat(); + myAvatar->setAvatarScale(avatarScale); + } else { + qCDebug(interfaceapp) << " Bookmark entry does not match client version, make sure client has a handler for the new AvatarEntitiesBookmark"; + } +} + +void AvatarEntitiesBookmarks::addBookmark() { + ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_PLACEMARK, "Bookmark Avatar Entities", "Name", QString()); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + disconnect(dlg, &ModalDialogListener::response, this, nullptr); + auto bookmarkName = response.toString(); + bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " "); + if (bookmarkName.length() == 0) { + return; + } + + auto myAvatar = DependencyManager::get()->getMyAvatar(); + + const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString(); + const QVariant& avatarScale = myAvatar->getAvatarScale(); + + QVariantMap *bookmark = new QVariantMap; + bookmark->insert(ENTRY_VERSION, AVATAR_ENTITIES_BOOKMARK_VERSION); + bookmark->insert(ENTRY_AVATAR_URL, avatarUrl); + bookmark->insert(ENTRY_AVATAR_SCALE, avatarScale); + bookmark->insert(ENTRY_AVATAR_ENTITIES, myAvatar->getAvatarEntitiesVariant()); + + Bookmarks::addBookmarkToFile(bookmarkName, *bookmark); + }); +} + +void AvatarEntitiesBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) { + QAction* changeAction = _bookmarksMenu->newAction(); + changeAction->setData(bookmark); + connect(changeAction, SIGNAL(triggered()), this, SLOT(applyBookmarkedAvatarEntities())); + if (!_isMenuSorted) { + menubar->addActionToQMenuAndActionHash(_bookmarksMenu, changeAction, name, 0, QAction::NoRole); + } else { + // TODO: this is aggressive but other alternatives have proved less fruitful so far. + menubar->addActionToQMenuAndActionHash(_bookmarksMenu, changeAction, name, 0, QAction::NoRole); + Bookmarks::sortActions(menubar, _bookmarksMenu); + } +} diff --git a/interface/src/AvatarEntitiesBookmarks.h b/interface/src/AvatarEntitiesBookmarks.h new file mode 100644 index 0000000000..0c70e4dbc0 --- /dev/null +++ b/interface/src/AvatarEntitiesBookmarks.h @@ -0,0 +1,45 @@ +// +// AvatarEntitiesBookmarks.h +// interface/src +// +// Created by Dante Ruiz on 15/01/18. +// 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 +// + +#ifndef hifi_AvatarEntitiesBookmarks_h +#define hifi_AvatarEntitiesBookmarks_h + +#include +#include "Bookmarks.h" + +class AvatarEntitiesBookmarks: public Bookmarks, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + AvatarEntitiesBookmarks(); + void setupMenus(Menu* menubar, MenuWrapper* menu) override; + +public slots: + void addBookmark(); + +protected: + void addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) override; + +private: + const QString AVATAR_ENTITIES_BOOKMARKS_FILENAME = "AvatarEntitiesBookmarks.json"; + const QString ENTRY_AVATAR_URL = "AvatarUrl"; + const QString ENTRY_AVATAR_SCALE = "AvatarScale"; + const QString ENTRY_AVATAR_ENTITIES = "AvatarEntities"; + const QString ENTRY_VERSION = "version"; + + const int AVATAR_ENTITIES_BOOKMARK_VERSION = 1; + +private slots: + void applyBookmarkedAvatarEntities(); +}; + +#endif diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index b129d44c04..50e25287f1 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -34,6 +34,7 @@ #include "audio/AudioScope.h" #include "avatar/AvatarManager.h" #include "AvatarBookmarks.h" +#include "AvatarEntitiesBookmarks.h" #include "devices/DdeFaceTracker.h" #include "MainWindow.h" #include "render/DrawStatus.h" @@ -206,6 +207,9 @@ Menu::Menu() { auto avatarBookmarks = DependencyManager::get(); avatarBookmarks->setupMenus(this, avatarMenu); + auto avatarEntitiesBookmarks = DependencyManager::get(); + avatarEntitiesBookmarks->setupMenus(this, avatarMenu); + // Display menu ---------------------------------- // FIXME - this is not yet matching Alan's spec because it doesn't have // menus for "2D"/"3D" - we need to add support for detecting the appropriate diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 11a27ff7f5..8cb1804fd4 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -46,9 +46,11 @@ namespace MenuOption { const QString AutoMuteAudio = "Auto Mute Microphone"; const QString AvatarReceiveStats = "Show Receive Stats"; const QString AvatarBookmarks = "Avatar Bookmarks"; + const QString AvatarEntitiesBookmarks = "Avatar Entities Bookmarks"; const QString Back = "Back"; const QString BinaryEyelidControl = "Binary Eyelid Control"; const QString BookmarkAvatar = "Bookmark Avatar"; + const QString BookmarkAvatarEntities = "Bookmark Avatar Entities"; const QString BookmarkLocation = "Bookmark Location"; const QString CalibrateCamera = "Calibrate Camera"; const QString CameraEntityMode = "Entity Mode"; @@ -78,6 +80,7 @@ namespace MenuOption { const QString DecreaseAvatarSize = "Decrease Avatar Size"; const QString DefaultSkybox = "Default Skybox"; const QString DeleteAvatarBookmark = "Delete Avatar Bookmark..."; + const QString DeleteAvatarEntitiesBookmark = "Delete Avatar Entities Bookmark"; const QString DeleteBookmark = "Delete Bookmark..."; const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment"; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 93caef555f..b71c060465 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -546,7 +546,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic continue; } - QString extraInfo; + QVariantMap extraInfo; intersects = avatarModel->findRayIntersectionAgainstSubMeshes(ray.origin, normDirection, distance, face, surfaceNormal, extraInfo, true); @@ -554,6 +554,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic result.intersects = true; result.avatarID = avatar->getID(); result.distance = distance; + result.extraInfo = extraInfo; } } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 2e633986a0..19a2d39a05 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -35,6 +35,8 @@ #include #include #include +#include +#include #include #include #include @@ -561,6 +563,12 @@ void MyAvatar::simulate(float deltaTime) { if (!_skeletonModel->getHeadPosition(headPosition)) { headPosition = getWorldPosition(); } + + if (isNaN(headPosition)) { + qCDebug(interfaceapp) << "MyAvatar::simulate headPosition is NaN"; + headPosition = glm::vec3(0.0f); + } + head->setPosition(headPosition); head->setScale(getModelScale()); head->simulate(deltaTime); @@ -1415,6 +1423,37 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { } +void MyAvatar::removeAvatarEntities() { + auto treeRenderer = DependencyManager::get(); + EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; + if (entityTree) { + entityTree->withWriteLock([&] { + AvatarEntityMap avatarEntities = getAvatarEntityData(); + for (auto entityID : avatarEntities.keys()) { + entityTree->deleteEntity(entityID, true, true); + } + }); + } +} + +QVariantList MyAvatar::getAvatarEntitiesVariant() { + QVariantList avatarEntitiesData; + QScriptEngine scriptEngine; + forEachChild([&](SpatiallyNestablePointer child) { + if (child->getNestableType() == NestableType::Entity) { + auto modelEntity = std::dynamic_pointer_cast(child); + if (modelEntity) { + QVariantMap avatarEntityData; + EntityItemProperties entityProperties = modelEntity->getProperties(); + QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(&scriptEngine, entityProperties); + avatarEntityData["properties"] = scriptProperties.toVariant(); + avatarEntitiesData.append(QVariant(avatarEntityData)); + } + } + }); + return avatarEntitiesData; +} + void MyAvatar::resetFullAvatarURL() { auto lastAvatarURL = getFullAvatarURLFromPreferences(); @@ -2414,7 +2453,6 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette }; auto findIntersection = [&](const glm::vec3& startPointIn, const glm::vec3& directionIn, glm::vec3& intersectionOut, EntityItemID& entityIdOut, glm::vec3& normalOut) { OctreeElementPointer element; - EntityItemPointer intersectedEntity = NULL; float distance; BoxFace face; const bool visibleOnly = false; @@ -2426,13 +2464,14 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette const auto lockType = Octree::Lock; // Should we refactor to take a lock just once? bool* accurateResult = NULL; - bool intersects = entityTree->findRayIntersection(startPointIn, directionIn, include, ignore, visibleOnly, collidableOnly, precisionPicking, - element, distance, face, normalOut, (void**)&intersectedEntity, lockType, accurateResult); - if (!intersects || !intersectedEntity) { + QVariantMap extraInfo; + EntityItemID entityID = entityTree->findRayIntersection(startPointIn, directionIn, include, ignore, visibleOnly, collidableOnly, precisionPicking, + element, distance, face, normalOut, extraInfo, lockType, accurateResult); + if (entityID.isNull()) { return false; } intersectionOut = startPointIn + (directionIn * distance); - entityIdOut = intersectedEntity->getEntityItemID(); + entityIdOut = entityID; return true; }; @@ -2700,27 +2739,48 @@ void MyAvatar::setWalkSpeed(float value) { } glm::vec3 MyAvatar::getPositionForAudio() { + glm::vec3 result; switch (_audioListenerMode) { case AudioListenerMode::FROM_HEAD: - return getHead()->getPosition(); + result = getHead()->getPosition(); + break; case AudioListenerMode::FROM_CAMERA: - return qApp->getCamera().getPosition(); + result = qApp->getCamera().getPosition(); + break; case AudioListenerMode::CUSTOM: - return _customListenPosition; + result = _customListenPosition; + break; } - return vec3(); + + if (isNaN(result)) { + qCDebug(interfaceapp) << "MyAvatar::getPositionForAudio produced NaN" << _audioListenerMode; + result = glm::vec3(0.0f); + } + + return result; } glm::quat MyAvatar::getOrientationForAudio() { + glm::quat result; + switch (_audioListenerMode) { case AudioListenerMode::FROM_HEAD: - return getHead()->getFinalOrientationInWorldFrame(); + result = getHead()->getFinalOrientationInWorldFrame(); + break; case AudioListenerMode::FROM_CAMERA: - return qApp->getCamera().getOrientation(); + result = qApp->getCamera().getOrientation(); + break; case AudioListenerMode::CUSTOM: - return _customListenOrientation; + result = _customListenOrientation; + break; } - return quat(); + + if (isNaN(result)) { + qCDebug(interfaceapp) << "MyAvatar::getOrientationForAudio produced NaN" << _audioListenerMode; + result = glm::quat(); + } + + return result; } void MyAvatar::setAudioListenerMode(AudioListenerMode audioListenerMode) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 58b49b61ff..8587ddc9c2 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -512,6 +512,9 @@ public: bool hasDriveInput() const; + QVariantList getAvatarEntitiesVariant(); + void removeAvatarEntities(); + Q_INVOKABLE bool isFlying(); Q_INVOKABLE bool isInAir(); Q_INVOKABLE void setFlyingEnabled(bool enabled); diff --git a/interface/src/raypick/PickScriptingInterface.h b/interface/src/raypick/PickScriptingInterface.h index 0e0b3e113b..98427e34ca 100644 --- a/interface/src/raypick/PickScriptingInterface.h +++ b/interface/src/raypick/PickScriptingInterface.h @@ -114,6 +114,7 @@ public: * @property {float} distance The distance to the intersection point from the origin of the ray. * @property {Vec3} intersection The intersection point in world-space. * @property {Vec3} surfaceNormal The surface normal at the intersected point. All NANs if type == INTERSECTED_HUD. + * @property {Variant} extraInfo Additional intersection details when available for Model objects. * @property {PickRay} searchRay The PickRay that was used. Valid even if there was no intersection. */ @@ -127,6 +128,7 @@ public: * @property {float} distance The distance to the intersection point from the origin of the ray. * @property {Vec3} intersection The intersection point in world-space. * @property {Vec3} surfaceNormal The surface normal at the intersected point. All NANs if type == INTERSECTED_HUD. + * @property {Variant} extraInfo Additional intersection details when available for Model objects. * @property {StylusTip} stylusTip The StylusTip that was used. Valid even if there was no intersection. */ diff --git a/interface/src/raypick/RayPick.cpp b/interface/src/raypick/RayPick.cpp index 4a39d5df59..75b5e77fd8 100644 --- a/interface/src/raypick/RayPick.cpp +++ b/interface/src/raypick/RayPick.cpp @@ -19,7 +19,7 @@ PickResultPointer RayPick::getEntityIntersection(const PickRay& pick) { DependencyManager::get()->findRayIntersectionVector(pick, !getFilter().doesPickCoarse(), getIncludeItemsAs(), getIgnoreItemsAs(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable()); if (entityRes.intersects) { - return std::make_shared(IntersectionType::ENTITY, entityRes.entityID, entityRes.distance, entityRes.intersection, pick, entityRes.surfaceNormal); + return std::make_shared(IntersectionType::ENTITY, entityRes.entityID, entityRes.distance, entityRes.intersection, pick, entityRes.surfaceNormal, entityRes.extraInfo); } else { return std::make_shared(pick.toVariantMap()); } @@ -30,7 +30,7 @@ PickResultPointer RayPick::getOverlayIntersection(const PickRay& pick) { qApp->getOverlays().findRayIntersectionVector(pick, !getFilter().doesPickCoarse(), getIncludeItemsAs(), getIgnoreItemsAs(), !getFilter().doesPickInvisible(), !getFilter().doesPickNonCollidable()); if (overlayRes.intersects) { - return std::make_shared(IntersectionType::OVERLAY, overlayRes.overlayID, overlayRes.distance, overlayRes.intersection, pick, overlayRes.surfaceNormal); + return std::make_shared(IntersectionType::OVERLAY, overlayRes.overlayID, overlayRes.distance, overlayRes.intersection, pick, overlayRes.surfaceNormal, overlayRes.extraInfo); } else { return std::make_shared(pick.toVariantMap()); } @@ -39,7 +39,7 @@ PickResultPointer RayPick::getOverlayIntersection(const PickRay& pick) { PickResultPointer RayPick::getAvatarIntersection(const PickRay& pick) { RayToAvatarIntersectionResult avatarRes = DependencyManager::get()->findRayIntersectionVector(pick, getIncludeItemsAs(), getIgnoreItemsAs()); if (avatarRes.intersects) { - return std::make_shared(IntersectionType::AVATAR, avatarRes.avatarID, avatarRes.distance, avatarRes.intersection, pick); + return std::make_shared(IntersectionType::AVATAR, avatarRes.avatarID, avatarRes.distance, avatarRes.intersection, pick, glm::vec3(NAN), avatarRes.extraInfo); } else { return std::make_shared(pick.toVariantMap()); } diff --git a/interface/src/raypick/RayPick.h b/interface/src/raypick/RayPick.h index 25ad4df1f3..6bdc2cb5b0 100644 --- a/interface/src/raypick/RayPick.h +++ b/interface/src/raypick/RayPick.h @@ -18,8 +18,8 @@ class RayPickResult : public PickResult { public: RayPickResult() {} RayPickResult(const QVariantMap& pickVariant) : PickResult(pickVariant) {} - RayPickResult(const IntersectionType type, const QUuid& objectID, float distance, const glm::vec3& intersection, const PickRay& searchRay, const glm::vec3& surfaceNormal = glm::vec3(NAN)) : - PickResult(searchRay.toVariantMap()), type(type), intersects(type != NONE), objectID(objectID), distance(distance), intersection(intersection), surfaceNormal(surfaceNormal) { + RayPickResult(const IntersectionType type, const QUuid& objectID, float distance, const glm::vec3& intersection, const PickRay& searchRay, const glm::vec3& surfaceNormal = glm::vec3(NAN), const QVariantMap& extraInfo = QVariantMap()) : + PickResult(searchRay.toVariantMap()), type(type), intersects(type != NONE), objectID(objectID), distance(distance), intersection(intersection), surfaceNormal(surfaceNormal), extraInfo(extraInfo) { } RayPickResult(const RayPickResult& rayPickResult) : PickResult(rayPickResult.pickVariant) { @@ -29,6 +29,7 @@ public: distance = rayPickResult.distance; intersection = rayPickResult.intersection; surfaceNormal = rayPickResult.surfaceNormal; + extraInfo = rayPickResult.extraInfo; } IntersectionType type { NONE }; @@ -37,6 +38,7 @@ public: float distance { FLT_MAX }; glm::vec3 intersection { NAN }; glm::vec3 surfaceNormal { NAN }; + QVariantMap extraInfo; virtual QVariantMap toVariantMap() const override { QVariantMap toReturn; @@ -47,6 +49,7 @@ public: toReturn["intersection"] = vec3toVariant(intersection); toReturn["surfaceNormal"] = vec3toVariant(surfaceNormal); toReturn["searchRay"] = PickResult::toVariantMap(); + toReturn["extraInfo"] = extraInfo; return toReturn; } diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.cpp b/interface/src/scripting/AssetMappingsScriptingInterface.cpp index c13b5b2e0d..dc227c21ba 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.cpp +++ b/interface/src/scripting/AssetMappingsScriptingInterface.cpp @@ -260,7 +260,7 @@ void AssetMappingModel::refresh() { for (auto& mapping : mappings) { auto& path = mapping.first; - if (path.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) { + if (path.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) { // Hide baked mappings continue; } @@ -303,7 +303,7 @@ void AssetMappingModel::refresh() { auto statusString = isFolder ? "--" : bakingStatusToString(mapping.second.status); lastItem->setData(statusString, Qt::UserRole + 5); lastItem->setData(mapping.second.bakingErrors, Qt::UserRole + 6); - if (mapping.second.status == Pending) { + if (mapping.second.status == AssetUtils::Pending) { ++numPendingBakes; } } diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index d65b9850d7..df0f3c4728 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -68,7 +68,7 @@ public: BoxFace& face, glm::vec3& surfaceNormal); virtual bool findRayIntersectionExtraInfo(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, QString& extraInfo) { + float& distance, BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo) { return findRayIntersection(origin, direction, distance, face, surfaceNormal); } diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 4804a2db3d..310dbf78d8 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -446,12 +446,12 @@ QVariant ModelOverlay::getProperty(const QString& property) { bool ModelOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) { - QString subMeshNameTemp; - return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, face, surfaceNormal, subMeshNameTemp); + QVariantMap extraInfo; + return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, face, surfaceNormal, extraInfo); } bool ModelOverlay::findRayIntersectionExtraInfo(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, QString& extraInfo) { + float& distance, BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo) { return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, face, surfaceNormal, extraInfo); } diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index a39f762210..60ba90e568 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -41,7 +41,7 @@ public: virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) override; virtual bool findRayIntersectionExtraInfo(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, QString& extraInfo) override; + float& distance, BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo) override; virtual ModelOverlay* createClone() const override; diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 7f897aee4a..6898b5ed2b 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -520,7 +520,7 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionVector(const PickRay float thisDistance; BoxFace thisFace; glm::vec3 thisSurfaceNormal; - QString thisExtraInfo; + QVariantMap thisExtraInfo; if (thisOverlay->findRayIntersectionExtraInfo(ray.origin, ray.direction, thisDistance, thisFace, thisSurfaceNormal, thisExtraInfo)) { bool isDrawInFront = thisOverlay->getDrawInFront(); @@ -578,7 +578,7 @@ QScriptValue RayToOverlayIntersectionResultToScriptValue(QScriptEngine* engine, obj.setProperty("face", faceName); auto intersection = vec3toScriptValue(engine, value.intersection); obj.setProperty("intersection", intersection); - obj.setProperty("extraInfo", value.extraInfo); + obj.setProperty("extraInfo", engine->toScriptValue(value.extraInfo)); return obj; } @@ -612,7 +612,7 @@ void RayToOverlayIntersectionResultFromScriptValue(const QScriptValue& objectVar value.intersection = newIntersection; } } - value.extraInfo = object["extraInfo"].toString(); + value.extraInfo = object["extraInfo"].toMap(); } bool Overlays::isLoaded(OverlayID id) { diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index fd57869048..113ee92b80 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -51,6 +51,7 @@ const OverlayID UNKNOWN_OVERLAY_ID = OverlayID(); * @property {number} distance - The distance from the {@link PickRay} origin to the intersection point. * @property {Vec3} surfaceNormal - The normal of the overlay surface at the intersection point. * @property {Vec3} intersection - The position of the intersection point. + * @property {Object} extraInfo Additional intersection details, if available. */ class RayToOverlayIntersectionResult { public: @@ -60,7 +61,7 @@ public: BoxFace face; glm::vec3 surfaceNormal; glm::vec3 intersection; - QString extraInfo; + QVariantMap extraInfo; }; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index b1b41775a8..309bb59cff 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -445,22 +445,31 @@ void Rig::setJointRotation(int index, bool valid, const glm::quat& rotation, flo } bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, glm::vec3 translation, glm::quat rotation) const { + bool success { false }; if (QThread::currentThread() == thread()) { if (isIndexValid(jointIndex)) { position = (rotation * _internalPoseSet._absolutePoses[jointIndex].trans()) + translation; - return true; + success = true; } else { - return false; + success = false; + } + } else { + QReadLocker readLock(&_externalPoseSetLock); + if (jointIndex >= 0 && jointIndex < (int)_externalPoseSet._absolutePoses.size()) { + position = (rotation * _externalPoseSet._absolutePoses[jointIndex].trans()) + translation; + success = true; + } else { + success = false; } } - QReadLocker readLock(&_externalPoseSetLock); - if (jointIndex >= 0 && jointIndex < (int)_externalPoseSet._absolutePoses.size()) { - position = (rotation * _externalPoseSet._absolutePoses[jointIndex].trans()) + translation; - return true; - } else { - return false; + if (isNaN(position)) { + qCWarning(animation) << "Rig::getJointPositionInWorldFrame produces NaN"; + success = false; + position = glm::vec3(0.0f); } + + return success; } bool Rig::getJointPosition(int jointIndex, glm::vec3& position) const { diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c2aadb8723..74888283df 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -2504,6 +2504,7 @@ QScriptValue RayToAvatarIntersectionResultToScriptValue(QScriptEngine* engine, c obj.setProperty("distance", value.distance); QScriptValue intersection = vec3toScriptValue(engine, value.intersection); obj.setProperty("intersection", intersection); + obj.setProperty("extraInfo", engine->toScriptValue(value.extraInfo)); return obj; } @@ -2516,6 +2517,7 @@ void RayToAvatarIntersectionResultFromScriptValue(const QScriptValue& object, Ra if (intersection.isValid()) { vec3FromScriptValue(intersection, value.intersection); } + value.extraInfo = object.property("extraInfo").toVariant().toMap(); } const float AvatarData::OUT_OF_VIEW_PENALTY = -10.0f; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 92c505f5c3..a363fb6d15 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -982,6 +982,7 @@ RayToAvatarIntersectionResult() : intersects(false), avatarID(), distance(0) {} QUuid avatarID; float distance; glm::vec3 intersection; + QVariantMap extraInfo; }; Q_DECLARE_METATYPE(RayToAvatarIntersectionResult) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index c2a201a965..ba81922979 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -669,15 +669,16 @@ void EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { auto entityScriptingInterface = DependencyManager::get(); PickRay ray = _viewState->computePickRay(event->x(), event->y()); RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); - if (rayPickResult.intersects && rayPickResult.entity) { - auto properties = rayPickResult.entity->getProperties(); + EntityItemPointer entity; + if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { + auto properties = entity->getProperties(); QString urlString = properties.getHref(); QUrl url = QUrl(urlString, QUrl::StrictMode); if (url.isValid() && !url.isEmpty()){ DependencyManager::get()->handleLookupString(urlString); } - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Press, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, @@ -708,8 +709,9 @@ void EntityTreeRenderer::mouseDoublePressEvent(QMouseEvent* event) { auto entityScriptingInterface = DependencyManager::get(); PickRay ray = _viewState->computePickRay(event->x(), event->y()); RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); - if (rayPickResult.intersects && rayPickResult.entity) { - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + EntityItemPointer entity; + if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Press, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, @@ -738,10 +740,11 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) { auto entityScriptingInterface = DependencyManager::get(); PickRay ray = _viewState->computePickRay(event->x(), event->y()); RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); - if (rayPickResult.intersects && rayPickResult.entity) { + EntityItemPointer entity; + if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { // qCDebug(entitiesrenderer) << "mouseReleaseEvent over entity:" << rayPickResult.entityID; - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Release, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, @@ -757,7 +760,7 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) { // Even if we're no longer intersecting with an entity, if we started clicking on it, and now // we're releasing the button, then this is considered a clickReleaseOn event if (!_currentClickingOnEntityID.isInvalidID()) { - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Release, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, @@ -782,8 +785,9 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { auto entityScriptingInterface = DependencyManager::get(); PickRay ray = _viewState->computePickRay(event->x(), event->y()); RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); - if (rayPickResult.intersects && rayPickResult.entity) { - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + EntityItemPointer entity; + if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Move, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, @@ -797,7 +801,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { // if we were previously hovering over an entity, and this new entity is not the same as our previous entity // then we need to send the hover leave. if (!_currentHoverOverEntityID.isInvalidID() && rayPickResult.entityID != _currentHoverOverEntityID) { - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Move, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, @@ -828,7 +832,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { // if we were previously hovering over an entity, and we're no longer hovering over any entity then we need to // send the hover leave for our previous entity if (!_currentHoverOverEntityID.isInvalidID()) { - glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); PointerEvent pointerEvent(PointerEvent::Move, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 14a880d946..9fcb7640ef 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -282,7 +282,7 @@ bool RenderableModelEntityItem::supportsDetailedRayIntersection() const { bool RenderableModelEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, - glm::vec3& surfaceNormal, void** intersectedObject, bool precisionPicking) const { + glm::vec3& surfaceNormal, QVariantMap& extraInfo, bool precisionPicking) const { auto model = getModel(); if (!model) { return true; @@ -290,9 +290,8 @@ bool RenderableModelEntityItem::findDetailedRayIntersection(const glm::vec3& ori // qCDebug(entitiesrenderer) << "RenderableModelEntityItem::findDetailedRayIntersection() precisionPicking:" // << precisionPicking; - QString extraInfo; return model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, - face, surfaceNormal, extraInfo, precisionPicking, false); + face, surfaceNormal, extraInfo, precisionPicking, false); } void RenderableModelEntityItem::getCollisionGeometryResource() { diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index b3988e0239..33fc9910a0 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -70,7 +70,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; virtual void setShapeType(ShapeType type) override; virtual void setCompoundShapeURL(const QString& url) override; diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index aadd49fde8..ade3790df6 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -565,7 +565,7 @@ public: bool RenderablePolyVoxEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const + QVariantMap& extraInfo, bool precisionPicking) const { // TODO -- correctly pick against marching-cube generated meshes if (!precisionPicking) { diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 03c7351a59..db0f0b729a 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -55,7 +55,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; virtual void setVoxelData(const QByteArray& voxelData) override; virtual void setVoxelVolumeSize(const glm::vec3& voxelVolumeSize) override; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index f9559a375b..4c398b8a29 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -160,7 +160,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const { return true; } + QVariantMap& extraInfo, bool precisionPicking) const { return true; } // attributes applicable to all entity types EntityTypes::EntityType getType() const { return _type; } diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 206065beeb..4342f0e683 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -816,13 +816,12 @@ RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersectionWorke RayToEntityIntersectionResult result; if (_entityTree) { OctreeElementPointer element; - EntityItemPointer intersectedEntity = NULL; - result.intersects = _entityTree->findRayIntersection(ray.origin, ray.direction, + result.entityID = _entityTree->findRayIntersection(ray.origin, ray.direction, entityIdsToInclude, entityIdsToDiscard, visibleOnly, collidableOnly, precisionPicking, element, result.distance, result.face, result.surfaceNormal, - (void**)&intersectedEntity, lockType, &result.accurate); - if (result.intersects && intersectedEntity) { - result.entityID = intersectedEntity->getEntityItemID(); + result.extraInfo, lockType, &result.accurate); + result.intersects = !result.entityID.isNull(); + if (result.intersects) { result.intersection = ray.origin + (ray.direction * result.distance); } } @@ -988,8 +987,7 @@ RayToEntityIntersectionResult::RayToEntityIntersectionResult() : accurate(true), // assume it's accurate entityID(), distance(0), - face(), - entity(NULL) + face() { } @@ -1036,6 +1034,7 @@ QScriptValue RayToEntityIntersectionResultToScriptValue(QScriptEngine* engine, c QScriptValue surfaceNormal = vec3toScriptValue(engine, value.surfaceNormal); obj.setProperty("surfaceNormal", surfaceNormal); + obj.setProperty("extraInfo", engine->toScriptValue(value.extraInfo)); return obj; } @@ -1071,6 +1070,7 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra if (surfaceNormal.isValid()) { vec3FromScriptValue(surfaceNormal, value.surfaceNormal); } + value.extraInfo = object.property("extraInfo").toVariant().toMap(); } bool EntityScriptingInterface::polyVoxWorker(QUuid entityID, std::function actor) { diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 095a821482..4c4e2ffbfd 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -62,7 +62,7 @@ public: BoxFace face; glm::vec3 intersection; glm::vec3 surfaceNormal; - EntityItemPointer entity; + QVariantMap extraInfo; }; Q_DECLARE_METATYPE(RayToEntityIntersectionResult) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 91694c86aa..8dad126d30 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -59,8 +59,8 @@ public: float& distance; BoxFace& face; glm::vec3& surfaceNormal; - void** intersectedObject; - bool found; + QVariantMap& extraInfo; + EntityItemID entityID; }; @@ -748,23 +748,24 @@ bool findRayIntersectionOp(const OctreeElementPointer& element, void* extraData) RayArgs* args = static_cast(extraData); bool keepSearching = true; EntityTreeElementPointer entityTreeElementPointer = std::static_pointer_cast(element); - if (entityTreeElementPointer->findRayIntersection(args->origin, args->direction, keepSearching, + EntityItemID entityID = entityTreeElementPointer->findRayIntersection(args->origin, args->direction, keepSearching, args->element, args->distance, args->face, args->surfaceNormal, args->entityIdsToInclude, - args->entityIdsToDiscard, args->visibleOnly, args->collidableOnly, args->intersectedObject, args->precisionPicking)) { - args->found = true; + args->entityIdsToDiscard, args->visibleOnly, args->collidableOnly, args->extraInfo, args->precisionPicking); + if (!entityID.isNull()) { + args->entityID = entityID; } return keepSearching; } -bool EntityTree::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, +EntityItemID EntityTree::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, QVector entityIdsToInclude, QVector entityIdsToDiscard, bool visibleOnly, bool collidableOnly, bool precisionPicking, OctreeElementPointer& element, float& distance, - BoxFace& face, glm::vec3& surfaceNormal, void** intersectedObject, + BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo, Octree::lockType lockType, bool* accurateResult) { RayArgs args = { origin, direction, entityIdsToInclude, entityIdsToDiscard, visibleOnly, collidableOnly, precisionPicking, - element, distance, face, surfaceNormal, intersectedObject, false }; + element, distance, face, surfaceNormal, extraInfo, EntityItemID() }; distance = FLT_MAX; bool requireLock = lockType == Octree::Lock; @@ -776,7 +777,7 @@ bool EntityTree::findRayIntersection(const glm::vec3& origin, const glm::vec3& d *accurateResult = lockResult; // if user asked to accuracy or result, let them know this is accurate } - return args.found; + return args.entityID; } diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index fa44fd37b8..f4ff7ad118 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -97,11 +97,11 @@ public: virtual void processChallengeOwnershipReplyPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) override; virtual void processChallengeOwnershipPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) override; - virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + virtual EntityItemID findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, QVector entityIdsToInclude, QVector entityIdsToDiscard, bool visibleOnly, bool collidableOnly, bool precisionPicking, OctreeElementPointer& node, float& distance, - BoxFace& face, glm::vec3& surfaceNormal, void** intersectedObject = NULL, + BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo, Octree::lockType lockType = Octree::TryLock, bool* accurateResult = NULL); virtual bool rootElementHasData() const override { return true; } diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 926975f735..c9e1ecc3f1 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -588,57 +588,60 @@ bool EntityTreeElement::bestFitBounds(const glm::vec3& minPoint, const glm::vec3 return false; } -bool EntityTreeElement::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, +EntityItemID EntityTreeElement::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, const QVector& entityIdsToInclude, const QVector& entityIdsToDiscard, bool visibleOnly, bool collidableOnly, - void** intersectedObject, bool precisionPicking) { + QVariantMap& extraInfo, bool precisionPicking) { keepSearching = true; // assume that we will continue searching after this. + EntityItemID result; float distanceToElementCube = std::numeric_limits::max(); float distanceToElementDetails = distance; BoxFace localFace; glm::vec3 localSurfaceNormal; + QVariantMap localExtraInfo; // if the ray doesn't intersect with our cube, we can stop searching! if (!_cube.findRayIntersection(origin, direction, distanceToElementCube, localFace, localSurfaceNormal)) { keepSearching = false; // no point in continuing to search - return false; // we did not intersect + return result; // we did not intersect } // by default, we only allow intersections with leaves with content if (!canRayIntersect()) { - return false; // we don't intersect with non-leaves, and we keep searching + return result; // we don't intersect with non-leaves, and we keep searching } // if the distance to the element cube is not less than the current best distance, then it's not possible // for any details inside the cube to be closer so we don't need to consider them. if (_cube.contains(origin) || distanceToElementCube < distance) { - if (findDetailedRayIntersection(origin, direction, keepSearching, element, distanceToElementDetails, + EntityItemID entityID = findDetailedRayIntersection(origin, direction, keepSearching, element, distanceToElementDetails, face, localSurfaceNormal, entityIdsToInclude, entityIdsToDiscard, visibleOnly, collidableOnly, - intersectedObject, precisionPicking, distanceToElementCube)) { - + localExtraInfo, precisionPicking, distanceToElementCube); + if (!entityID.isNull()) { if (distanceToElementDetails < distance) { distance = distanceToElementDetails; face = localFace; surfaceNormal = localSurfaceNormal; - return true; + extraInfo = localExtraInfo; + result = entityID; } } } - return false; + return result; } -bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, +EntityItemID EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, const QVector& entityIdsToInclude, const QVector& entityIDsToDiscard, - bool visibleOnly, bool collidableOnly, void** intersectedObject, bool precisionPicking, float distanceToElementCube) { + bool visibleOnly, bool collidableOnly, QVariantMap& extraInfo, bool precisionPicking, float distanceToElementCube) { // only called if we do intersect our bounding cube, but find if we actually intersect with entities... int entityNumber = 0; - bool somethingIntersected = false; + EntityItemID entityID; forEachEntity([&](EntityItemPointer entity) { if ( (visibleOnly && !entity->isVisible()) || (collidableOnly && (entity->getCollisionless() || entity->getShapeType() == SHAPE_TYPE_NONE)) || (entityIdsToInclude.size() > 0 && !entityIdsToInclude.contains(entity->getID())) @@ -655,6 +658,7 @@ bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, con float localDistance; BoxFace localFace; glm::vec3 localSurfaceNormal; + QVariantMap localExtraInfo; // if the ray doesn't intersect with our cube, we can stop searching! if (!entityBox.findRayIntersection(origin, direction, localDistance, localFace, localSurfaceNormal)) { @@ -684,14 +688,14 @@ bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, con // now ask the entity if we actually intersect if (entity->supportsDetailedRayIntersection()) { if (entity->findDetailedRayIntersection(origin, direction, keepSearching, element, localDistance, - localFace, localSurfaceNormal, intersectedObject, precisionPicking)) { + localFace, localSurfaceNormal, localExtraInfo, precisionPicking)) { if (localDistance < distance) { distance = localDistance; face = localFace; surfaceNormal = localSurfaceNormal; - *intersectedObject = (void*)entity.get(); - somethingIntersected = true; + extraInfo = localExtraInfo; + entityID = entity->getEntityItemID(); } } } else { @@ -701,15 +705,14 @@ bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, con distance = localDistance; face = localFace; surfaceNormal = glm::vec3(rotation * glm::vec4(localSurfaceNormal, 1.0f)); - *intersectedObject = (void*)entity.get(); - somethingIntersected = true; + entityID = entity->getEntityItemID(); } } } } entityNumber++; }); - return somethingIntersected; + return entityID; } // TODO: change this to use better bounding shape for entity than sphere diff --git a/libraries/entities/src/EntityTreeElement.h b/libraries/entities/src/EntityTreeElement.h index cafae9941a..a524904c71 100644 --- a/libraries/entities/src/EntityTreeElement.h +++ b/libraries/entities/src/EntityTreeElement.h @@ -146,16 +146,16 @@ public: virtual bool deleteApproved() const override { return !hasEntities(); } virtual bool canRayIntersect() const override { return hasEntities(); } - virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + virtual EntityItemID findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& node, float& distance, BoxFace& face, glm::vec3& surfaceNormal, const QVector& entityIdsToInclude, - const QVector& entityIdsToDiscard, bool visibleOnly = false, bool collidableOnly = false, - void** intersectedObject = NULL, bool precisionPicking = false); - virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + const QVector& entityIdsToDiscard, bool visibleOnly, bool collidableOnly, + QVariantMap& extraInfo, bool precisionPicking = false); + virtual EntityItemID findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, const QVector& entityIdsToInclude, const QVector& entityIdsToDiscard, bool visibleOnly, bool collidableOnly, - void** intersectedObject, bool precisionPicking, float distanceToElementCube); + QVariantMap& extraInfo, bool precisionPicking, float distanceToElementCube); virtual bool findSpherePenetration(const glm::vec3& center, float radius, glm::vec3& penetration, void** penetratedObject) const override; diff --git a/libraries/entities/src/LightEntityItem.cpp b/libraries/entities/src/LightEntityItem.cpp index 45f36a1744..85edefa413 100644 --- a/libraries/entities/src/LightEntityItem.cpp +++ b/libraries/entities/src/LightEntityItem.cpp @@ -300,7 +300,7 @@ void LightEntityItem::resetLightPropertiesChanged() { bool LightEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const { + QVariantMap& extraInfo, bool precisionPicking) const { // TODO: consider if this is really what we want to do. We've made it so that "lights are pickable" is a global state // this is probably reasonable since there's typically only one tree you'd be picking on at a time. Technically we could diff --git a/libraries/entities/src/LightEntityItem.h b/libraries/entities/src/LightEntityItem.h index 870b283c26..3be1d48aa5 100644 --- a/libraries/entities/src/LightEntityItem.h +++ b/libraries/entities/src/LightEntityItem.h @@ -88,7 +88,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; private: // properties of a light diff --git a/libraries/entities/src/LineEntityItem.h b/libraries/entities/src/LineEntityItem.h index 7fda113510..9f16807084 100644 --- a/libraries/entities/src/LineEntityItem.h +++ b/libraries/entities/src/LineEntityItem.h @@ -63,7 +63,7 @@ class LineEntityItem : public EntityItem { virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, + QVariantMap& extraInfo, bool precisionPicking) const override { return false; } bool pointsChanged() const { return _pointsChanged; } void resetPointsChanged(); diff --git a/libraries/entities/src/PolyLineEntityItem.h b/libraries/entities/src/PolyLineEntityItem.h index 3164a9646b..8af2b26216 100644 --- a/libraries/entities/src/PolyLineEntityItem.h +++ b/libraries/entities/src/PolyLineEntityItem.h @@ -96,7 +96,7 @@ class PolyLineEntityItem : public EntityItem { virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override { return false; } + QVariantMap& extraInfo, bool precisionPicking) const override { return false; } // disable these external interfaces as PolyLineEntities caculate their own dimensions based on the points they contain virtual void setRegistrationPoint(const glm::vec3& value) override {}; // FIXME: this is suspicious! diff --git a/libraries/entities/src/PolyVoxEntityItem.h b/libraries/entities/src/PolyVoxEntityItem.h index d8d998b944..47d2a4b4e1 100644 --- a/libraries/entities/src/PolyVoxEntityItem.h +++ b/libraries/entities/src/PolyVoxEntityItem.h @@ -47,7 +47,7 @@ class PolyVoxEntityItem : public EntityItem { virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override { return false; } + QVariantMap& extraInfo, bool precisionPicking) const override { return false; } virtual void debugDump() const override; diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index 3750bc3b57..cbcfcaaa1d 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -223,7 +223,7 @@ bool ShapeEntityItem::supportsDetailedRayIntersection() const { bool ShapeEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const { + QVariantMap& extraInfo, bool precisionPicking) const { // determine the ray in the frame of the entity transformed from a unit sphere glm::mat4 entityToWorldMatrix = getEntityToWorldMatrix(); glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix); diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h index c5df17db54..84ce1ce57e 100644 --- a/libraries/entities/src/ShapeEntityItem.h +++ b/libraries/entities/src/ShapeEntityItem.h @@ -94,7 +94,7 @@ public: bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; void debugDump() const override; diff --git a/libraries/entities/src/TextEntityItem.cpp b/libraries/entities/src/TextEntityItem.cpp index 639da69a17..7b1089e6ed 100644 --- a/libraries/entities/src/TextEntityItem.cpp +++ b/libraries/entities/src/TextEntityItem.cpp @@ -131,7 +131,7 @@ void TextEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBits bool TextEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const { + QVariantMap& extraInfo, bool precisionPicking) const { glm::vec3 dimensions = getScaledDimensions(); glm::vec2 xyDimensions(dimensions.x, dimensions.y); glm::quat rotation = getWorldOrientation(); diff --git a/libraries/entities/src/TextEntityItem.h b/libraries/entities/src/TextEntityItem.h index 520a935e55..3ab743ecfd 100644 --- a/libraries/entities/src/TextEntityItem.h +++ b/libraries/entities/src/TextEntityItem.h @@ -50,7 +50,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; static const QString DEFAULT_TEXT; void setText(const QString& value); diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp index 54db6e7c3b..91e7bca063 100644 --- a/libraries/entities/src/WebEntityItem.cpp +++ b/libraries/entities/src/WebEntityItem.cpp @@ -108,7 +108,7 @@ void WebEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitst bool WebEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const { + QVariantMap& extraInfo, bool precisionPicking) const { glm::vec3 dimensions = getScaledDimensions(); glm::vec2 xyDimensions(dimensions.x, dimensions.y); glm::quat rotation = getWorldOrientation(); diff --git a/libraries/entities/src/WebEntityItem.h b/libraries/entities/src/WebEntityItem.h index f5066beebc..7d8f37cd83 100644 --- a/libraries/entities/src/WebEntityItem.h +++ b/libraries/entities/src/WebEntityItem.h @@ -49,7 +49,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; virtual void setSourceUrl(const QString& value); QString getSourceUrl() const; diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 107e7cf561..6083e5b8de 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -298,7 +298,7 @@ void ZoneEntityItem::setCompoundShapeURL(const QString& url) { bool ZoneEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const { + QVariantMap& extraInfo, bool precisionPicking) const { return _zonesArePickable; } diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 05dcc05416..95b6248fde 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -107,7 +107,7 @@ public: virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const override; + QVariantMap& extraInfo, bool precisionPicking) const override; virtual void debugDump() const override; diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 171fc88443..4fac3b8f0f 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1891,6 +1891,9 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS geometry.meshes.append(extracted.mesh); int meshIndex = geometry.meshes.size() - 1; + if (extracted.mesh._mesh) { + extracted.mesh._mesh->displayName = QString("%1#/mesh/%2").arg(url).arg(meshIndex); + } meshIDsToMeshIndices.insert(it.key(), meshIndex); } @@ -1959,7 +1962,19 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } } } - + { + int i = 0; + for (const auto& mesh : geometry.meshes) { + auto name = geometry.getModelNameOfMesh(i++); + if (!name.isEmpty()) { + if (mesh._mesh) { + mesh._mesh->displayName += "#" + name; + } else { + qDebug() << "modelName but no mesh._mesh" << name; + } + } + } + } return geometryPtr; } @@ -1975,7 +1990,7 @@ FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QStri reader._loadLightmaps = loadLightmaps; reader._lightmapLevel = lightmapLevel; - qDebug() << "Reading FBX: " << url; + qCDebug(modelformat) << "Reading FBX: " << url; return reader.extractFBXGeometry(mapping, url); } diff --git a/libraries/graphics/src/graphics/Geometry.h b/libraries/graphics/src/graphics/Geometry.h index cd64a7c185..642aa9e38d 100755 --- a/libraries/graphics/src/graphics/Geometry.h +++ b/libraries/graphics/src/graphics/Geometry.h @@ -135,6 +135,8 @@ public: static MeshPointer createIndexedTriangles_P3F(uint32_t numVertices, uint32_t numTriangles, const glm::vec3* vertices = nullptr, const uint32_t* indices = nullptr); + QString displayName; + protected: gpu::Stream::FormatPointer _vertexFormat; diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 6fdfc5a42b..1db93e8cb9 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -20,6 +20,7 @@ #include #include +#include #include "AssetRequest.h" #include "AssetUpload.h" @@ -72,7 +73,177 @@ void AssetClient::init() { networkAccessManager.setCache(cache); qInfo() << "ResourceManager disk cache setup at" << _cacheDir << "(size:" << MAXIMUM_CACHE_SIZE / BYTES_PER_GIGABYTES << "GB)"; + } else { + auto cache = qobject_cast(networkAccessManager.cache()); + qInfo() << "ResourceManager disk cache already setup at" << cache->cacheDirectory() + << "(size:" << cache->maximumCacheSize() / BYTES_PER_GIGABYTES << "GB)"; } + +} + +namespace { + const QString& CACHE_ERROR_MESSAGE{ "AssetClient::Error: %1 %2" }; +} + +MiniPromise::Promise AssetClient::cacheInfoRequestAsync(MiniPromise::Promise deferred) { + if (!deferred) { + deferred = makePromise(__FUNCTION__); // create on caller's thread + } + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "cacheInfoRequestAsync", Q_ARG(MiniPromise::Promise, deferred)); + } else { + auto cache = qobject_cast(NetworkAccessManager::getInstance().cache()); + if (cache) { + deferred->resolve({ + { "cacheDirectory", cache->cacheDirectory() }, + { "cacheSize", cache->cacheSize() }, + { "maximumCacheSize", cache->maximumCacheSize() }, + }); + } else { + deferred->reject(CACHE_ERROR_MESSAGE.arg(__FUNCTION__).arg("cache unavailable")); + } + } + return deferred; +} + +MiniPromise::Promise AssetClient::queryCacheMetaAsync(const QUrl& url, MiniPromise::Promise deferred) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "queryCacheMetaAsync", Q_ARG(const QUrl&, url), Q_ARG(MiniPromise::Promise, deferred)); + } else { + auto cache = NetworkAccessManager::getInstance().cache(); + if (cache) { + QNetworkCacheMetaData metaData = cache->metaData(url); + QVariantMap attributes, rawHeaders; + if (!metaData.isValid()) { + deferred->reject("invalid cache entry", { + { "_url", url }, + { "isValid", metaData.isValid() }, + { "metaDataURL", metaData.url() }, + }); + } else { + auto metaAttributes = metaData.attributes(); + foreach(QNetworkRequest::Attribute k, metaAttributes.keys()) { + attributes[QString::number(k)] = metaAttributes[k]; + } + for (const auto& i : metaData.rawHeaders()) { + rawHeaders[i.first] = i.second; + } + deferred->resolve({ + { "_url", url }, + { "isValid", metaData.isValid() }, + { "url", metaData.url() }, + { "expirationDate", metaData.expirationDate() }, + { "lastModified", metaData.lastModified().toString().isEmpty() ? QDateTime() : metaData.lastModified() }, + { "saveToDisk", metaData.saveToDisk() }, + { "attributes", attributes }, + { "rawHeaders", rawHeaders }, + }); + } + } else { + deferred->reject(CACHE_ERROR_MESSAGE.arg(__FUNCTION__).arg("cache unavailable")); + } + } + return deferred; +} + +MiniPromise::Promise AssetClient::loadFromCacheAsync(const QUrl& url, MiniPromise::Promise deferred) { + auto errorMessage = CACHE_ERROR_MESSAGE.arg(__FUNCTION__); + if (!deferred) { + deferred = makePromise(__FUNCTION__); // create on caller's thread + } + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "loadFromCacheAsync", Q_ARG(const QUrl&, url), Q_ARG(MiniPromise::Promise, deferred)); + } else { + auto cache = NetworkAccessManager::getInstance().cache(); + if (cache) { + MiniPromise::Promise metaRequest = makePromise(__FUNCTION__); + queryCacheMetaAsync(url, metaRequest); + metaRequest->finally([&](QString error, QVariantMap metadata) { + QVariantMap result = { + { "url", url }, + { "metadata", metadata }, + { "data", QByteArray() }, + }; + if (!error.isEmpty()) { + deferred->reject(error, result); + return; + } + // caller is responsible for the deletion of the ioDevice, hence the unique_ptr + auto ioDevice = std::unique_ptr(cache->data(url)); + if (ioDevice) { + result["data"] = ioDevice->readAll(); + } else { + error = errorMessage.arg("error reading data"); + } + deferred->handle(error, result); + }); + } else { + deferred->reject(errorMessage.arg("cache unavailable")); + } + } + return deferred; +} + +namespace { + // parse RFC 1123 HTTP date format + QDateTime parseHttpDate(const QString& dateString) { + QDateTime dt = QDateTime::fromString(dateString.left(25), "ddd, dd MMM yyyy HH:mm:ss"); + if (!dt.isValid()) { + dt = QDateTime::fromString(dateString, Qt::ISODateWithMs); + } + if (!dt.isValid()) { + qDebug() << __FUNCTION__ << "unrecognized date format:" << dateString; + } + dt.setTimeSpec(Qt::UTC); + return dt; + } + QDateTime getHttpDateValue(const QVariantMap& headers, const QString& keyName, const QDateTime& defaultValue) { + return headers.contains(keyName) ? parseHttpDate(headers[keyName].toString()) : defaultValue; + } +} + +MiniPromise::Promise AssetClient::saveToCacheAsync(const QUrl& url, const QByteArray& data, const QVariantMap& headers, MiniPromise::Promise deferred) { + if (!deferred) { + deferred = makePromise(__FUNCTION__); // create on caller's thread + } + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod( + this, "saveToCacheAsync", Qt::QueuedConnection, + Q_ARG(const QUrl&, url), + Q_ARG(const QByteArray&, data), + Q_ARG(const QVariantMap&, headers), + Q_ARG(MiniPromise::Promise, deferred)); + } else { + auto cache = NetworkAccessManager::getInstance().cache(); + if (cache) { + QNetworkCacheMetaData metaData; + metaData.setUrl(url); + metaData.setSaveToDisk(true); + metaData.setLastModified(getHttpDateValue(headers, "last-modified", QDateTime::currentDateTimeUtc())); + metaData.setExpirationDate(getHttpDateValue(headers, "expires", QDateTime())); // nil defaultValue == never expires + auto ioDevice = cache->prepare(metaData); + if (ioDevice) { + ioDevice->write(data); + cache->insert(ioDevice); + qCDebug(asset_client) << url.toDisplayString() << "saved to disk cache ("<< data.size()<<" bytes)"; + deferred->resolve({ + { "url", url }, + { "success", true }, + { "metaDataURL", metaData.url() }, + { "byteLength", data.size() }, + { "expirationDate", metaData.expirationDate() }, + { "lastModified", metaData.lastModified().toString().isEmpty() ? QDateTime() : metaData.lastModified() }, + }); + } else { + auto error = QString("Could not save %1 to disk cache").arg(url.toDisplayString()); + qCWarning(asset_client) << error; + deferred->reject(CACHE_ERROR_MESSAGE.arg(__FUNCTION__).arg(error)); + } + } else { + deferred->reject(CACHE_ERROR_MESSAGE.arg(__FUNCTION__).arg("unavailable")); + } + } + return deferred; } void AssetClient::cacheInfoRequest(QObject* reciever, QString slot) { @@ -113,7 +284,7 @@ void AssetClient::handleAssetMappingOperationReply(QSharedPointerreadPrimitive(&messageID); - AssetServerError error; + AssetUtils::AssetServerError error; message->readPrimitive(&error); // Check if we have any pending requests for this node @@ -149,7 +320,7 @@ bool haveAssetServer() { return true; } -GetMappingRequest* AssetClient::createGetMappingRequest(const AssetPath& path) { +GetMappingRequest* AssetClient::createGetMappingRequest(const AssetUtils::AssetPath& path) { auto request = new GetMappingRequest(path); request->moveToThread(thread()); @@ -165,7 +336,7 @@ GetAllMappingsRequest* AssetClient::createGetAllMappingsRequest() { return request; } -DeleteMappingsRequest* AssetClient::createDeleteMappingsRequest(const AssetPathList& paths) { +DeleteMappingsRequest* AssetClient::createDeleteMappingsRequest(const AssetUtils::AssetPathList& paths) { auto request = new DeleteMappingsRequest(paths); request->moveToThread(thread()); @@ -173,7 +344,7 @@ DeleteMappingsRequest* AssetClient::createDeleteMappingsRequest(const AssetPathL return request; } -SetMappingRequest* AssetClient::createSetMappingRequest(const AssetPath& path, const AssetHash& hash) { +SetMappingRequest* AssetClient::createSetMappingRequest(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash) { auto request = new SetMappingRequest(path, hash); request->moveToThread(thread()); @@ -181,7 +352,7 @@ SetMappingRequest* AssetClient::createSetMappingRequest(const AssetPath& path, c return request; } -RenameMappingRequest* AssetClient::createRenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath) { +RenameMappingRequest* AssetClient::createRenameMappingRequest(const AssetUtils::AssetPath& oldPath, const AssetUtils::AssetPath& newPath) { auto request = new RenameMappingRequest(oldPath, newPath); request->moveToThread(thread()); @@ -189,7 +360,7 @@ RenameMappingRequest* AssetClient::createRenameMappingRequest(const AssetPath& o return request; } -SetBakingEnabledRequest* AssetClient::createSetBakingEnabledRequest(const AssetPathList& path, bool enabled) { +SetBakingEnabledRequest* AssetClient::createSetBakingEnabledRequest(const AssetUtils::AssetPathList& path, bool enabled) { auto bakingEnabledRequest = new SetBakingEnabledRequest(path, enabled); bakingEnabledRequest->moveToThread(thread()); @@ -197,7 +368,7 @@ SetBakingEnabledRequest* AssetClient::createSetBakingEnabledRequest(const AssetP return bakingEnabledRequest; } -AssetRequest* AssetClient::createRequest(const AssetHash& hash, const ByteRange& byteRange) { +AssetRequest* AssetClient::createRequest(const AssetUtils::AssetHash& hash, const ByteRange& byteRange) { auto request = new AssetRequest(hash, byteRange); // Move to the AssetClient thread in case we are not currently on that thread (which will usually be the case) @@ -222,11 +393,11 @@ AssetUpload* AssetClient::createUpload(const QByteArray& data) { return upload; } -MessageID AssetClient::getAsset(const QString& hash, DataOffset start, DataOffset end, +MessageID AssetClient::getAsset(const QString& hash, AssetUtils::DataOffset start, AssetUtils::DataOffset end, ReceivedAssetCallback callback, ProgressCallback progressCallback) { Q_ASSERT(QThread::currentThread() == thread()); - if (hash.length() != SHA256_HASH_HEX_LENGTH) { + if (hash.length() != AssetUtils::SHA256_HASH_HEX_LENGTH) { qCWarning(asset_client) << "Invalid hash size"; return false; } @@ -238,7 +409,7 @@ MessageID AssetClient::getAsset(const QString& hash, DataOffset start, DataOffse auto messageID = ++_currentID; - auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH + sizeof(start) + sizeof(end); + auto payloadSize = sizeof(messageID) + AssetUtils::SHA256_HASH_LENGTH + sizeof(start) + sizeof(end); auto packet = NLPacket::create(PacketType::AssetGet, payloadSize, true); qCDebug(asset_client) << "Requesting data from" << start << "to" << end << "of" << hash << "from asset-server."; @@ -257,7 +428,7 @@ MessageID AssetClient::getAsset(const QString& hash, DataOffset start, DataOffse } } - callback(false, AssetServerError::NoError, QByteArray()); + callback(false, AssetUtils::AssetServerError::NoError, QByteArray()); return INVALID_MESSAGE_ID; } @@ -270,7 +441,7 @@ MessageID AssetClient::getAssetInfo(const QString& hash, GetInfoCallback callbac if (assetServer) { auto messageID = ++_currentID; - auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH; + auto payloadSize = sizeof(messageID) + AssetUtils::SHA256_HASH_LENGTH; auto packet = NLPacket::create(PacketType::AssetGetInfo, payloadSize, true); packet->writePrimitive(messageID); @@ -283,7 +454,7 @@ MessageID AssetClient::getAssetInfo(const QString& hash, GetInfoCallback callbac } } - callback(false, AssetServerError::NoError, { "", 0 }); + callback(false, AssetUtils::AssetServerError::NoError, { "", 0 }); return INVALID_MESSAGE_ID; } @@ -292,14 +463,14 @@ void AssetClient::handleAssetGetInfoReply(QSharedPointer messag MessageID messageID; message->readPrimitive(&messageID); - auto assetHash = message->read(SHA256_HASH_LENGTH); + auto assetHash = message->read(AssetUtils::SHA256_HASH_LENGTH); - AssetServerError error; + AssetUtils::AssetServerError error; message->readPrimitive(&error); AssetInfo info { assetHash.toHex(), 0 }; - if (error == AssetServerError::NoError) { + if (error == AssetUtils::AssetServerError::NoError) { message->readPrimitive(&info.size); } @@ -326,16 +497,16 @@ void AssetClient::handleAssetGetInfoReply(QSharedPointer messag void AssetClient::handleAssetGetReply(QSharedPointer message, SharedNodePointer senderNode) { Q_ASSERT(QThread::currentThread() == thread()); - auto assetHash = message->readHead(SHA256_HASH_LENGTH); + auto assetHash = message->readHead(AssetUtils::SHA256_HASH_LENGTH); qCDebug(asset_client) << "Got reply for asset: " << assetHash.toHex(); MessageID messageID; message->readHeadPrimitive(&messageID); - AssetServerError error; + AssetUtils::AssetServerError error; message->readHeadPrimitive(&error); - DataOffset length = 0; + AssetUtils::DataOffset length = 0; if (!error) { message->readHeadPrimitive(&length); } else { @@ -388,7 +559,7 @@ void AssetClient::handleAssetGetReply(QSharedPointer message, S } void AssetClient::handleProgressCallback(const QWeakPointer& node, MessageID messageID, - qint64 size, DataOffset length) { + qint64 size, AssetUtils::DataOffset length) { auto senderNode = node.toStrongRef(); if (!senderNode) { @@ -414,7 +585,7 @@ void AssetClient::handleProgressCallback(const QWeakPointer& node, Message callbacks.progressCallback(size, length); } -void AssetClient::handleCompleteCallback(const QWeakPointer& node, MessageID messageID, DataOffset length) { +void AssetClient::handleCompleteCallback(const QWeakPointer& node, MessageID messageID, AssetUtils::DataOffset length) { auto senderNode = node.toStrongRef(); if (!senderNode) { @@ -448,9 +619,9 @@ void AssetClient::handleCompleteCallback(const QWeakPointer& node, Message } if (message->failed() || length != message->getBytesLeftToRead()) { - callbacks.completeCallback(false, AssetServerError::NoError, QByteArray()); + callbacks.completeCallback(false, AssetUtils::AssetServerError::NoError, QByteArray()); } else { - callbacks.completeCallback(true, AssetServerError::NoError, message->readAll()); + callbacks.completeCallback(true, AssetUtils::AssetServerError::NoError, message->readAll()); } // We should never get to this point without the associated senderNode and messageID @@ -461,7 +632,7 @@ void AssetClient::handleCompleteCallback(const QWeakPointer& node, Message } -MessageID AssetClient::getAssetMapping(const AssetPath& path, MappingOperationCallback callback) { +MessageID AssetClient::getAssetMapping(const AssetUtils::AssetPath& path, MappingOperationCallback callback) { Q_ASSERT(QThread::currentThread() == thread()); auto nodeList = DependencyManager::get(); @@ -473,7 +644,7 @@ MessageID AssetClient::getAssetMapping(const AssetPath& path, MappingOperationCa auto messageID = ++_currentID; packetList->writePrimitive(messageID); - packetList->writePrimitive(AssetMappingOperationType::Get); + packetList->writePrimitive(AssetUtils::AssetMappingOperationType::Get); packetList->writeString(path); @@ -484,7 +655,7 @@ MessageID AssetClient::getAssetMapping(const AssetPath& path, MappingOperationCa } } - callback(false, AssetServerError::NoError, QSharedPointer()); + callback(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); return INVALID_MESSAGE_ID; } @@ -500,7 +671,7 @@ MessageID AssetClient::getAllAssetMappings(MappingOperationCallback callback) { auto messageID = ++_currentID; packetList->writePrimitive(messageID); - packetList->writePrimitive(AssetMappingOperationType::GetAll); + packetList->writePrimitive(AssetUtils::AssetMappingOperationType::GetAll); if (nodeList->sendPacketList(std::move(packetList), *assetServer) != -1) { _pendingMappingRequests[assetServer][messageID] = callback; @@ -509,11 +680,11 @@ MessageID AssetClient::getAllAssetMappings(MappingOperationCallback callback) { } } - callback(false, AssetServerError::NoError, QSharedPointer()); + callback(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); return INVALID_MESSAGE_ID; } -MessageID AssetClient::deleteAssetMappings(const AssetPathList& paths, MappingOperationCallback callback) { +MessageID AssetClient::deleteAssetMappings(const AssetUtils::AssetPathList& paths, MappingOperationCallback callback) { auto nodeList = DependencyManager::get(); SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); @@ -523,7 +694,7 @@ MessageID AssetClient::deleteAssetMappings(const AssetPathList& paths, MappingOp auto messageID = ++_currentID; packetList->writePrimitive(messageID); - packetList->writePrimitive(AssetMappingOperationType::Delete); + packetList->writePrimitive(AssetUtils::AssetMappingOperationType::Delete); packetList->writePrimitive(int(paths.size())); @@ -538,11 +709,11 @@ MessageID AssetClient::deleteAssetMappings(const AssetPathList& paths, MappingOp } } - callback(false, AssetServerError::NoError, QSharedPointer()); + callback(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); return INVALID_MESSAGE_ID; } -MessageID AssetClient::setAssetMapping(const QString& path, const AssetHash& hash, MappingOperationCallback callback) { +MessageID AssetClient::setAssetMapping(const QString& path, const AssetUtils::AssetHash& hash, MappingOperationCallback callback) { Q_ASSERT(QThread::currentThread() == thread()); auto nodeList = DependencyManager::get(); @@ -554,7 +725,7 @@ MessageID AssetClient::setAssetMapping(const QString& path, const AssetHash& has auto messageID = ++_currentID; packetList->writePrimitive(messageID); - packetList->writePrimitive(AssetMappingOperationType::Set); + packetList->writePrimitive(AssetUtils::AssetMappingOperationType::Set); packetList->writeString(path); packetList->write(QByteArray::fromHex(hash.toUtf8())); @@ -566,11 +737,11 @@ MessageID AssetClient::setAssetMapping(const QString& path, const AssetHash& has } } - callback(false, AssetServerError::NoError, QSharedPointer()); + callback(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); return INVALID_MESSAGE_ID; } -MessageID AssetClient::renameAssetMapping(const AssetPath& oldPath, const AssetPath& newPath, MappingOperationCallback callback) { +MessageID AssetClient::renameAssetMapping(const AssetUtils::AssetPath& oldPath, const AssetUtils::AssetPath& newPath, MappingOperationCallback callback) { auto nodeList = DependencyManager::get(); SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); @@ -580,7 +751,7 @@ MessageID AssetClient::renameAssetMapping(const AssetPath& oldPath, const AssetP auto messageID = ++_currentID; packetList->writePrimitive(messageID); - packetList->writePrimitive(AssetMappingOperationType::Rename); + packetList->writePrimitive(AssetUtils::AssetMappingOperationType::Rename); packetList->writeString(oldPath); packetList->writeString(newPath); @@ -593,11 +764,11 @@ MessageID AssetClient::renameAssetMapping(const AssetPath& oldPath, const AssetP } } - callback(false, AssetServerError::NoError, QSharedPointer()); + callback(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); return INVALID_MESSAGE_ID; } -MessageID AssetClient::setBakingEnabled(const AssetPathList& paths, bool enabled, MappingOperationCallback callback) { +MessageID AssetClient::setBakingEnabled(const AssetUtils::AssetPathList& paths, bool enabled, MappingOperationCallback callback) { auto nodeList = DependencyManager::get(); SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer); @@ -607,7 +778,7 @@ MessageID AssetClient::setBakingEnabled(const AssetPathList& paths, bool enabled auto messageID = ++_currentID; packetList->writePrimitive(messageID); - packetList->writePrimitive(AssetMappingOperationType::SetBakingEnabled); + packetList->writePrimitive(AssetUtils::AssetMappingOperationType::SetBakingEnabled); packetList->writePrimitive(enabled); @@ -624,7 +795,7 @@ MessageID AssetClient::setBakingEnabled(const AssetPathList& paths, bool enabled } } - callback(false, AssetServerError::NoError, QSharedPointer()); + callback(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); return INVALID_MESSAGE_ID; } @@ -708,7 +879,7 @@ MessageID AssetClient::uploadAsset(const QByteArray& data, UploadResultCallback } } - callback(false, AssetServerError::NoError, QString()); + callback(false, AssetUtils::AssetServerError::NoError, QString()); return INVALID_MESSAGE_ID; } @@ -718,7 +889,7 @@ void AssetClient::handleAssetUploadReply(QSharedPointer message MessageID messageID; message->readPrimitive(&messageID); - AssetServerError error; + AssetUtils::AssetServerError error; message->readPrimitive(&error); QString hashString; @@ -726,7 +897,7 @@ void AssetClient::handleAssetUploadReply(QSharedPointer message if (error) { qCWarning(asset_client) << "Error uploading file to asset server"; } else { - auto hash = message->read(SHA256_HASH_LENGTH); + auto hash = message->read(AssetUtils::SHA256_HASH_LENGTH); hashString = hash.toHex(); qCDebug(asset_client) << "Successfully uploaded asset to asset-server - SHA256 hash is " << hashString; @@ -765,7 +936,7 @@ void AssetClient::handleNodeKilled(SharedNodePointer node) { auto messageMapIt = _pendingUploads.find(node); if (messageMapIt != _pendingUploads.end()) { for (const auto& value : messageMapIt->second) { - value.second(false, AssetServerError::NoError, ""); + value.second(false, AssetUtils::AssetServerError::NoError, ""); } messageMapIt->second.clear(); } @@ -797,7 +968,7 @@ void AssetClient::forceFailureOfPendingRequests(SharedNodePointer node) { disconnect(message.data(), nullptr, this, nullptr); } - value.second.completeCallback(false, AssetServerError::NoError, QByteArray()); + value.second.completeCallback(false, AssetUtils::AssetServerError::NoError, QByteArray()); } messageMapIt->second.clear(); } @@ -808,7 +979,7 @@ void AssetClient::forceFailureOfPendingRequests(SharedNodePointer node) { if (messageMapIt != _pendingInfoRequests.end()) { AssetInfo info { "", 0 }; for (const auto& value : messageMapIt->second) { - value.second(false, AssetServerError::NoError, info); + value.second(false, AssetUtils::AssetServerError::NoError, info); } messageMapIt->second.clear(); } @@ -818,7 +989,7 @@ void AssetClient::forceFailureOfPendingRequests(SharedNodePointer node) { auto messageMapIt = _pendingMappingRequests.find(node); if (messageMapIt != _pendingMappingRequests.end()) { for (const auto& value : messageMapIt->second) { - value.second(false, AssetServerError::NoError, QSharedPointer()); + value.second(false, AssetUtils::AssetServerError::NoError, QSharedPointer()); } messageMapIt->second.clear(); } diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h index 8035aa886e..3ec96c3dd4 100644 --- a/libraries/networking/src/AssetClient.h +++ b/libraries/networking/src/AssetClient.h @@ -19,6 +19,7 @@ #include #include +#include #include "AssetUtils.h" #include "ByteRange.h" @@ -41,10 +42,10 @@ struct AssetInfo { int64_t size; }; -using MappingOperationCallback = std::function message)>; -using ReceivedAssetCallback = std::function; -using GetInfoCallback = std::function; -using UploadResultCallback = std::function; +using MappingOperationCallback = std::function message)>; +using ReceivedAssetCallback = std::function; +using GetInfoCallback = std::function; +using UploadResultCallback = std::function; using ProgressCallback = std::function; class AssetClient : public QObject, public Dependency { @@ -52,13 +53,13 @@ class AssetClient : public QObject, public Dependency { public: AssetClient(); - Q_INVOKABLE GetMappingRequest* createGetMappingRequest(const AssetPath& path); + Q_INVOKABLE GetMappingRequest* createGetMappingRequest(const AssetUtils::AssetPath& path); Q_INVOKABLE GetAllMappingsRequest* createGetAllMappingsRequest(); - Q_INVOKABLE DeleteMappingsRequest* createDeleteMappingsRequest(const AssetPathList& paths); - Q_INVOKABLE SetMappingRequest* createSetMappingRequest(const AssetPath& path, const AssetHash& hash); - Q_INVOKABLE RenameMappingRequest* createRenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath); - Q_INVOKABLE SetBakingEnabledRequest* createSetBakingEnabledRequest(const AssetPathList& path, bool enabled); - Q_INVOKABLE AssetRequest* createRequest(const AssetHash& hash, const ByteRange& byteRange = ByteRange()); + Q_INVOKABLE DeleteMappingsRequest* createDeleteMappingsRequest(const AssetUtils::AssetPathList& paths); + Q_INVOKABLE SetMappingRequest* createSetMappingRequest(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash); + Q_INVOKABLE RenameMappingRequest* createRenameMappingRequest(const AssetUtils::AssetPath& oldPath, const AssetUtils::AssetPath& newPath); + Q_INVOKABLE SetBakingEnabledRequest* createSetBakingEnabledRequest(const AssetUtils::AssetPathList& path, bool enabled); + Q_INVOKABLE AssetRequest* createRequest(const AssetUtils::AssetHash& hash, const ByteRange& byteRange = ByteRange()); Q_INVOKABLE AssetUpload* createUpload(const QString& filename); Q_INVOKABLE AssetUpload* createUpload(const QByteArray& data); @@ -66,6 +67,10 @@ public slots: void init(); void cacheInfoRequest(QObject* reciever, QString slot); + MiniPromise::Promise cacheInfoRequestAsync(MiniPromise::Promise deferred = nullptr); + MiniPromise::Promise queryCacheMetaAsync(const QUrl& url, MiniPromise::Promise deferred = nullptr); + MiniPromise::Promise loadFromCacheAsync(const QUrl& url, MiniPromise::Promise deferred = nullptr); + MiniPromise::Promise saveToCacheAsync(const QUrl& url, const QByteArray& data, const QVariantMap& metadata = QVariantMap(), MiniPromise::Promise deferred = nullptr); void clearCache(); private slots: @@ -78,15 +83,15 @@ private slots: void handleNodeClientConnectionReset(SharedNodePointer node); private: - MessageID getAssetMapping(const AssetHash& hash, MappingOperationCallback callback); + MessageID getAssetMapping(const AssetUtils::AssetHash& hash, MappingOperationCallback callback); MessageID getAllAssetMappings(MappingOperationCallback callback); - MessageID setAssetMapping(const QString& path, const AssetHash& hash, MappingOperationCallback callback); - MessageID deleteAssetMappings(const AssetPathList& paths, MappingOperationCallback callback); - MessageID renameAssetMapping(const AssetPath& oldPath, const AssetPath& newPath, MappingOperationCallback callback); - MessageID setBakingEnabled(const AssetPathList& paths, bool enabled, MappingOperationCallback callback); + MessageID setAssetMapping(const QString& path, const AssetUtils::AssetHash& hash, MappingOperationCallback callback); + MessageID deleteAssetMappings(const AssetUtils::AssetPathList& paths, MappingOperationCallback callback); + MessageID renameAssetMapping(const AssetUtils::AssetPath& oldPath, const AssetUtils::AssetPath& newPath, MappingOperationCallback callback); + MessageID setBakingEnabled(const AssetUtils::AssetPathList& paths, bool enabled, MappingOperationCallback callback); MessageID getAssetInfo(const QString& hash, GetInfoCallback callback); - MessageID getAsset(const QString& hash, DataOffset start, DataOffset end, + MessageID getAsset(const QString& hash, AssetUtils::DataOffset start, AssetUtils::DataOffset end, ReceivedAssetCallback callback, ProgressCallback progressCallback); MessageID uploadAsset(const QByteArray& data, UploadResultCallback callback); @@ -95,8 +100,8 @@ private: bool cancelGetAssetRequest(MessageID id); bool cancelUploadAssetRequest(MessageID id); - void handleProgressCallback(const QWeakPointer& node, MessageID messageID, qint64 size, DataOffset length); - void handleCompleteCallback(const QWeakPointer& node, MessageID messageID, DataOffset length); + void handleProgressCallback(const QWeakPointer& node, MessageID messageID, qint64 size, AssetUtils::DataOffset length); + void handleCompleteCallback(const QWeakPointer& node, MessageID messageID, AssetUtils::DataOffset length); void forceFailureOfPendingRequests(SharedNodePointer node); diff --git a/libraries/networking/src/AssetRequest.cpp b/libraries/networking/src/AssetRequest.cpp index 7fa563d4ad..c42e9aff14 100644 --- a/libraries/networking/src/AssetRequest.cpp +++ b/libraries/networking/src/AssetRequest.cpp @@ -52,7 +52,7 @@ void AssetRequest::start() { } // in case we haven't parsed a valid hash, return an error now - if (!isValidHash(_hash)) { + if (!AssetUtils::isValidHash(_hash)) { _error = InvalidHash; _state = Finished; @@ -61,7 +61,7 @@ void AssetRequest::start() { } // Try to load from cache - _data = loadFromCache(getUrl()); + _data = AssetUtils::loadFromCache(getUrl()); if (!_data.isNull()) { _error = NoError; @@ -80,7 +80,7 @@ void AssetRequest::start() { auto hash = _hash; _assetRequestID = assetClient->getAsset(_hash, _byteRange.fromInclusive, _byteRange.toExclusive, - [this, that, hash](bool responseReceived, AssetServerError serverError, const QByteArray& data) { + [this, that, hash](bool responseReceived, AssetUtils::AssetServerError serverError, const QByteArray& data) { if (!that) { qCWarning(asset_client) << "Got reply for dead asset request " << hash << "- error code" << _error; @@ -91,12 +91,12 @@ void AssetRequest::start() { if (!responseReceived) { _error = NetworkError; - } else if (serverError != AssetServerError::NoError) { + } else if (serverError != AssetUtils::AssetServerError::NoError) { switch (serverError) { - case AssetServerError::AssetNotFound: + case AssetUtils::AssetServerError::AssetNotFound: _error = NotFound; break; - case AssetServerError::InvalidByteRange: + case AssetUtils::AssetServerError::InvalidByteRange: _error = InvalidByteRange; break; default: @@ -104,7 +104,7 @@ void AssetRequest::start() { break; } } else { - if (!_byteRange.isSet() && hashData(data).toHex() != _hash) { + if (!_byteRange.isSet() && AssetUtils::hashData(data).toHex() != _hash) { // the hash of the received data does not match what we expect, so we return an error _error = HashVerificationFailed; } @@ -115,7 +115,7 @@ void AssetRequest::start() { emit progress(_totalReceived, data.size()); if (!_byteRange.isSet()) { - saveToCache(getUrl(), data); + AssetUtils::saveToCache(getUrl(), data); } } } @@ -134,3 +134,14 @@ void AssetRequest::start() { emit progress(totalReceived, total); }); } + + +const QString AssetRequest::getErrorString() const { + QString result; + if (_error != Error::NoError) { + QVariant v; + v.setValue(_error); + result = v.toString(); // courtesy of Q_ENUM + } + return result; +} diff --git a/libraries/networking/src/AssetRequest.h b/libraries/networking/src/AssetRequest.h index a7213a90d7..bf901c4e8f 100644 --- a/libraries/networking/src/AssetRequest.h +++ b/libraries/networking/src/AssetRequest.h @@ -42,7 +42,7 @@ public: NetworkError, UnknownError }; - + Q_ENUM(Error) AssetRequest(const QString& hash, const ByteRange& byteRange = ByteRange()); virtual ~AssetRequest() override; @@ -51,7 +51,8 @@ public: const QByteArray& getData() const { return _data; } const State& getState() const { return _state; } const Error& getError() const { return _error; } - QUrl getUrl() const { return ::getATPUrl(_hash); } + const QString getErrorString() const; + QUrl getUrl() const { return AssetUtils::getATPUrl(_hash); } QString getHash() const { return _hash; } bool loadedFromCache() const { return _loadedFromCache; } diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index 55d0ad4f75..6d5bbb3ac5 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -68,7 +68,7 @@ void AssetResourceRequest::doSend() { } } -void AssetResourceRequest::requestMappingForPath(const AssetPath& path) { +void AssetResourceRequest::requestMappingForPath(const AssetUtils::AssetPath& path) { auto statTracker = DependencyManager::get(); statTracker->incrementStat(STAT_ATP_MAPPING_REQUEST_STARTED); @@ -140,7 +140,7 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) { _assetMappingRequest->start(); } -void AssetResourceRequest::requestHash(const AssetHash& hash) { +void AssetResourceRequest::requestHash(const AssetUtils::AssetHash& hash) { // Make request to atp auto assetClient = DependencyManager::get(); _assetRequest = assetClient->createRequest(hash, _byteRange); diff --git a/libraries/networking/src/AssetResourceRequest.h b/libraries/networking/src/AssetResourceRequest.h index ac36c83985..2fe79040ca 100644 --- a/libraries/networking/src/AssetResourceRequest.h +++ b/libraries/networking/src/AssetResourceRequest.h @@ -34,8 +34,8 @@ private slots: private: static bool urlIsAssetHash(const QUrl& url); - void requestMappingForPath(const AssetPath& path); - void requestHash(const AssetHash& hash); + void requestMappingForPath(const AssetUtils::AssetPath& path); + void requestHash(const AssetUtils::AssetHash& hash); GetMappingRequest* _assetMappingRequest { nullptr }; AssetRequest* _assetRequest { nullptr }; diff --git a/libraries/networking/src/AssetUpload.cpp b/libraries/networking/src/AssetUpload.cpp index 077a0388cf..f1c84e474a 100644 --- a/libraries/networking/src/AssetUpload.cpp +++ b/libraries/networking/src/AssetUpload.cpp @@ -81,21 +81,21 @@ void AssetUpload::start() { qCDebug(asset_client) << "Attempting to upload" << _filename << "to asset-server."; } - assetClient->uploadAsset(_data, [this](bool responseReceived, AssetServerError error, const QString& hash){ + assetClient->uploadAsset(_data, [this](bool responseReceived, AssetUtils::AssetServerError error, const QString& hash){ if (!responseReceived) { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; - case AssetServerError::AssetTooLarge: + case AssetUtils::AssetServerError::AssetTooLarge: _error = TooLarge; break; - case AssetServerError::PermissionDenied: + case AssetUtils::AssetServerError::PermissionDenied: _error = PermissionDenied; break; - case AssetServerError::FileOperationFailed: + case AssetUtils::AssetServerError::FileOperationFailed: _error = ServerFileError; break; default: @@ -104,8 +104,8 @@ void AssetUpload::start() { } } - if (_error == NoError && hash == hashData(_data).toHex()) { - saveToCache(getATPUrl(hash), _data); + if (_error == NoError && hash == AssetUtils::hashData(_data).toHex()) { + AssetUtils::saveToCache(AssetUtils::getATPUrl(hash), _data); } emit finished(this, hash); diff --git a/libraries/networking/src/AssetUtils.cpp b/libraries/networking/src/AssetUtils.cpp index 76fda6aed4..117274eab8 100644 --- a/libraries/networking/src/AssetUtils.cpp +++ b/libraries/networking/src/AssetUtils.cpp @@ -15,6 +15,7 @@ #include #include +#include // for baseName #include #include "NetworkAccessManager.h" @@ -22,8 +23,38 @@ #include "ResourceManager.h" -QUrl getATPUrl(const QString& hash) { - return QUrl(QString("%1:%2").arg(URL_SCHEME_ATP, hash)); +namespace AssetUtils { + +// Extract the valid AssetHash portion from atp: URLs like "[atp:]HASH[.fbx][?query]" +// (or an invalid AssetHash if not found) +AssetHash extractAssetHash(const QString& input) { + if (isValidHash(input)) { + return input; + } + QString path = getATPUrl(input).path(); + QString baseName = QFileInfo(path).baseName(); + if (isValidHash(baseName)) { + return baseName; + } + return AssetHash(); +} + +// Get the normalized ATP URL for a raw hash, /path or "atp:" input string. +QUrl getATPUrl(const QString& input) { + QUrl url = input; + if (!url.scheme().isEmpty() && url.scheme() != URL_SCHEME_ATP) { + return QUrl(); + } + // this strips extraneous info from the URL (while preserving fragment/querystring) + QString path = url.toEncoded( + QUrl::RemoveAuthority | QUrl::RemoveScheme | + QUrl::StripTrailingSlash | QUrl::NormalizePathSegments + ); + QString baseName = QFileInfo(url.path()).baseName(); + if (isValidPath(path) || isValidHash(baseName)) { + return QUrl(QString("%1:%2").arg(URL_SCHEME_ATP).arg(path)); + } + return QUrl(); } QByteArray hashData(const QByteArray& data) { @@ -102,3 +133,5 @@ QString bakingStatusToString(BakingStatus status) { return "--"; } } + +} // namespace AssetUtils diff --git a/libraries/networking/src/AssetUtils.h b/libraries/networking/src/AssetUtils.h index a7c053c3d6..42e8dfad62 100644 --- a/libraries/networking/src/AssetUtils.h +++ b/libraries/networking/src/AssetUtils.h @@ -19,6 +19,8 @@ #include #include +namespace AssetUtils { + using DataOffset = int64_t; using AssetPath = QString; @@ -71,7 +73,8 @@ struct MappingInfo { using AssetMapping = std::map; -QUrl getATPUrl(const QString& hash); +QUrl getATPUrl(const QString& input); +AssetHash extractAssetHash(const QString& input); QByteArray hashData(const QByteArray& data); @@ -84,4 +87,6 @@ bool isValidHash(const QString& hashString); QString bakingStatusToString(BakingStatus status); +} // namespace AssetUtils + #endif // hifi_AssetUtils_h diff --git a/libraries/networking/src/BaseAssetScriptingInterface.cpp b/libraries/networking/src/BaseAssetScriptingInterface.cpp new file mode 100644 index 0000000000..3149bbc768 --- /dev/null +++ b/libraries/networking/src/BaseAssetScriptingInterface.cpp @@ -0,0 +1,365 @@ +// +// BaseAssetScriptingInterface.cpp +// libraries/networking/src +// +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#include "BaseAssetScriptingInterface.h" + +#include +#include +#include +#include + +#include "AssetRequest.h" +#include "AssetUpload.h" +#include "AssetUtils.h" +#include "MappingRequest.h" +#include "NetworkLogging.h" + +#include + +#include +#include "Gzip.h" + +using Promise = MiniPromise::Promise; + +QSharedPointer BaseAssetScriptingInterface::assetClient() { + auto client = DependencyManager::get(); + Q_ASSERT(client); + if (!client) { + qDebug() << "BaseAssetScriptingInterface::assetClient unavailable"; + } + return client; +} + +BaseAssetScriptingInterface::BaseAssetScriptingInterface(QObject* parent) : QObject(parent) {} + +bool BaseAssetScriptingInterface::initializeCache() { + if (!assetClient()) { + return false; // not yet possible to initialize the cache + } + if (_cacheReady) { + return true; // cache is ready + } + + // attempt to initialize the cache + QMetaObject::invokeMethod(assetClient().data(), "init"); + + Promise deferred = makePromise("BaseAssetScriptingInterface--queryCacheStatus"); + deferred->then([this](QVariantMap result) { + _cacheReady = !result.value("cacheDirectory").toString().isEmpty(); + }); + deferred->fail([](QString error) { + qDebug() << "BaseAssetScriptingInterface::queryCacheStatus ERROR" << QThread::currentThread() << error; + }); + assetClient()->cacheInfoRequestAsync(deferred); + return false; // cache is not ready yet +} + +Promise BaseAssetScriptingInterface::getCacheStatus() { + return assetClient()->cacheInfoRequestAsync(makePromise(__FUNCTION__)); +} + +Promise BaseAssetScriptingInterface::queryCacheMeta(const QUrl& url) { + return assetClient()->queryCacheMetaAsync(url, makePromise(__FUNCTION__)); +} + +Promise BaseAssetScriptingInterface::loadFromCache(const QUrl& url, bool decompress, const QString& responseType) { + QVariantMap metaData = { + { "_type", "cache" }, + { "url", url }, + { "responseType", responseType }, + }; + + Promise completed = makePromise("loadFromCache::completed"); + Promise fetched = makePromise("loadFromCache::fetched"); + + Promise downloaded = assetClient()->loadFromCacheAsync(url, makePromise("loadFromCache-retrieval")); + downloaded->mixin(metaData); + downloaded->fail(fetched); + + if (decompress) { + downloaded->then([=](QVariantMap result) { + fetched->mixin(result); + Promise decompressed = decompressBytes(result.value("data").toByteArray()); + decompressed->mixin(result); + decompressed->ready(fetched); + }); + } else { + downloaded->then(fetched); + } + + fetched->fail(completed); + fetched->then([=](QVariantMap result) { + Promise converted = convertBytes(result.value("data").toByteArray(), responseType); + converted->mixin(result); + converted->ready(completed); + }); + + return completed; +} + +Promise BaseAssetScriptingInterface::saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& headers) { + return assetClient()->saveToCacheAsync(url, data, headers, makePromise(__FUNCTION__)); +} + +Promise BaseAssetScriptingInterface::loadAsset(QString asset, bool decompress, QString responseType) { + auto hash = AssetUtils::extractAssetHash(asset); + auto url = AssetUtils::getATPUrl(hash).toString(); + + QVariantMap metaData = { + { "_asset", asset }, + { "_type", "download" }, + { "hash", hash }, + { "url", url }, + { "responseType", responseType }, + }; + + Promise completed = makePromise("loadAsset::completed"); + Promise fetched = makePromise("loadAsset::fetched"); + + Promise downloaded = downloadBytes(hash); + downloaded->mixin(metaData); + downloaded->fail(fetched); + + if (decompress) { + downloaded->then([=](QVariantMap result) { + Q_ASSERT(thread() == QThread::currentThread()); + fetched->mixin(result); + Promise decompressed = decompressBytes(result.value("data").toByteArray()); + decompressed->mixin(result); + decompressed->ready(fetched); + }); + } else { + downloaded->then(fetched); + } + + fetched->fail(completed); + fetched->then([=](QVariantMap result) { + Promise converted = convertBytes(result.value("data").toByteArray(), responseType); + converted->mixin(result); + converted->ready(completed); + }); + + return completed; +} + +Promise BaseAssetScriptingInterface::convertBytes(const QByteArray& dataByteArray, const QString& responseType) { + QVariantMap result = { + { "_contentType", QMimeDatabase().mimeTypeForData(dataByteArray).name() }, + { "_byteLength", dataByteArray.size() }, + { "_responseType", responseType }, + }; + QString error; + Promise conversion = makePromise(__FUNCTION__); + if (!RESPONSE_TYPES.contains(responseType)) { + error = QString("convertBytes: invalid responseType: '%1' (expected: %2)").arg(responseType).arg(RESPONSE_TYPES.join(" | ")); + } else if (responseType == "arraybuffer") { + // interpret as bytes + result["response"] = dataByteArray; + } else if (responseType == "text") { + // interpret as utf-8 text + result["response"] = QString::fromUtf8(dataByteArray); + } else if (responseType == "json") { + // interpret as JSON + QJsonParseError status; + auto parsed = QJsonDocument::fromJson(dataByteArray, &status); + if (status.error == QJsonParseError::NoError) { + result["response"] = parsed.isArray() ? QVariant(parsed.array().toVariantList()) : QVariant(parsed.object().toVariantMap()); + } else { + result = { + { "error", status.error }, + { "offset", status.offset }, + }; + error = "JSON Parse Error: " + status.errorString(); + } + } + if (result.value("response").canConvert()) { + auto data = result.value("response").toByteArray(); + result["contentType"] = QMimeDatabase().mimeTypeForData(data).name(); + result["byteLength"] = data.size(); + result["responseType"] = responseType; + } + return conversion->handle(error, result); +} + +Promise BaseAssetScriptingInterface::decompressBytes(const QByteArray& dataByteArray) { + QByteArray inflated; + Promise decompressed = makePromise(__FUNCTION__); + auto start = usecTimestampNow(); + if (gunzip(dataByteArray, inflated)) { + auto end = usecTimestampNow(); + decompressed->resolve({ + { "_compressedByteLength", dataByteArray.size() }, + { "_compressedContentType", QMimeDatabase().mimeTypeForData(dataByteArray).name() }, + { "_compressMS", (double)(end - start) / 1000.0 }, + { "decompressed", true }, + { "byteLength", inflated.size() }, + { "contentType", QMimeDatabase().mimeTypeForData(inflated).name() }, + { "data", inflated }, + }); + } else { + decompressed->reject("gunzip error"); + } + return decompressed; +} + +Promise BaseAssetScriptingInterface::compressBytes(const QByteArray& dataByteArray, int level) { + QByteArray deflated; + auto start = usecTimestampNow(); + Promise compressed = makePromise(__FUNCTION__); + if (gzip(dataByteArray, deflated, level)) { + auto end = usecTimestampNow(); + compressed->resolve({ + { "_uncompressedByteLength", dataByteArray.size() }, + { "_uncompressedContentType", QMimeDatabase().mimeTypeForData(dataByteArray).name() }, + { "_compressMS", (double)(end - start) / 1000.0 }, + { "compressed", true }, + { "byteLength", deflated.size() }, + { "contentType", QMimeDatabase().mimeTypeForData(deflated).name() }, + { "data", deflated }, + }); + } else { + compressed->reject("gzip error", {}); + } + return compressed; +} + +Promise BaseAssetScriptingInterface::downloadBytes(QString hash) { + QPointer assetRequest = assetClient()->createRequest(hash); + Promise deferred = makePromise(__FUNCTION__); + + QObject::connect(assetRequest, &AssetRequest::finished, assetRequest, [this, deferred](AssetRequest* request) { + // note: we are now on the "Resource Manager" thread + Q_ASSERT(QThread::currentThread() == request->thread()); + Q_ASSERT(request->getState() == AssetRequest::Finished); + QString error; + QVariantMap result; + if (request->getError() == AssetRequest::Error::NoError) { + QByteArray data = request->getData(); + result = { + { "url", request->getUrl() }, + { "hash", request->getHash() }, + { "cached", request->loadedFromCache() }, + { "contentType", QMimeDatabase().mimeTypeForData(data).name() }, + { "data", data }, + }; + } else { + error = request->getError(); + result = { { "error", request->getError() } }; + } + // forward thread-safe copies back to our thread + deferred->handle(error, result); + request->deleteLater(); + }); + assetRequest->start(); + return deferred; +} + +Promise BaseAssetScriptingInterface::uploadBytes(const QByteArray& bytes) { + Promise deferred = makePromise(__FUNCTION__); + QPointer upload = assetClient()->createUpload(bytes); + + const auto byteLength = bytes.size(); + QObject::connect(upload, &AssetUpload::finished, upload, [=](AssetUpload* upload, const QString& hash) { + Q_ASSERT(QThread::currentThread() == upload->thread()); + // note: we are now on the "Resource Manager" thread + QString error; + QVariantMap result; + if (upload->getError() == AssetUpload::NoError) { + result = { + { "hash", hash }, + { "url", AssetUtils::getATPUrl(hash).toString() }, + { "filename", upload->getFilename() }, + { "byteLength", byteLength }, + }; + } else { + error = upload->getErrorString(); + result = { { "error", upload->getError() } }; + } + // forward thread-safe copies back to our thread + deferred->handle(error, result); + upload->deleteLater(); + }); + upload->start(); + return deferred; +} + +Promise BaseAssetScriptingInterface::getAssetInfo(QString asset) { + Promise deferred = makePromise(__FUNCTION__); + auto url = AssetUtils::getATPUrl(asset); + auto path = url.path(); + auto hash = AssetUtils::extractAssetHash(asset); + if (AssetUtils::isValidHash(hash)) { + // already a valid ATP hash -- nothing to do + deferred->resolve({ + { "hash", hash }, + { "path", path }, + { "url", url }, + }); + } else if (AssetUtils::isValidFilePath(path)) { + QPointer request = assetClient()->createGetMappingRequest(path); + + QObject::connect(request, &GetMappingRequest::finished, request, [=]() { + Q_ASSERT(QThread::currentThread() == request->thread()); + // note: we are now on the "Resource Manager" thread + QString error; + QVariantMap result; + if (request->getError() == GetMappingRequest::NoError) { + result = { + { "_hash", hash }, + { "_path", path }, + { "_url", url }, + { "url", url }, + { "hash", request->getHash() }, + { "hashURL", AssetUtils::getATPUrl(request->getHash()).toString() }, + { "wasRedirected", request->wasRedirected() }, + { "path", request->wasRedirected() ? request->getRedirectedPath() : path }, + }; + } else { + error = request->getErrorString(); + result = { { "error", request->getError() } }; + } + // forward thread-safe copies back to our thread + deferred->handle(error, result); + request->deleteLater(); + }); + request->start(); + } else { + deferred->reject("invalid ATP file path: " + asset + "("+path+")", {}); + } + return deferred; +} + +Promise BaseAssetScriptingInterface::symlinkAsset(QString hash, QString path) { + auto deferred = makePromise(__FUNCTION__); + QPointer setMappingRequest = assetClient()->createSetMappingRequest(path, hash); + + connect(setMappingRequest, &SetMappingRequest::finished, setMappingRequest, [=](SetMappingRequest* request) { + Q_ASSERT(QThread::currentThread() == request->thread()); + // we are now on the "Resource Manager" thread + QString error; + QVariantMap result; + if (request->getError() == SetMappingRequest::NoError) { + result = { + { "_hash", hash }, + { "_path", path }, + { "hash", request->getHash() }, + { "path", request->getPath() }, + { "url", AssetUtils::getATPUrl(request->getPath()).toString() }, + }; + } else { + error = request->getErrorString(); + result = { { "error", request->getError() } }; + } + // forward results back to our thread + deferred->handle(error, result); + request->deleteLater(); + }); + setMappingRequest->start(); + return deferred; +} diff --git a/libraries/networking/src/BaseAssetScriptingInterface.h b/libraries/networking/src/BaseAssetScriptingInterface.h new file mode 100644 index 0000000000..336f3f81db --- /dev/null +++ b/libraries/networking/src/BaseAssetScriptingInterface.h @@ -0,0 +1,59 @@ +// +// BaseAssetScriptingInterface.h +// libraries/networking/src +// +// Copyright 2017 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 +// + +// BaseAssetScriptingInterface contains the engine-agnostic support code that can be used from +// both QML JS and QScriptEngine JS engine implementations + +#ifndef hifi_BaseAssetScriptingInterface_h +#define hifi_BaseAssetScriptingInterface_h + +#include +#include +#include "AssetClient.h" +#include +#include "NetworkAccessManager.h" +#include + +class BaseAssetScriptingInterface : public QObject { + Q_OBJECT +public: + const QStringList RESPONSE_TYPES{ "text", "arraybuffer", "json" }; + using Promise = MiniPromise::Promise; + QSharedPointer assetClient(); + + BaseAssetScriptingInterface(QObject* parent = nullptr); + +public slots: + bool isValidPath(QString input) { return AssetUtils::isValidPath(input); } + bool isValidFilePath(QString input) { return AssetUtils::isValidFilePath(input); } + QUrl getATPUrl(QString input) { return AssetUtils::getATPUrl(input); } + QString extractAssetHash(QString input) { return AssetUtils::extractAssetHash(input); } + bool isValidHash(QString input) { return AssetUtils::isValidHash(input); } + QByteArray hashData(const QByteArray& data) { return AssetUtils::hashData(data); } + QString hashDataHex(const QByteArray& data) { return hashData(data).toHex(); } + +protected: + bool _cacheReady{ false }; + bool initializeCache(); + Promise getCacheStatus(); + Promise queryCacheMeta(const QUrl& url); + Promise loadFromCache(const QUrl& url, bool decompress = false, const QString& responseType = "arraybuffer"); + Promise saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& metadata = QVariantMap()); + + Promise loadAsset(QString asset, bool decompress, QString responseType); + Promise getAssetInfo(QString asset); + Promise downloadBytes(QString hash); + Promise uploadBytes(const QByteArray& bytes); + Promise compressBytes(const QByteArray& bytes, int level = -1); + Promise convertBytes(const QByteArray& dataByteArray, const QString& responseType); + Promise decompressBytes(const QByteArray& bytes); + Promise symlinkAsset(QString hash, QString path); +}; +#endif // hifi_BaseAssetScriptingInterface_h diff --git a/libraries/networking/src/MappingRequest.cpp b/libraries/networking/src/MappingRequest.cpp index a79105e3ab..07639d4994 100644 --- a/libraries/networking/src/MappingRequest.cpp +++ b/libraries/networking/src/MappingRequest.cpp @@ -51,13 +51,13 @@ QString MappingRequest::getErrorString() const { } } -GetMappingRequest::GetMappingRequest(const AssetPath& path) : _path(path.trimmed()) { +GetMappingRequest::GetMappingRequest(const AssetUtils::AssetPath& path) : _path(path.trimmed()) { }; void GetMappingRequest::doStart() { // short circuit the request if the path is invalid - if (!isValidFilePath(_path)) { + if (!AssetUtils::isValidFilePath(_path)) { _error = MappingRequest::InvalidPath; emit finished(this); return; @@ -66,17 +66,17 @@ void GetMappingRequest::doStart() { auto assetClient = DependencyManager::get(); _mappingRequestID = assetClient->getAssetMapping(_path, - [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { + [this, assetClient](bool responseReceived, AssetUtils::AssetServerError error, QSharedPointer message) { _mappingRequestID = INVALID_MESSAGE_ID; if (!responseReceived) { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; - case AssetServerError::AssetNotFound: + case AssetUtils::AssetServerError::AssetNotFound: _error = NotFound; break; default: @@ -86,7 +86,7 @@ void GetMappingRequest::doStart() { } if (!_error) { - _hash = message->read(SHA256_HASH_LENGTH).toHex(); + _hash = message->read(AssetUtils::SHA256_HASH_LENGTH).toHex(); // check the boolean to see if this request got re-directed quint8 wasRedirected; @@ -112,7 +112,7 @@ GetAllMappingsRequest::GetAllMappingsRequest() { void GetAllMappingsRequest::doStart() { auto assetClient = DependencyManager::get(); _mappingRequestID = assetClient->getAllAssetMappings( - [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { + [this, assetClient](bool responseReceived, AssetUtils::AssetServerError error, QSharedPointer message) { _mappingRequestID = INVALID_MESSAGE_ID; @@ -120,7 +120,7 @@ void GetAllMappingsRequest::doStart() { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; default: @@ -135,11 +135,11 @@ void GetAllMappingsRequest::doStart() { message->readPrimitive(&numberOfMappings); for (uint32_t i = 0; i < numberOfMappings; ++i) { auto path = message->readString(); - auto hash = message->read(SHA256_HASH_LENGTH).toHex(); - BakingStatus status; + auto hash = message->read(AssetUtils::SHA256_HASH_LENGTH).toHex(); + AssetUtils::BakingStatus status; QString lastBakeErrors; message->readPrimitive(&status); - if (status == BakingStatus::Error) { + if (status == AssetUtils::BakingStatus::Error) { lastBakeErrors = message->readString(); } _mappings[path] = { hash, status, lastBakeErrors }; @@ -149,7 +149,7 @@ void GetAllMappingsRequest::doStart() { }); }; -SetMappingRequest::SetMappingRequest(const AssetPath& path, const AssetHash& hash) : +SetMappingRequest::SetMappingRequest(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash) : _path(path.trimmed()), _hash(hash) { @@ -159,8 +159,8 @@ SetMappingRequest::SetMappingRequest(const AssetPath& path, const AssetHash& has void SetMappingRequest::doStart() { // short circuit the request if the hash or path are invalid - auto validPath = isValidFilePath(_path); - auto validHash = isValidHash(_hash); + auto validPath = AssetUtils::isValidFilePath(_path); + auto validHash = AssetUtils::isValidHash(_hash); if (!validPath || !validHash) { _error = !validPath ? MappingRequest::InvalidPath : MappingRequest::InvalidHash; emit finished(this); @@ -170,17 +170,17 @@ void SetMappingRequest::doStart() { auto assetClient = DependencyManager::get(); _mappingRequestID = assetClient->setAssetMapping(_path, _hash, - [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { + [this, assetClient](bool responseReceived, AssetUtils::AssetServerError error, QSharedPointer message) { _mappingRequestID = INVALID_MESSAGE_ID; if (!responseReceived) { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; - case AssetServerError::PermissionDenied: + case AssetUtils::AssetServerError::PermissionDenied: _error = PermissionDenied; break; default: @@ -193,7 +193,7 @@ void SetMappingRequest::doStart() { }); }; -DeleteMappingsRequest::DeleteMappingsRequest(const AssetPathList& paths) : _paths(paths) { +DeleteMappingsRequest::DeleteMappingsRequest(const AssetUtils::AssetPathList& paths) : _paths(paths) { for (auto& path : _paths) { path = path.trimmed(); } @@ -203,7 +203,7 @@ void DeleteMappingsRequest::doStart() { // short circuit the request if any of the paths are invalid for (auto& path : _paths) { - if (!isValidPath(path)) { + if (!AssetUtils::isValidPath(path)) { _error = MappingRequest::InvalidPath; emit finished(this); return; @@ -213,17 +213,17 @@ void DeleteMappingsRequest::doStart() { auto assetClient = DependencyManager::get(); _mappingRequestID = assetClient->deleteAssetMappings(_paths, - [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { + [this, assetClient](bool responseReceived, AssetUtils::AssetServerError error, QSharedPointer message) { _mappingRequestID = INVALID_MESSAGE_ID; if (!responseReceived) { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; - case AssetServerError::PermissionDenied: + case AssetUtils::AssetServerError::PermissionDenied: _error = PermissionDenied; break; default: @@ -236,7 +236,7 @@ void DeleteMappingsRequest::doStart() { }); }; -RenameMappingRequest::RenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath) : +RenameMappingRequest::RenameMappingRequest(const AssetUtils::AssetPath& oldPath, const AssetUtils::AssetPath& newPath) : _oldPath(oldPath.trimmed()), _newPath(newPath.trimmed()) { @@ -246,7 +246,7 @@ RenameMappingRequest::RenameMappingRequest(const AssetPath& oldPath, const Asset void RenameMappingRequest::doStart() { // short circuit the request if either of the paths are invalid - if (!isValidFilePath(_oldPath) || !isValidFilePath(_newPath)) { + if (!AssetUtils::isValidFilePath(_oldPath) || !AssetUtils::isValidFilePath(_newPath)) { _error = InvalidPath; emit finished(this); return; @@ -255,17 +255,17 @@ void RenameMappingRequest::doStart() { auto assetClient = DependencyManager::get(); _mappingRequestID = assetClient->renameAssetMapping(_oldPath, _newPath, - [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { + [this, assetClient](bool responseReceived, AssetUtils::AssetServerError error, QSharedPointer message) { _mappingRequestID = INVALID_MESSAGE_ID; if (!responseReceived) { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; - case AssetServerError::PermissionDenied: + case AssetUtils::AssetServerError::PermissionDenied: _error = PermissionDenied; break; default: @@ -278,7 +278,7 @@ void RenameMappingRequest::doStart() { }); } -SetBakingEnabledRequest::SetBakingEnabledRequest(const AssetPathList& paths, bool enabled) : _paths(paths), _enabled(enabled) { +SetBakingEnabledRequest::SetBakingEnabledRequest(const AssetUtils::AssetPathList& paths, bool enabled) : _paths(paths), _enabled(enabled) { for (auto& path : _paths) { path = path.trimmed(); } @@ -288,7 +288,7 @@ void SetBakingEnabledRequest::doStart() { // short circuit the request if any of the paths are invalid for (auto& path : _paths) { - if (!isValidPath(path)) { + if (!AssetUtils::isValidPath(path)) { _error = MappingRequest::InvalidPath; emit finished(this); return; @@ -298,17 +298,17 @@ void SetBakingEnabledRequest::doStart() { auto assetClient = DependencyManager::get(); _mappingRequestID = assetClient->setBakingEnabled(_paths, _enabled, - [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer message) { + [this, assetClient](bool responseReceived, AssetUtils::AssetServerError error, QSharedPointer message) { _mappingRequestID = INVALID_MESSAGE_ID; if (!responseReceived) { _error = NetworkError; } else { switch (error) { - case AssetServerError::NoError: + case AssetUtils::AssetServerError::NoError: _error = NoError; break; - case AssetServerError::PermissionDenied: + case AssetUtils::AssetServerError::PermissionDenied: _error = PermissionDenied; break; default: @@ -319,4 +319,4 @@ void SetBakingEnabledRequest::doStart() { emit finished(this); }); -}; \ No newline at end of file +}; diff --git a/libraries/networking/src/MappingRequest.h b/libraries/networking/src/MappingRequest.h index fc43375469..5286849dd1 100644 --- a/libraries/networking/src/MappingRequest.h +++ b/libraries/networking/src/MappingRequest.h @@ -50,10 +50,10 @@ private: class GetMappingRequest : public MappingRequest { Q_OBJECT public: - GetMappingRequest(const AssetPath& path); + GetMappingRequest(const AssetUtils::AssetPath& path); - AssetHash getHash() const { return _hash; } - AssetPath getRedirectedPath() const { return _redirectedPath; } + AssetUtils::AssetHash getHash() const { return _hash; } + AssetUtils::AssetPath getRedirectedPath() const { return _redirectedPath; } bool wasRedirected() const { return _wasRedirected; } signals: @@ -62,21 +62,21 @@ signals: private: virtual void doStart() override; - AssetPath _path; - AssetHash _hash; + AssetUtils::AssetPath _path; + AssetUtils::AssetHash _hash; - AssetPath _redirectedPath; + AssetUtils::AssetPath _redirectedPath; bool _wasRedirected { false }; }; class SetMappingRequest : public MappingRequest { Q_OBJECT public: - SetMappingRequest(const AssetPath& path, const AssetHash& hash); + SetMappingRequest(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash); - AssetPath getPath() const { return _path; } - AssetHash getHash() const { return _hash; } + AssetUtils::AssetPath getPath() const { return _path; } + AssetUtils::AssetHash getHash() const { return _hash; } signals: void finished(SetMappingRequest* thisRequest); @@ -84,14 +84,14 @@ signals: private: virtual void doStart() override; - AssetPath _path; - AssetHash _hash; + AssetUtils::AssetPath _path; + AssetUtils::AssetHash _hash; }; class DeleteMappingsRequest : public MappingRequest { Q_OBJECT public: - DeleteMappingsRequest(const AssetPathList& path); + DeleteMappingsRequest(const AssetUtils::AssetPathList& path); signals: void finished(DeleteMappingsRequest* thisRequest); @@ -99,13 +99,13 @@ signals: private: virtual void doStart() override; - AssetPathList _paths; + AssetUtils::AssetPathList _paths; }; class RenameMappingRequest : public MappingRequest { Q_OBJECT public: - RenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath); + RenameMappingRequest(const AssetUtils::AssetPath& oldPath, const AssetUtils::AssetPath& newPath); signals: void finished(RenameMappingRequest* thisRequest); @@ -113,8 +113,8 @@ signals: private: virtual void doStart() override; - AssetPath _oldPath; - AssetPath _newPath; + AssetUtils::AssetPath _oldPath; + AssetUtils::AssetPath _newPath; }; class GetAllMappingsRequest : public MappingRequest { @@ -122,7 +122,7 @@ class GetAllMappingsRequest : public MappingRequest { public: GetAllMappingsRequest(); - AssetMapping getMappings() const { return _mappings; } + AssetUtils::AssetMapping getMappings() const { return _mappings; } signals: void finished(GetAllMappingsRequest* thisRequest); @@ -130,13 +130,13 @@ signals: private: virtual void doStart() override; - AssetMapping _mappings; + AssetUtils::AssetMapping _mappings; }; class SetBakingEnabledRequest : public MappingRequest { Q_OBJECT public: - SetBakingEnabledRequest(const AssetPathList& path, bool enabled); + SetBakingEnabledRequest(const AssetUtils::AssetPathList& path, bool enabled); signals: void finished(SetBakingEnabledRequest* thisRequest); @@ -144,7 +144,7 @@ signals: private: virtual void doStart() override; - AssetPathList _paths; + AssetUtils::AssetPathList _paths; bool _enabled; }; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 45c77a606d..b0763c0fb3 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -422,8 +422,8 @@ void Model::initJointStates() { } bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, - BoxFace& face, glm::vec3& surfaceNormal, - QString& extraInfo, bool pickAgainstTriangles, bool allowBackface) { + BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo, + bool pickAgainstTriangles, bool allowBackface) { bool intersectedSomething = false; @@ -454,6 +454,10 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g QMutexLocker locker(&_mutex); float bestDistance = std::numeric_limits::max(); + Triangle bestModelTriangle; + Triangle bestWorldTriangle; + int bestSubMeshIndex = 0; + int subMeshIndex = 0; const FBXGeometry& geometry = getFBXGeometry(); @@ -471,8 +475,8 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g for (auto& triangleSet : _modelSpaceMeshTriangleSets) { float triangleSetDistance = 0.0f; BoxFace triangleSetFace; - glm::vec3 triangleSetNormal; - if (triangleSet.findRayIntersection(meshFrameOrigin, meshFrameDirection, triangleSetDistance, triangleSetFace, triangleSetNormal, pickAgainstTriangles, allowBackface)) { + Triangle triangleSetTriangle; + if (triangleSet.findRayIntersection(meshFrameOrigin, meshFrameDirection, triangleSetDistance, triangleSetFace, triangleSetTriangle, pickAgainstTriangles, allowBackface)) { glm::vec3 meshIntersectionPoint = meshFrameOrigin + (meshFrameDirection * triangleSetDistance); glm::vec3 worldIntersectionPoint = glm::vec3(meshToWorldMatrix * glm::vec4(meshIntersectionPoint, 1.0f)); @@ -482,8 +486,11 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g bestDistance = worldDistance; intersectedSomething = true; face = triangleSetFace; - surfaceNormal = glm::vec3(meshToWorldMatrix * glm::vec4(triangleSetNormal, 0.0f)); - extraInfo = geometry.getModelNameOfMesh(subMeshIndex); + bestModelTriangle = triangleSetTriangle; + bestWorldTriangle = triangleSetTriangle * meshToWorldMatrix; + extraInfo["worldIntersectionPoint"] = vec3toVariant(worldIntersectionPoint); + extraInfo["meshIntersectionPoint"] = vec3toVariant(meshIntersectionPoint); + bestSubMeshIndex = subMeshIndex; } } subMeshIndex++; @@ -491,9 +498,24 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g if (intersectedSomething) { distance = bestDistance; + surfaceNormal = bestWorldTriangle.getNormal(); + if (pickAgainstTriangles) { + extraInfo["subMeshIndex"] = bestSubMeshIndex; + extraInfo["subMeshName"] = geometry.getModelNameOfMesh(bestSubMeshIndex); + extraInfo["subMeshTriangleWorld"] = QVariantMap{ + { "v0", vec3toVariant(bestWorldTriangle.v0) }, + { "v1", vec3toVariant(bestWorldTriangle.v1) }, + { "v2", vec3toVariant(bestWorldTriangle.v2) }, + }; + extraInfo["subMeshNormal"] = vec3toVariant(bestModelTriangle.getNormal()); + extraInfo["subMeshTriangle"] = QVariantMap{ + { "v0", vec3toVariant(bestModelTriangle.v0) }, + { "v1", vec3toVariant(bestModelTriangle.v1) }, + { "v2", vec3toVariant(bestModelTriangle.v2) }, + }; + } + } - - return intersectedSomething; } return intersectedSomething; diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 560aa82f0c..27eaedec40 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -161,8 +161,8 @@ public: void setJointTranslation(int index, bool valid, const glm::vec3& translation, float priority); bool findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const glm::vec3& direction, float& distance, - BoxFace& face, glm::vec3& surfaceNormal, - QString& extraInfo, bool pickAgainstTriangles = false, bool allowBackface = false); + BoxFace& face, glm::vec3& surfaceNormal, + QVariantMap& extraInfo, bool pickAgainstTriangles = false, bool allowBackface = false); void setOffset(const glm::vec3& offset); const glm::vec3& getOffset() const { return _offset; } diff --git a/libraries/script-engine/src/ArrayBufferClass.cpp b/libraries/script-engine/src/ArrayBufferClass.cpp index b84188f707..4a06dee391 100644 --- a/libraries/script-engine/src/ArrayBufferClass.cpp +++ b/libraries/script-engine/src/ArrayBufferClass.cpp @@ -18,14 +18,19 @@ #include "ArrayBufferClass.h" + static const QString CLASS_NAME = "ArrayBuffer"; +// FIXME: Q_DECLARE_METATYPE is global and really belongs in a shared header file, not per .cpp like this +// (see DataViewClass.cpp, etc. which would also have to be updated to resolve) +Q_DECLARE_METATYPE(QScriptClass*) Q_DECLARE_METATYPE(QByteArray*) +int qScriptClassPointerMetaTypeId = qRegisterMetaType(); +int qByteArrayPointerMetaTypeId = qRegisterMetaType(); ArrayBufferClass::ArrayBufferClass(ScriptEngine* scriptEngine) : QObject(scriptEngine), -QScriptClass(scriptEngine), -_scriptEngine(scriptEngine) { +QScriptClass(scriptEngine) { qScriptRegisterMetaType(engine(), toScriptValue, fromScriptValue); QScriptValue global = engine()->globalObject(); @@ -144,12 +149,30 @@ QScriptValue ArrayBufferClass::toScriptValue(QScriptEngine* engine, const QByteA QScriptValue ctor = engine->globalObject().property(CLASS_NAME); ArrayBufferClass* cls = qscriptvalue_cast(ctor.data()); if (!cls) { - return engine->newVariant(QVariant::fromValue(ba)); + if (engine->currentContext()) { + engine->currentContext()->throwError("arrayBufferClass::toScriptValue -- could not get " + CLASS_NAME + " class constructor"); + } + return QScriptValue::NullValue; } return cls->newInstance(ba); } -void ArrayBufferClass::fromScriptValue(const QScriptValue& obj, QByteArray& ba) { - ba = qvariant_cast(obj.data().toVariant()); +void ArrayBufferClass::fromScriptValue(const QScriptValue& object, QByteArray& byteArray) { + if (object.isString()) { + // UTF-8 encoded String + byteArray = object.toString().toUtf8(); + } else if (object.isArray()) { + // Array of uint8s eg: [ 128, 3, 25, 234 ] + auto Uint8Array = object.engine()->globalObject().property("Uint8Array"); + auto typedArray = Uint8Array.construct(QScriptValueList{object}); + if (QByteArray* buffer = qscriptvalue_cast(typedArray.property("buffer"))) { + byteArray = *buffer; + } + } else if (object.isObject()) { + // ArrayBuffer instance (or any JS class that supports coercion into QByteArray*) + if (QByteArray* buffer = qscriptvalue_cast(object.data())) { + byteArray = *buffer; + } + } } diff --git a/libraries/script-engine/src/ArrayBufferClass.h b/libraries/script-engine/src/ArrayBufferClass.h index 166a21b773..295b9c3fa4 100644 --- a/libraries/script-engine/src/ArrayBufferClass.h +++ b/libraries/script-engine/src/ArrayBufferClass.h @@ -41,7 +41,6 @@ public: QString name() const override; QScriptValue prototype() const override; - ScriptEngine* getEngine() { return _scriptEngine; } private: static QScriptValue construct(QScriptContext* context, QScriptEngine* engine); @@ -56,7 +55,6 @@ private: QScriptString _name; QScriptString _byteLength; - ScriptEngine* _scriptEngine; }; #endif // hifi_ArrayBufferClass_h diff --git a/libraries/script-engine/src/AssetScriptingInterface.cpp b/libraries/script-engine/src/AssetScriptingInterface.cpp index e0e04a1e25..1c811573fb 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.cpp +++ b/libraries/script-engine/src/AssetScriptingInterface.cpp @@ -11,102 +11,113 @@ #include "AssetScriptingInterface.h" +#include +#include #include #include #include +#include +#include #include #include +#include +#include "ScriptEngine.h" #include "ScriptEngineLogging.h" +#include +#include -AssetScriptingInterface::AssetScriptingInterface(QScriptEngine* engine) : - _engine(engine) -{ +using Promise = MiniPromise::Promise; + +AssetScriptingInterface::AssetScriptingInterface(QObject* parent) : BaseAssetScriptingInterface(parent) { + qCDebug(scriptengine) << "AssetScriptingInterface::AssetScriptingInterface" << parent; + MiniPromise::registerMetaTypes(parent); } +#define JS_VERIFY(cond, error) { if (!this->jsVerify(cond, error)) { return; } } + void AssetScriptingInterface::uploadData(QString data, QScriptValue callback) { + auto handler = jsBindCallback(thisObject(), callback); QByteArray dataByteArray = data.toUtf8(); auto upload = DependencyManager::get()->createUpload(dataByteArray); - QObject::connect(upload, &AssetUpload::finished, this, [this, callback](AssetUpload* upload, const QString& hash) mutable { - if (callback.isFunction()) { - QString url = "atp:" + hash; - QScriptValueList args { url, hash }; - callback.call(_engine->currentContext()->thisObject(), args); - } + Promise deferred = makePromise(__FUNCTION__); + deferred->ready([=](QString error, QVariantMap result) { + auto url = result.value("url").toString(); + auto hash = result.value("hash").toString(); + jsCallback(handler, url, hash); + }); + + connect(upload, &AssetUpload::finished, upload, [this, deferred](AssetUpload* upload, const QString& hash) { + // we are now on the "Resource Manager" thread (and "hash" being a *reference* makes it unsafe to use directly) + Q_ASSERT(QThread::currentThread() == upload->thread()); + deferred->resolve({ + { "url", "atp:" + hash }, + { "hash", hash }, + }); upload->deleteLater(); }); upload->start(); } void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValue callback) { - auto setMappingRequest = DependencyManager::get()->createSetMappingRequest(path, hash); + auto handler = jsBindCallback(thisObject(), callback); + auto setMappingRequest = assetClient()->createSetMappingRequest(path, hash); + Promise deferred = makePromise(__FUNCTION__); + deferred->ready([=](QString error, QVariantMap result) { + jsCallback(handler, error, result); + }); - QObject::connect(setMappingRequest, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable { - if (callback.isFunction()) { - QString error = request->getErrorString(); - QScriptValueList args { error }; - callback.call(_engine->currentContext()->thisObject(), args); - } + connect(setMappingRequest, &SetMappingRequest::finished, setMappingRequest, [this, deferred](SetMappingRequest* request) { + Q_ASSERT(QThread::currentThread() == request->thread()); + // we are now on the "Resource Manager" thread + QString error = request->getErrorString(); + // forward a thread-safe values back to our thread + deferred->handle(error, { { "error", request->getError() } }); request->deleteLater(); }); setMappingRequest->start(); } -void AssetScriptingInterface::getMapping(QString path, QScriptValue callback) { - auto request = DependencyManager::get()->createGetMappingRequest(path); - QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { - auto result = request->getError(); - if (callback.isFunction()) { - if (result == GetMappingRequest::NotFound) { - QScriptValueList args { "", true }; - callback.call(_engine->currentContext()->thisObject(), args); - } else if (result == GetMappingRequest::NoError) { - QScriptValueList args { request->getHash(), true }; - callback.call(_engine->currentContext()->thisObject(), args); - } else { - qCDebug(scriptengine) << "error -- " << request->getError() << " -- " << request->getErrorString(); - QScriptValueList args { "", false }; - callback.call(_engine->currentContext()->thisObject(), args); - } - request->deleteLater(); - } - }); - request->start(); -} - void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) { + // FIXME: historically this API method failed silently when given a non-atp prefixed + // urlString (or if the AssetRequest failed). + // .. is that by design or could we update without breaking things to provide better feedback to scripts? if (!urlString.startsWith(ATP_SCHEME)) { + // ... for now at least log a message so user can check logs qCDebug(scriptengine) << "AssetScriptingInterface::downloadData url must be of form atp:"; return; } - - // Make request to atp - auto path = urlString.right(urlString.length() - ATP_SCHEME.length()); - auto parts = path.split(".", QString::SkipEmptyParts); - auto hash = parts.length() > 0 ? parts[0] : ""; - + QString hash = AssetUtils::extractAssetHash(urlString); + auto handler = jsBindCallback(thisObject(), callback); auto assetClient = DependencyManager::get(); auto assetRequest = assetClient->createRequest(hash); - _pendingRequests << assetRequest; + Promise deferred = makePromise(__FUNCTION__); + deferred->ready([=](QString error, QVariantMap result) { + // FIXME: to remain backwards-compatible the signature here is "callback(data, n/a)" + jsCallback(handler, result.value("data").toString(), { { "errorMessage", error } }); + }); - connect(assetRequest, &AssetRequest::finished, this, [this, callback](AssetRequest* request) mutable { + connect(assetRequest, &AssetRequest::finished, assetRequest, [this, deferred](AssetRequest* request) { + Q_ASSERT(QThread::currentThread() == request->thread()); + // we are now on the "Resource Manager" thread Q_ASSERT(request->getState() == AssetRequest::Finished); if (request->getError() == AssetRequest::Error::NoError) { - if (callback.isFunction()) { - QString data = QString::fromUtf8(request->getData()); - QScriptValueList args { data }; - callback.call(_engine->currentContext()->thisObject(), args); - } + QString data = QString::fromUtf8(request->getData()); + // forward a thread-safe values back to our thread + deferred->resolve({ { "data", data } }); + } else { + // FIXME: propagate error to scripts? (requires changing signature or inverting param order above..) + //deferred->resolve(request->getErrorString(), { { "error", requet->getError() } }); + qCDebug(scriptengine) << "AssetScriptingInterface::downloadData ERROR: " << request->getErrorString(); } request->deleteLater(); - _pendingRequests.remove(request); }); assetRequest->start(); @@ -115,12 +126,15 @@ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callb void AssetScriptingInterface::setBakingEnabled(QString path, bool enabled, QScriptValue callback) { auto setBakingEnabledRequest = DependencyManager::get()->createSetBakingEnabledRequest({ path }, enabled); - QObject::connect(setBakingEnabledRequest, &SetBakingEnabledRequest::finished, this, [this, callback](SetBakingEnabledRequest* request) mutable { - if (callback.isFunction()) { - QString error = request->getErrorString(); - QScriptValueList args{ error }; - callback.call(_engine->currentContext()->thisObject(), args); - } + Promise deferred = jsPromiseReady(makePromise(__FUNCTION__), thisObject(), callback); + + connect(setBakingEnabledRequest, &SetBakingEnabledRequest::finished, setBakingEnabledRequest, [this, deferred](SetBakingEnabledRequest* request) { + Q_ASSERT(QThread::currentThread() == request->thread()); + // we are now on the "Resource Manager" thread + + QString error = request->getErrorString(); + // forward thread-safe values back to our thread + deferred->handle(error, {}); request->deleteLater(); }); setBakingEnabledRequest->start(); @@ -135,3 +149,298 @@ void AssetScriptingInterface::sendFakedHandshake() { } #endif + +void AssetScriptingInterface::getMapping(QString asset, QScriptValue callback) { + auto path = AssetUtils::getATPUrl(asset).path(); + auto handler = jsBindCallback(thisObject(), callback); + JS_VERIFY(AssetUtils::isValidFilePath(path), "invalid ATP file path: " + asset + "(path:"+path+")"); + JS_VERIFY(callback.isFunction(), "expected second parameter to be a callback function"); + Promise promise = getAssetInfo(path); + promise->ready([=](QString error, QVariantMap result) { + jsCallback(handler, error, result.value("hash").toString()); + }); +} + +bool AssetScriptingInterface::jsVerify(bool condition, const QString& error) { + if (condition) { + return true; + } + if (context()) { + context()->throwError(error); + } else { + qCDebug(scriptengine) << "WARNING -- jsVerify failed outside of a valid JS context: " + error; + } + return false; +} + +QScriptValue AssetScriptingInterface::jsBindCallback(QScriptValue scope, QScriptValue callback) { + QScriptValue handler = ::makeScopedHandlerObject(scope, callback); + QScriptValue value = handler.property("callback"); + if (!jsVerify(handler.isObject() && value.isFunction(), + QString("jsBindCallback -- .callback is not a function (%1)").arg(value.toVariant().typeName()))) { + return QScriptValue(); + } + return handler; +} + +Promise AssetScriptingInterface::jsPromiseReady(Promise promise, QScriptValue scope, QScriptValue callback) { + auto handler = jsBindCallback(scope, callback); + if (!jsVerify(handler.isValid(), "jsPromiseReady -- invalid callback handler")) { + return nullptr; + } + return promise->ready([this, handler](QString error, QVariantMap result) { + jsCallback(handler, error, result); + }); +} + +void AssetScriptingInterface::jsCallback(const QScriptValue& handler, + const QScriptValue& error, const QScriptValue& result) { + Q_ASSERT(thread() == QThread::currentThread()); + auto errorValue = !error.toBool() ? QScriptValue::NullValue : error; + JS_VERIFY(handler.isObject() && handler.property("callback").isFunction(), + QString("jsCallback -- .callback is not a function (%1)") + .arg(handler.property("callback").toVariant().typeName())); + ::callScopedHandlerObject(handler, errorValue, result); +} + +void AssetScriptingInterface::jsCallback(const QScriptValue& handler, + const QScriptValue& error, const QVariantMap& result) { + Q_ASSERT(thread() == QThread::currentThread()); + Q_ASSERT(handler.engine()); + auto engine = handler.engine(); + jsCallback(handler, error, engine->toScriptValue(result)); +} + +void AssetScriptingInterface::deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { + jsVerify(false, "TODO: deleteAsset API"); +} + +/**jsdoc + * @typedef {string} Assets.GetOptions.ResponseType + *

Available responseType values for use with @{link Assets.getAsset} and @{link Assets.loadFromCache} configuration option.

+ * + * + * + * + * + * + * + * + * + *
responseTypetypeof response value
"text"contents returned as utf-8 decoded String value
"arraybuffer"contents as a binary ArrayBuffer object
"json"contents as a parsed JSON object
+ */ + +void AssetScriptingInterface::getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { + JS_VERIFY(options.isObject() || options.isString(), "expected request options Object or URL as first parameter"); + + auto decompress = options.property("decompress").toBool() || options.property("compressed").toBool(); + auto responseType = options.property("responseType").toString().toLower(); + auto url = options.property("url").toString(); + if (options.isString()) { + url = options.toString(); + } + if (responseType.isEmpty()) { + responseType = "text"; + } + auto asset = AssetUtils::getATPUrl(url).path(); + JS_VERIFY(AssetUtils::isValidHash(asset) || AssetUtils::isValidFilePath(asset), + QString("Invalid ATP url '%1'").arg(url)); + JS_VERIFY(RESPONSE_TYPES.contains(responseType), + QString("Invalid responseType: '%1' (expected: %2)").arg(responseType).arg(RESPONSE_TYPES.join(" | "))); + + Promise fetched = jsPromiseReady(makePromise("fetched"), scope, callback); + Promise mapped = makePromise("mapped"); + + mapped->fail(fetched); + mapped->then([=](QVariantMap result) { + QString hash = result.value("hash").toString(); + QString url = result.value("url").toString(); + if (!AssetUtils::isValidHash(hash)) { + fetched->reject("internal hash error: " + hash, result); + } else { + Promise promise = loadAsset(hash, decompress, responseType); + promise->mixin(result); + promise->ready([=](QString error, QVariantMap loadResult) { + loadResult["url"] = url; // maintain mapped .url in results (vs. atp:hash returned by loadAsset) + fetched->handle(error, loadResult); + }); + } + }); + + if (AssetUtils::isValidHash(asset)) { + mapped->resolve({ + { "hash", asset }, + { "url", url }, + }); + } else { + getAssetInfo(asset)->ready(mapped); + } +} + +void AssetScriptingInterface::resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { + const QString& URL{ "url" }; + + auto url = (options.isString() ? options : options.property(URL)).toString(); + auto asset = AssetUtils::getATPUrl(url).path(); + + JS_VERIFY(AssetUtils::isValidFilePath(asset) || AssetUtils::isValidHash(asset), + "expected options to be an asset URL or request options containing .url property"); + + jsPromiseReady(getAssetInfo(asset), scope, callback); +} + +void AssetScriptingInterface::decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback) { + auto data = options.property("data"); + QByteArray dataByteArray = qscriptvalue_cast(data); + auto responseType = options.property("responseType").toString().toLower(); + if (responseType.isEmpty()) { + responseType = "text"; + } + Promise completed = jsPromiseReady(makePromise(__FUNCTION__), scope, callback); + Promise decompressed = decompressBytes(dataByteArray); + if (responseType == "arraybuffer") { + decompressed->ready(completed); + } else { + decompressed->ready([=](QString error, QVariantMap result) { + Promise converted = convertBytes(result.value("data").toByteArray(), responseType); + converted->mixin(result); + converted->ready(completed); + }); + } +} + +namespace { + const int32_t DEFAULT_GZIP_COMPRESSION_LEVEL = -1; + const int32_t MAX_GZIP_COMPRESSION_LEVEL = 9; +} +void AssetScriptingInterface::compressData(QScriptValue options, QScriptValue scope, QScriptValue callback) { + auto data = options.property("data").isValid() ? options.property("data") : options; + QByteArray dataByteArray = data.isString() ? data.toString().toUtf8() : qscriptvalue_cast(data); + int level = options.property("level").isNumber() ? options.property("level").toInt32() : DEFAULT_GZIP_COMPRESSION_LEVEL; + JS_VERIFY(level >= DEFAULT_GZIP_COMPRESSION_LEVEL || level <= MAX_GZIP_COMPRESSION_LEVEL, QString("invalid .level %1").arg(level)); + jsPromiseReady(compressBytes(dataByteArray, level), scope, callback); +} + +void AssetScriptingInterface::putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { + auto compress = options.property("compress").toBool() || options.property("compressed").toBool(); + auto data = options.isObject() ? options.property("data") : options; + auto rawPath = options.property("path").toString(); + auto path = AssetUtils::getATPUrl(rawPath).path(); + + QByteArray dataByteArray = data.isString() ? data.toString().toUtf8() : qscriptvalue_cast(data); + + JS_VERIFY(path.isEmpty() || AssetUtils::isValidFilePath(path), + QString("expected valid ATP file path '%1' ('%2')").arg(rawPath).arg(path)); + JS_VERIFY(dataByteArray.size() > 0, + QString("expected non-zero .data (got %1 / #%2 bytes)").arg(data.toVariant().typeName()).arg(dataByteArray.size())); + + // [compressed] => uploaded to server => [mapped to path] + Promise prepared = makePromise("putAsset::prepared"); + Promise uploaded = makePromise("putAsset::uploaded"); + Promise completed = makePromise("putAsset::completed"); + jsPromiseReady(completed, scope, callback); + + if (compress) { + Promise compress = compressBytes(dataByteArray, DEFAULT_GZIP_COMPRESSION_LEVEL); + compress->ready(prepared); + } else { + prepared->resolve({{ "data", dataByteArray }}); + } + + prepared->fail(completed); + prepared->then([=](QVariantMap result) { + Promise upload = uploadBytes(result.value("data").toByteArray()); + upload->mixin(result); + upload->ready(uploaded); + }); + + uploaded->fail(completed); + if (path.isEmpty()) { + uploaded->then(completed); + } else { + uploaded->then([=](QVariantMap result) { + QString hash = result.value("hash").toString(); + if (!AssetUtils::isValidHash(hash)) { + completed->reject("path mapping requested, but did not receive valid hash", result); + } else { + Promise link = symlinkAsset(hash, path); + link->mixin(result); + link->ready(completed); + } + }); + } +} + +void AssetScriptingInterface::queryCacheMeta(QScriptValue options, QScriptValue scope, QScriptValue callback) { + QString url = options.isString() ? options.toString() : options.property("url").toString(); + JS_VERIFY(QUrl(url).isValid(), QString("Invalid URL '%1'").arg(url)); + jsPromiseReady(Parent::queryCacheMeta(url), scope, callback); +} + +void AssetScriptingInterface::loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback) { + QString url, responseType; + bool decompress = false; + if (options.isString()) { + url = options.toString(); + responseType = "text"; + } else { + url = options.property("url").toString(); + responseType = options.property("responseType").isValid() ? options.property("responseType").toString() : "text"; + decompress = options.property("decompress").toBool() || options.property("compressed").toBool(); + } + JS_VERIFY(QUrl(url).isValid(), QString("Invalid URL '%1'").arg(url)); + JS_VERIFY(RESPONSE_TYPES.contains(responseType), + QString("Invalid responseType: '%1' (expected: %2)").arg(responseType).arg(RESPONSE_TYPES.join(" | "))); + + jsPromiseReady(Parent::loadFromCache(url, decompress, responseType), scope, callback); +} + +bool AssetScriptingInterface::canWriteCacheValue(const QUrl& url) { + auto scriptEngine = qobject_cast(engine()); + if (!scriptEngine) { + qCDebug(scriptengine) << __FUNCTION__ << "invalid script engine" << url; + return false; + } + // allow cache writes only from Client, EntityServer and Agent scripts + bool isAllowedContext = ( + scriptEngine->isClientScript() || + scriptEngine->isAgentScript() + ); + if (!isAllowedContext) { + qCDebug(scriptengine) << __FUNCTION__ << "invalid context" << scriptEngine->getContext() << url; + return false; + } + return true; +} + +void AssetScriptingInterface::saveToCache(QScriptValue options, QScriptValue scope, QScriptValue callback) { + JS_VERIFY(options.isObject(), QString("expected options object as first parameter not: %1").arg(options.toVariant().typeName())); + + QString url = options.property("url").toString(); + QByteArray data = qscriptvalue_cast(options.property("data")); + QVariantMap headers = qscriptvalue_cast(options.property("headers")); + + saveToCache(url, data, headers, scope, callback); +} + +void AssetScriptingInterface::saveToCache(const QUrl& rawURL, const QByteArray& data, const QVariantMap& metadata, QScriptValue scope, QScriptValue callback) { + QUrl url = rawURL; + if (url.path().isEmpty() && !data.isEmpty()) { + // generate a valid ATP URL from the data -- appending any existing fragment or querystring values + auto atpURL = AssetUtils::getATPUrl(hashDataHex(data)); + atpURL.setQuery(url.query()); + atpURL.setFragment(url.fragment()); + qCDebug(scriptengine) << "autogenerated ATP URL" << url << "=>" << atpURL; + url = atpURL; + } + auto hash = AssetUtils::extractAssetHash(url.toDisplayString()); + + JS_VERIFY(url.isValid(), QString("Invalid URL '%1'").arg(url.toString())); + JS_VERIFY(canWriteCacheValue(url), "Invalid cache write URL: " + url.toString()); + JS_VERIFY(url.scheme() == "atp" || url.scheme() == "cache", "only 'atp' and 'cache' URL schemes supported"); + JS_VERIFY(hash.isEmpty() || hash == hashDataHex(data), QString("invalid checksum hash for atp:HASH style URL (%1 != %2)").arg(hash, hashDataHex(data))); + + qCDebug(scriptengine) << "saveToCache" << url.toDisplayString() << data << hash << metadata; + + jsPromiseReady(Parent::saveToCache(url, data, metadata), scope, callback); +} diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index dded2ef21d..fdce173dfe 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -15,17 +15,23 @@ #define hifi_AssetScriptingInterface_h #include +#include #include - +#include #include +#include +#include +#include +#include /**jsdoc * @namespace Assets */ -class AssetScriptingInterface : public QObject { +class AssetScriptingInterface : public BaseAssetScriptingInterface, QScriptable { Q_OBJECT public: - AssetScriptingInterface(QScriptEngine* engine); + using Parent = BaseAssetScriptingInterface; + AssetScriptingInterface(QObject* parent = nullptr); /**jsdoc * Upload content to the connected domain's asset server. @@ -87,21 +93,105 @@ public: /**jsdoc * Called when getMapping is complete. * @callback Assets~getMappingCallback + * @param error {string} error description if the path could not be resolved; otherwise a null value. * @param assetID {string} hash value if found, else an empty string - * @param success {boolean} false for errors other than "not found", else true */ Q_INVOKABLE void getMapping(QString path, QScriptValue callback); - Q_INVOKABLE void setBakingEnabled(QString path, bool enabled, QScriptValue callback); #if (PR_BUILD || DEV_BUILD) Q_INVOKABLE void sendFakedHandshake(); #endif + /**jsdoc + * Request Asset data from the ATP Server + * @function Assets.getAsset + * @param {URL|Assets.GetOptions} options An atp: style URL, hash, or relative mapped path; or an {@link Assets.GetOptions} object with request parameters + * @param {Assets~getAssetCallback} scope[callback] A scope callback function to receive (error, results) values + */ + /**jsdoc + * A set of properties that can be passed to {@link Assets.getAsset}. + * @typedef {Object} Assets.GetOptions + * @property {URL} [url] an "atp:" style URL, hash, or relative mapped path to fetch + * @property {string} [responseType=text] the desired reponse type (text | arraybuffer | json) + * @property {boolean} [decompress=false] whether to attempt gunzip decompression on the fetched data + * See: {@link Assets.putAsset} and its .compress=true option + */ + /**jsdoc + * Called when Assets.getAsset is complete. + * @callback Assets~getAssetCallback + * @param {string} error - contains error message or null value if no error occured fetching the asset + * @param {Asset~getAssetResult} result - result object containing, on success containing asset metadata and contents + */ + /**jsdoc + * Result value returned by {@link Assets.getAsset}. + * @typedef {Object} Assets~getAssetResult + * @property {url} [url] the resolved "atp:" style URL for the fetched asset + * @property {string} [hash] the resolved hash for the fetched asset + * @property {string|ArrayBuffer|Object} [response] response data (possibly converted per .responseType value) + * @property {string} [responseType] response type (text | arraybuffer | json) + * @property {string} [contentType] detected asset mime-type (autodetected) + * @property {number} [byteLength] response data size in bytes + * @property {number} [decompressed] flag indicating whether data was decompressed + */ + Q_INVOKABLE void getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + + /**jsdoc + * Upload Asset data to the ATP Server + * @function Assets.putAsset + * @param {Assets.PutOptions} options A PutOptions object with upload parameters + * @param {Assets~putAssetCallback} scope[callback] A scoped callback function invoked with (error, results) + */ + /**jsdoc + * A set of properties that can be passed to {@link Assets.putAsset}. + * @typedef {Object} Assets.PutOptions + * @property {ArrayBuffer|string} [data] byte buffer or string value representing the new asset's content + * @property {string} [path=null] ATP path mapping to automatically create (upon successful upload to hash) + * @property {boolean} [compress=false] whether to gzip compress data before uploading + */ + /**jsdoc + * Called when Assets.putAsset is complete. + * @callback Assets~puttAssetCallback + * @param {string} error - contains error message (or null value if no error occured while uploading/mapping the new asset) + * @param {Asset~putAssetResult} result - result object containing error or result status of asset upload + */ + /**jsdoc + * Result value returned by {@link Assets.putAsset}. + * @typedef {Object} Assets~putAssetResult + * @property {url} [url] the resolved "atp:" style URL for the uploaded asset (based on .path if specified, otherwise on the resulting ATP hash) + * @property {string} [path] the uploaded asset's resulting ATP path (or undefined if no path mapping was assigned) + * @property {string} [hash] the uploaded asset's resulting ATP hash + * @property {boolean} [compressed] flag indicating whether the data was compressed before upload + * @property {number} [byteLength] flag indicating final byte size of the data uploaded to the ATP server + */ + Q_INVOKABLE void putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + + Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + Q_INVOKABLE void resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + Q_INVOKABLE void decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + Q_INVOKABLE void compressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + + Q_INVOKABLE bool initializeCache() { return Parent::initializeCache(); } + + Q_INVOKABLE bool canWriteCacheValue(const QUrl& url); + + Q_INVOKABLE void getCacheStatus(QScriptValue scope, QScriptValue callback = QScriptValue()) { + jsPromiseReady(Parent::getCacheStatus(), scope, callback); + } + + Q_INVOKABLE void queryCacheMeta(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + Q_INVOKABLE void loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + Q_INVOKABLE void saveToCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + Q_INVOKABLE void saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& metadata, + QScriptValue scope, QScriptValue callback = QScriptValue()); protected: - QSet _pendingRequests; - QScriptEngine* _engine; + QScriptValue jsBindCallback(QScriptValue scope, QScriptValue callback = QScriptValue()); + Promise jsPromiseReady(Promise promise, QScriptValue scope, QScriptValue callback = QScriptValue()); + + void jsCallback(const QScriptValue& handler, const QScriptValue& error, const QVariantMap& result); + void jsCallback(const QScriptValue& handler, const QScriptValue& error, const QScriptValue& result); + bool jsVerify(bool condition, const QString& error); }; #endif // hifi_AssetScriptingInterface_h diff --git a/libraries/script-engine/src/Mat4.cpp b/libraries/script-engine/src/Mat4.cpp index 6965f43b32..15015782e2 100644 --- a/libraries/script-engine/src/Mat4.cpp +++ b/libraries/script-engine/src/Mat4.cpp @@ -11,7 +11,9 @@ #include #include +#include #include +#include #include "ScriptEngineLogging.h" #include "ScriptEngine.h" #include "Mat4.h" @@ -32,6 +34,14 @@ glm::mat4 Mat4::createFromColumns(const glm::vec4& col0, const glm::vec4& col1, return glm::mat4(col0, col1, col2, col3); } +glm::mat4 Mat4::createFromArray(const QVector& floats) const { + if (floats.size() != 16 && floats.size() != 9) { + context()->throwError("createFromVector requires 16 floats for mat4 (or 9 if providing a mat3)"); + return glm::mat4(); + } + return floats.size() == 9 ? glm::mat4(glm::make_mat3(floats.constData())) : glm::make_mat4(floats.constData()); +} + glm::vec3 Mat4::extractTranslation(const glm::mat4& m) const { return ::extractTranslation(m); } diff --git a/libraries/script-engine/src/Mat4.h b/libraries/script-engine/src/Mat4.h index 8b942874ee..ceeea3ccec 100644 --- a/libraries/script-engine/src/Mat4.h +++ b/libraries/script-engine/src/Mat4.h @@ -17,6 +17,9 @@ #include #include #include +#include +#include +#include "RegisteredMetaTypes.h" /// Scriptable Mat4 object. Used exclusively in the JavaScript API class Mat4 : public QObject, protected QScriptable { @@ -28,6 +31,7 @@ public slots: glm::mat4 createFromRotAndTrans(const glm::quat& rot, const glm::vec3& trans) const; glm::mat4 createFromScaleRotAndTrans(const glm::vec3& scale, const glm::quat& rot, const glm::vec3& trans) const; glm::mat4 createFromColumns(const glm::vec4& col0, const glm::vec4& col1, const glm::vec4& col2, const glm::vec4& col3) const; + glm::mat4 createFromArray(const QVector& floats) const; glm::vec3 extractTranslation(const glm::mat4& m) const; glm::quat extractRotation(const glm::mat4& m) const; diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 9d073453ae..c79ffffec7 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -56,6 +56,7 @@ #include #include "ArrayBufferViewClass.h" +#include "AssetScriptingInterface.h" #include "BatchLoader.h" #include "BaseScriptEngine.h" #include "DataViewClass.h" @@ -175,6 +176,7 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const _timerFunctionMap(), _fileNameString(fileNameString), _arrayBufferClass(new ArrayBufferClass(this)), + _assetScriptingInterface(new AssetScriptingInterface(this)), // don't delete `ScriptEngines` until all `ScriptEngine`s are gone _scriptEngines(DependencyManager::get()) { @@ -704,7 +706,7 @@ void ScriptEngine::init() { // constants globalObject().setProperty("TREE_SCALE", newVariant(QVariant(TREE_SCALE))); - registerGlobalObject("Assets", &_assetScriptingInterface); + registerGlobalObject("Assets", _assetScriptingInterface); registerGlobalObject("Resources", DependencyManager::get().data()); registerGlobalObject("DebugDraw", &DebugDraw::getInstance()); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 17c0e0713a..7f69eee990 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -321,7 +321,7 @@ protected: ArrayBufferClass* _arrayBufferClass; - AssetScriptingInterface _assetScriptingInterface{ this }; + AssetScriptingInterface* _assetScriptingInterface; std::function _emitScriptUpdates{ []() { return true; } }; diff --git a/libraries/shared/src/BaseScriptEngine.cpp b/libraries/shared/src/BaseScriptEngine.cpp index c92d629b75..22ae01d72f 100644 --- a/libraries/shared/src/BaseScriptEngine.cpp +++ b/libraries/shared/src/BaseScriptEngine.cpp @@ -325,6 +325,12 @@ QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue } else if (methodOrName.isFunction()) { scope = scopeOrCallback; callback = methodOrName; + } else if (!methodOrName.isValid()) { + // instantiate from an existing scoped handler object + if (scopeOrCallback.property("callback").isFunction()) { + scope = scopeOrCallback.property("scope"); + callback = scopeOrCallback.property("callback"); + } } } auto handler = engine->newObject(); diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp index 956c61deaf..0742a5625b 100644 --- a/libraries/shared/src/GeometryUtil.cpp +++ b/libraries/shared/src/GeometryUtil.cpp @@ -292,6 +292,14 @@ glm::vec3 Triangle::getNormal() const { return glm::normalize(glm::cross(edge1, edge2)); } +Triangle Triangle::operator*(const glm::mat4& transform) const { + return { + glm::vec3(transform * glm::vec4(v0, 1.0f)), + glm::vec3(transform * glm::vec4(v1, 1.0f)), + glm::vec3(transform * glm::vec4(v2, 1.0f)) + }; +} + bool findRayTriangleIntersection(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, float& distance, bool allowBackface) { glm::vec3 firstSide = v0 - v1; diff --git a/libraries/shared/src/GeometryUtil.h b/libraries/shared/src/GeometryUtil.h index dcb90643b6..4832616fbd 100644 --- a/libraries/shared/src/GeometryUtil.h +++ b/libraries/shared/src/GeometryUtil.h @@ -104,6 +104,7 @@ public: glm::vec3 v1; glm::vec3 v2; glm::vec3 getNormal() const; + Triangle operator*(const glm::mat4& transform) const; }; inline bool findRayTriangleIntersection(const glm::vec3& origin, const glm::vec3& direction, diff --git a/libraries/shared/src/TriangleSet.cpp b/libraries/shared/src/TriangleSet.cpp index ce7dd18921..3f8f748720 100644 --- a/libraries/shared/src/TriangleSet.cpp +++ b/libraries/shared/src/TriangleSet.cpp @@ -31,7 +31,7 @@ void TriangleSet::clear() { } bool TriangleSet::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, bool allowBackface) { + float& distance, BoxFace& face, Triangle& triangle, bool precision, bool allowBackface) { // reset our distance to be the max possible, lower level tests will store best distance here distance = std::numeric_limits::max(); @@ -41,7 +41,7 @@ bool TriangleSet::findRayIntersection(const glm::vec3& origin, const glm::vec3& } int trianglesTouched = 0; - auto result = _triangleOctree.findRayIntersection(origin, direction, distance, face, surfaceNormal, precision, trianglesTouched, allowBackface); + auto result = _triangleOctree.findRayIntersection(origin, direction, distance, face, triangle, precision, trianglesTouched, allowBackface); #if WANT_DEBUGGING if (precision) { @@ -95,11 +95,12 @@ void TriangleSet::balanceOctree() { // Determine of the given ray (origin/direction) in model space intersects with any triangles // in the set. If an intersection occurs, the distance and surface normal will be provided. bool TriangleSet::TriangleOctreeCell::findRayIntersectionInternal(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface) { + float& distance, BoxFace& face, Triangle& triangle, bool precision, int& trianglesTouched, bool allowBackface) { bool intersectedSomething = false; float boxDistance = distance; float bestDistance = distance; + glm::vec3 surfaceNormal; if (_bounds.findRayIntersection(origin, direction, boxDistance, face, surfaceNormal)) { @@ -112,14 +113,14 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersectionInternal(const glm::vec if (precision) { for (const auto& triangleIndex : _triangleIndices) { - const auto& triangle = _allTriangles[triangleIndex]; + const auto& thisTriangle = _allTriangles[triangleIndex]; float thisTriangleDistance; trianglesTouched++; - if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance, allowBackface)) { + if (findRayTriangleIntersection(origin, direction, thisTriangle, thisTriangleDistance, allowBackface)) { if (thisTriangleDistance < bestDistance) { bestDistance = thisTriangleDistance; intersectedSomething = true; - surfaceNormal = triangle.getNormal(); + triangle = thisTriangle; distance = bestDistance; } } @@ -204,7 +205,8 @@ void TriangleSet::TriangleOctreeCell::insert(size_t triangleIndex) { } bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface) { + float& distance, BoxFace& face, Triangle& triangle, bool precision, int& trianglesTouched, + bool allowBackface) { if (_population < 1) { return false; // no triangles below here, so we can't intersect @@ -212,6 +214,7 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origi float bestLocalDistance = distance; BoxFace bestLocalFace; + Triangle bestLocalTriangle; glm::vec3 bestLocalNormal; bool intersects = false; @@ -229,7 +232,7 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origi float childDistance = distance; BoxFace childFace; - glm::vec3 childNormal; + Triangle childTriangle; // if we're not yet at the max depth, then check which child the triangle fits in if (_depth < MAX_DEPTH) { @@ -237,22 +240,22 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origi // check each child, if there's an intersection, it will return some distance that we need // to compare against the other results, because there might be multiple intersections and // we will always choose the best (shortest) intersection - if (child.second.findRayIntersection(origin, direction, childDistance, childFace, childNormal, precision, trianglesTouched)) { + if (child.second.findRayIntersection(origin, direction, childDistance, childFace, childTriangle, precision, trianglesTouched)) { if (childDistance < bestLocalDistance) { bestLocalDistance = childDistance; bestLocalFace = childFace; - bestLocalNormal = childNormal; + bestLocalTriangle = childTriangle; intersects = true; } } } } // also check our local triangle set - if (findRayIntersectionInternal(origin, direction, childDistance, childFace, childNormal, precision, trianglesTouched, allowBackface)) { + if (findRayIntersectionInternal(origin, direction, childDistance, childFace, childTriangle, precision, trianglesTouched, allowBackface)) { if (childDistance < bestLocalDistance) { bestLocalDistance = childDistance; bestLocalFace = childFace; - bestLocalNormal = childNormal; + bestLocalTriangle = childTriangle; intersects = true; } } @@ -260,7 +263,7 @@ bool TriangleSet::TriangleOctreeCell::findRayIntersection(const glm::vec3& origi if (intersects) { distance = bestLocalDistance; face = bestLocalFace; - surfaceNormal = bestLocalNormal; + triangle = bestLocalTriangle; } return intersects; } diff --git a/libraries/shared/src/TriangleSet.h b/libraries/shared/src/TriangleSet.h index 3b0b33d7d5..786f58804f 100644 --- a/libraries/shared/src/TriangleSet.h +++ b/libraries/shared/src/TriangleSet.h @@ -27,7 +27,8 @@ class TriangleSet { void clear(); bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface = false); + float& distance, BoxFace& face, Triangle& triangle, bool precision, int& trianglesTouched, + bool allowBackface = false); const AABox& getBounds() const { return _bounds; } @@ -38,7 +39,8 @@ class TriangleSet { // checks our internal list of triangles bool findRayIntersectionInternal(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched, bool allowBackface = false); + float& distance, BoxFace& face, Triangle& triangle, bool precision, int& trianglesTouched, + bool allowBackface = false); std::vector& _allTriangles; std::map _children; @@ -60,7 +62,7 @@ public: void insert(const Triangle& t); bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, bool allowBackface = false); + float& distance, BoxFace& face, Triangle& triangle, bool precision, bool allowBackface = false); void balanceOctree(); @@ -72,7 +74,7 @@ public: // intersection occurs, the distance and surface normal will be provided. // note: this might side-effect internal structures bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision, int& trianglesTouched); + float& distance, BoxFace& face, Triangle& triangle, bool precision, int& trianglesTouched); // Determine if a point is "inside" all the triangles of a convex hull. It is the responsibility of the caller to // determine that the triangle set is indeed a convex hull. If the triangles added to this set are not in fact a diff --git a/libraries/shared/src/shared/MiniPromises.cpp b/libraries/shared/src/shared/MiniPromises.cpp new file mode 100644 index 0000000000..bb78852c29 --- /dev/null +++ b/libraries/shared/src/shared/MiniPromises.cpp @@ -0,0 +1,27 @@ +// +// Created by Timothy Dedischew on 2017/12/21 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "MiniPromises.h" +#include +#include + +int MiniPromise::metaTypeID = qRegisterMetaType("MiniPromise::Promise"); + +namespace { + void promiseFromScriptValue(const QScriptValue& object, MiniPromise::Promise& promise) { + Q_ASSERT(false); + } + QScriptValue promiseToScriptValue(QScriptEngine *engine, const MiniPromise::Promise& promise) { + return engine->newQObject(promise.get()); + } +} +void MiniPromise::registerMetaTypes(QObject* engine) { + auto scriptEngine = qobject_cast(engine); + qDebug() << "----------------------- MiniPromise::registerMetaTypes ------------" << scriptEngine; + qScriptRegisterMetaType(scriptEngine, promiseToScriptValue, promiseFromScriptValue); +} diff --git a/libraries/shared/src/shared/MiniPromises.h b/libraries/shared/src/shared/MiniPromises.h new file mode 100644 index 0000000000..5983f135b7 --- /dev/null +++ b/libraries/shared/src/shared/MiniPromises.h @@ -0,0 +1,278 @@ +// +// Created by Timothy Dedischew on 2017/12/21 +// Copyright 2017 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 +// + +#pragma once + +// Minimalist threadsafe Promise-like helper for managing asynchronous results +// +// This class pivots around continuation-style callback handlers: +// auto successCallback = [=](QVariantMap result) { .... } +// auto errorCallback = [=](QString error) { .... } +// auto combinedCallback = [=](QString error, QVariantMap result) { .... } +// +// * Callback Handlers are automatically invoked on the right thread (the Promise's thread). +// * Callbacks can be assigned anytime during a Promise's life and "do the right thing". +// - ie: for code clarity you can define success cases first or choose to maintain time order +// * "Deferred" concept can be used to publish outcomes. +// * "Promise" concept be used to subscribe to outcomes. +// +// See AssetScriptingInterface.cpp for some examples of using to simplify chained async results. + +#include +#include +#include +#include +#include "ReadWriteLockable.h" +#include + +class MiniPromise : public QObject, public std::enable_shared_from_this, public ReadWriteLockable { + Q_OBJECT + Q_PROPERTY(QString state READ getStateString) + Q_PROPERTY(QString error READ getError) + Q_PROPERTY(QVariantMap result READ getResult) +public: + using HandlerFunction = std::function; + using SuccessFunction = std::function; + using ErrorFunction = std::function; + using HandlerFunctions = QVector; + using Promise = std::shared_ptr; + + static void registerMetaTypes(QObject* engine); + static int metaTypeID; + + MiniPromise() {} + MiniPromise(const QString debugName) { setObjectName(debugName); } + + ~MiniPromise() { + if (getStateString() == "pending") { + qWarning() << "MiniPromise::~MiniPromise -- destroying pending promise:" << objectName() << _error << _result << "handlers:" << getPendingHandlerCount(); + } + } + Promise self() { return shared_from_this(); } + + Q_INVOKABLE void executeOnPromiseThread(std::function function, MiniPromise::Promise root = nullptr) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod( + this, "executeOnPromiseThread", Qt::QueuedConnection, + Q_ARG(std::function, function), + Q_ARG(MiniPromise::Promise, self())); + } else { + function(); + } + } + + // result aggregation helpers -- eg: deferred->defaults(interimResultMap)->ready(...) + // copy values from the input map, but only for keys that don't already exist + Promise mixin(const QVariantMap& source) { + withWriteLock([&]{ + for (const auto& key : source.keys()) { + if (!_result.contains(key)) { + _result[key] = source[key]; + } + } + }); + return self(); + } + // copy values from the input map, replacing any existing keys + Promise assignResult(const QVariantMap& source) { + withWriteLock([&]{ + for (const auto& key : source.keys()) { + _result[key] = source[key]; + } + }); + return self(); + } + + // callback registration methods + Promise ready(HandlerFunction always) { return finally(always); } + Promise finally(HandlerFunction always) { + if (!_rejected && !_resolved) { + withWriteLock([&]{ + _onfinally << always; + }); + } else { + executeOnPromiseThread([&]{ + always(getError(), getResult()); + }); + } + return self(); + } + Promise fail(ErrorFunction errorOnly) { + return fail([this, errorOnly](QString error, QVariantMap result) { + errorOnly(error); + }); + } + + Promise fail(HandlerFunction failFunc) { + if (!_rejected) { + withWriteLock([&]{ + _onreject << failFunc; + }); + } else { + executeOnPromiseThread([&]{ + failFunc(getError(), getResult()); + }); + } + return self(); + } + + Promise then(SuccessFunction successOnly) { + return then([this, successOnly](QString error, QVariantMap result) { + successOnly(result); + }); + } + Promise then(HandlerFunction successFunc) { + if (!_resolved) { + withWriteLock([&]{ + _onresolve << successFunc; + }); + } else { + executeOnPromiseThread([&]{ + successFunc(getError(), getResult()); + }); + } + return self(); + } + + // NOTE: first arg may be null (see ES6 .then(null, errorHandler) conventions) + Promise then(SuccessFunction successOnly, ErrorFunction errorOnly) { + if (successOnly) { + then(successOnly); + } + if (errorOnly) { + fail(errorOnly); + } + return self(); + } + + + // helper functions for forwarding results on to a next Promise + Promise ready(Promise next) { return finally(next); } + Promise finally(Promise next) { + return finally([next](QString error, QVariantMap result) { + next->handle(error, result); + }); + } + Promise fail(Promise next) { + return fail([next](QString error, QVariantMap result) { + next->reject(error, result); + }); + } + Promise then(Promise next) { + return then([next](QString error, QVariantMap result) { + next->resolve(error, result); + }); + } + + + // trigger methods + // handle() automatically resolves or rejects the promise (based on whether an error value occurred) + Promise handle(QString error, const QVariantMap& result) { + if (error.isEmpty()) { + resolve(error, result); + } else { + reject(error, result); + } + return self(); + } + + Promise resolve(QVariantMap result) { + return resolve(QString(), result); + } + Promise resolve(QString error, const QVariantMap& result) { + setState(true, error, result); + + executeOnPromiseThread([&]{ + const QString localError{ getError() }; + const QVariantMap localResult{ getResult() }; + HandlerFunctions resolveHandlers; + HandlerFunctions finallyHandlers; + withReadLock([&]{ + resolveHandlers = _onresolve; + finallyHandlers = _onfinally; + }); + for (const auto& onresolve : resolveHandlers) { + onresolve(localError, localResult); + } + for (const auto& onfinally : finallyHandlers) { + onfinally(localError, localResult); + } + }); + return self(); + } + + Promise reject(QString error) { + return reject(error, QVariantMap()); + } + Promise reject(QString error, const QVariantMap& result) { + setState(false, error, result); + + executeOnPromiseThread([&]{ + const QString localError{ getError() }; + const QVariantMap localResult{ getResult() }; + HandlerFunctions rejectHandlers; + HandlerFunctions finallyHandlers; + withReadLock([&]{ + rejectHandlers = _onreject; + finallyHandlers = _onfinally; + }); + for (const auto& onreject : rejectHandlers) { + onreject(localError, localResult); + } + for (const auto& onfinally : finallyHandlers) { + onfinally(localError, localResult); + } + }); + return self(); + } + +private: + + Promise setState(bool resolved, QString error, const QVariantMap& result) { + if (resolved) { + _resolved = true; + } else { + _rejected = true; + } + setError(error); + assignResult(result); + return self(); + } + + void setError(const QString error) { withWriteLock([&]{ _error = error; }); } + QString getError() const { return resultWithReadLock([this]() -> QString { return _error; }); } + QVariantMap getResult() const { return resultWithReadLock([this]() -> QVariantMap { return _result; }); } + int getPendingHandlerCount() const { + return resultWithReadLock([this]() -> int { + return _onresolve.size() + _onreject.size() + _onfinally.size(); + }); + } + QString getStateString() const { + return _rejected ? "rejected" : + _resolved ? "resolved" : + getPendingHandlerCount() ? "pending" : + "unknown"; + } + QString _error; + QVariantMap _result; + std::atomic _rejected{false}; + std::atomic _resolved{false}; + HandlerFunctions _onresolve; + HandlerFunctions _onreject; + HandlerFunctions _onfinally; +}; + +Q_DECLARE_METATYPE(MiniPromise::Promise) + +inline MiniPromise::Promise makePromise(const QString& hint = QString()) { + if (!QMetaType::isRegistered(qMetaTypeId())) { + int type = qRegisterMetaType(); + qDebug() << "makePromise -- registered MetaType:" << type; + } + return std::make_shared(hint); +} diff --git a/script-archive/avatarSelector.js b/script-archive/avatarSelector.js index 47740ef0b3..119044e35a 100644 --- a/script-archive/avatarSelector.js +++ b/script-archive/avatarSelector.js @@ -274,7 +274,7 @@ function actionStartEvent(event) { var result = Overlays.findRayIntersection(event.actionRay); if (result.intersects && result.overlayID == panelWall) { - var panelName = result.extraInfo; + var panelName = result.extraInfo.subMeshName + ''; var panelStringIndex = panelName.indexOf("Panel"); if (panelStringIndex != -1) { @@ -338,7 +338,7 @@ function handleLookAt(pickRay) { // check if we hit a panel and if we should jump there var result = Overlays.findRayIntersection(pickRay); if (result.intersects && result.overlayID == panelWall) { - var panelName = result.extraInfo; + var panelName = result.extraInfo.subMeshName + ''; var panelStringIndex = panelName.indexOf("Panel"); if (panelStringIndex != -1) { var panelIndex = parseInt(panelName.slice(5)); diff --git a/script-archive/lobby.js b/script-archive/lobby.js index 6fa4a42cb6..7a06cdd906 100644 --- a/script-archive/lobby.js +++ b/script-archive/lobby.js @@ -272,7 +272,7 @@ function actionStartEvent(event) { var result = Overlays.findRayIntersection(event.actionRay); if (result.intersects && result.overlayID == panelWall) { - var panelName = result.extraInfo; + var panelName = result.extraInfo.subMeshName + ''; var panelStringIndex = panelName.indexOf("Panel"); if (panelStringIndex != -1) { @@ -321,7 +321,7 @@ function handleLookAt(pickRay) { // check if we hit a panel and if we should jump there var result = Overlays.findRayIntersection(pickRay); if (result.intersects && result.overlayID == panelWall) { - var panelName = result.extraInfo; + var panelName = result.extraInfo.subMeshName + ''; var panelStringIndex = panelName.indexOf("Panel"); if (panelStringIndex != -1) { var panelIndex = parseInt(panelName.slice(5)); diff --git a/scripts/developer/tests/unit_tests/assetUnitTests.js b/scripts/developer/tests/unit_tests/assetUnitTests.js new file mode 100644 index 0000000000..5d43eaf1de --- /dev/null +++ b/scripts/developer/tests/unit_tests/assetUnitTests.js @@ -0,0 +1,502 @@ +/* eslint-env jasmine */ + +if (!Entities.canWriteAssets()) { + Window.alert('!Entities.canWriteAssets -- please goto a domain with asset rights and reload this test script'); + throw new Error('!Entities.canWriteAssets'); +} + +instrument_testrunner(true); + +describe("Assets", function () { + var context = { + memo: {}, + cache: {}, + definedHash: null, + definedPath: null, + definedContent: null, + }; + var ATP_TIMEOUT_MS = 1000; // ms + + var NULL_HASH = new Array(64+1).join('0'); // 64 hex + var SAMPLE_FOLDER = '/assetUnitTests'; + var SAMPLE_FILE_PATH = SAMPLE_FOLDER + "/test.json"; + var SAMPLE_CONTENTS = 'Test Run on ' + JSON.stringify(Date()); + var SAMPLE_JSON = JSON.stringify({ content: SAMPLE_CONTENTS }); + var SAMPLE_FLOATS = [ Math.PI, 1.1, 2.2, 3.3 ]; + var SAMPLE_BUFFER = new Float32Array(SAMPLE_FLOATS).buffer; + + var IS_ASSET_HASH_REGEX = /^[a-f0-9]{64}$/i; + var IS_ASSET_URL = /^atp:/; + it('Entities.canWriteAssets', function() { + expect(Entities.canWriteAssets()).toBe(true); + }); + + describe('extractAssetHash(input)', function() { + // new helper method that provides a catch-all way to get at the sha256 hash + // considering the multiple, different ways ATP hash URLs are found across the + // system and in content. + + var POSITIVE_TEST_URLS = [ + 'atp:HASH', + 'atp:HASH.obj', + 'atp:HASH.fbx#cache-buster', + 'atp:HASH.fbx?cache-buster', + 'HASH' + ]; + var NEGATIVE_TEST_URLS = [ + 'asdf', + 'http://domain.tld', + '/path/filename.fbx', + 'atp:/path/filename.fbx?#HASH', + 'atp:/.baked/HASH/asset.fbx', + '' + ]; + it("POSITIVE_TEST_URLS", function() { + POSITIVE_TEST_URLS.forEach(function(url) { + url = url.replace('HASH', NULL_HASH); + expect([url, Assets.extractAssetHash(url)].join('|')).toEqual([url, NULL_HASH].join('|')); + }); + }); + it("NEGATIVE_TEST_URLS", function() { + NEGATIVE_TEST_URLS.forEach(function(url) { + expect(Assets.extractAssetHash(url.replace('HASH', NULL_HASH))).toEqual(''); + }); + }); + }); + + // new APIs + describe('.putAsset', function() { + it("data", function(done) { + Assets.putAsset(SAMPLE_CONTENTS, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.hash).toMatch(IS_ASSET_HASH_REGEX); + context.memo.stringURL = result.url; + done(); + }); + }); + it('nopath.text', function(done) { + Assets.putAsset({ + data: SAMPLE_CONTENTS, + }, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.hash).toMatch(IS_ASSET_HASH_REGEX); + context.memo.textHash = result.hash; + context.memo.textURL = result.url; + done(); + }); + }); + it('path.text', function(done) { + var samplePath = SAMPLE_FOLDER + '/content.json'; + Assets.putAsset({ + path: samplePath, + data: SAMPLE_JSON, + }, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.path).toEqual(samplePath); + expect(result.hash).toMatch(IS_ASSET_HASH_REGEX); + context.memo.jsonPath = result.path; + done(); + }); + }); + it('path.buffer', function(done) { + var samplePath = SAMPLE_FOLDER + '/content.buffer'; + Assets.putAsset({ + path: samplePath, + data: SAMPLE_BUFFER, + }, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.path).toEqual(samplePath); + expect(result.hash).toMatch(IS_ASSET_HASH_REGEX); + expect(result.byteLength).toEqual(SAMPLE_BUFFER.byteLength); + context.memo.bufferURL = result.url; + context.memo.bufferHash = result.hash; + done(); + }); + }); + it('path.json.gz', function(done) { + var samplePath = SAMPLE_FOLDER + '/content.json.gz'; + Assets.putAsset({ + path: samplePath, + data: SAMPLE_JSON, + compress: true, + }, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.path).toEqual(samplePath); + expect(result.hash).toMatch(IS_ASSET_HASH_REGEX); + context.memo.jsonCompressedPath = result.path; + done(); + }); + }); + }); + // it('.deleteAsset(options, {callback(error, result)})', function() { pending(); }); + it('.resolveAsset', function(done) { + expect(context.memo.bufferURL).toBeDefined(); + Assets.resolveAsset(context.memo.bufferURL, function(error, result) { + expect(error).toBe(null); + expect(result.url).toEqual(context.memo.bufferURL); + expect(result.hash).toEqual(context.memo.bufferHash); + done(); + }); + }); + describe('.getAsset', function() { + it('path/', function(done) { + expect(context.memo.jsonPath).toBeDefined(); + Assets.getAsset(context.memo.jsonPath, function(error, result) { + expect(error).toBe(null); + expect(result.response).toEqual(SAMPLE_JSON); + done(); + }); + }); + it('text', function(done) { + expect(context.memo.textURL).toBeDefined(); + Assets.getAsset({ + url: context.memo.textURL, + responseType: 'text', + }, function(error, result) { + expect(error).toBe(null); + expect(result.response).toEqual(SAMPLE_CONTENTS); + expect(result.url).toEqual(context.memo.textURL); + done(); + }); + }); + it('arraybuffer', function(done) { + expect(context.memo.bufferURL).toBeDefined(); + Assets.getAsset({ + url: context.memo.bufferURL, + responseType: 'arraybuffer', + }, function(error, result) { + expect(error).toBe(null); + expect(result.response.byteLength).toEqual(SAMPLE_BUFFER.byteLength); + var expected = [].slice.call(new Float32Array(SAMPLE_BUFFER)).join(','); + var actual = [].slice.call(new Float32Array(result.response)).join(','); + expect(actual).toEqual(expected); + done(); + }); + }); + it('json', function(done) { + expect(context.memo.jsonPath).toBeDefined(); + Assets.getAsset({ + url: context.memo.jsonPath, + responseType: 'json', + }, function(error, result) { + expect(error).toBe(null); + expect(result.response.content).toEqual(SAMPLE_CONTENTS); + done(); + }); + }); + it('json.gz', function(done) { + expect(context.memo.jsonCompressedPath).toBeDefined(); + Assets.getAsset({ + url: context.memo.jsonCompressedPath, + responseType: 'json', + decompress: true, + }, function(error, result) { + expect(error).toBe(null); + expect(result.decompressed).toBe(true); + expect(result.response.content).toEqual(SAMPLE_CONTENTS); + done(); + }); + }); + }); + + // cache APIs + it('.getCacheStatus', function(done) { + Assets.getCacheStatus(function(error, result) { + expect(error).toBe(null); + expect(result.cacheSize).toBeGreaterThan(0); + expect(result.maximumCacheSize).toBeGreaterThan(0); + done(); + }); + }); + describe('.saveToCache', function() { + it(".data", function(done) { + Assets.saveToCache({ data: SAMPLE_CONTENTS }, function(error, result) { + expect(error).toBe(null); + expect(result.success).toBe(true); + context.cache.textURL = result.url; + done(); + }); + }); + it('.url', function(done) { + var sampleURL = 'atp:' + SAMPLE_FOLDER + '/cache.json'; + Assets.saveToCache({ + url: sampleURL, + data: SAMPLE_JSON, + }, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.url).toEqual(sampleURL); + expect(result.success).toBe(true); + context.cache.jsonURL = result.url; + done(); + }); + }); + it('.buffer', function(done) { + var sampleURL = 'atp:' + SAMPLE_FOLDER + '/cache.buffer'; + Assets.saveToCache({ + url: sampleURL, + data: SAMPLE_BUFFER, + }, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.url).toEqual(sampleURL); + expect(result.success).toBe(true); + expect(result.byteLength).toEqual(SAMPLE_BUFFER.byteLength); + context.cache.bufferURL = result.url; + done(); + }); + }); + }); + it('.queryCacheMeta', function(done) { + expect(context.cache.bufferURL).toBeDefined(); + Assets.queryCacheMeta(context.cache.bufferURL, function(error, result) { + expect(error).toBe(null); + expect(result.url).toMatch(IS_ASSET_URL); + expect(result.url).toEqual(context.cache.bufferURL); + done(); + }); + }); + describe('.loadFromCache', function() { + it('urlString', function(done) { + expect(context.cache.jsonURL).toBeDefined(); + Assets.loadFromCache(context.cache.jsonURL, function(error, result) { + expect(error).toBe(null); + expect(result.response).toEqual(SAMPLE_JSON); + done(); + }); + }); + it('.responseType=text', function(done) { + expect(context.cache.textURL).toBeDefined(); + Assets.loadFromCache({ + url: context.cache.textURL, + responseType: 'text', + }, function(error, result) { + expect(error).toBe(null); + expect(result.response).toEqual(SAMPLE_CONTENTS); + done(); + }); + }); + it('.responseType=arraybuffer', function(done) { + expect(context.cache.bufferURL).toBeDefined(); + Assets.loadFromCache({ + url: context.cache.bufferURL, + responseType: 'arraybuffer', + }, function(error, result) { + expect(error).toBe(null); + expect(result.response.byteLength).toEqual(SAMPLE_BUFFER.byteLength); + var expected = [].slice.call(new Float32Array(SAMPLE_BUFFER)).join(','); + var actual = [].slice.call(new Float32Array(result.response)).join(','); + expect(actual).toEqual(expected); + done(); + }); + }); + it('.responseType=json', function(done) { + expect(context.cache.jsonURL).toBeDefined(); + Assets.loadFromCache({ + url: context.cache.jsonURL, + responseType: 'json', + }, function(error, result) { + expect(error).toBe(null); + expect(result.response.content).toEqual(SAMPLE_CONTENTS); + done(); + }); + }); + }); + + // existing newly-mapped APIs + it('.getATPUrl', function() { + expect(Assets.getATPUrl(SAMPLE_FILE_PATH)).toEqual('atp:' + SAMPLE_FILE_PATH); + expect(Assets.getATPUrl(NULL_HASH)).toEqual('atp:' + NULL_HASH); + expect(Assets.getATPUrl("/file.ext?a=b#c=d")).toEqual('atp:/file.ext?a=b#c=d'); + expect(Assets.getATPUrl("atp:xxx")).toEqual(''); + expect(Assets.getATPUrl("xxx")).toEqual(''); + }); + xit('.isValidPath', function(done) { pending(); }); + xit('.isValidFilePath', function() { pending(); }); + xit('.isValidHash', function() { pending(); }); + xit('.setBakingEnabled', function() { pending(); }); + + it("Assets.uploadData(data, {callback(url, hash)})", function (done) { + Assets.uploadData(SAMPLE_CONTENTS, function(url, hash) { + expect(url).toMatch(IS_ASSET_URL); + expect(hash).toMatch(IS_ASSET_HASH_REGEX); + context.definedHash = hash; // used in later tests + context.definedContent = SAMPLE_CONTENTS; + print('context.definedHash = ' + context.definedHash); + print('context.definedContent = ' + context.definedContent); + done(); + }); + }); + + it("Assets.setMapping", function (done) { + expect(context.definedHash).toMatch(IS_ASSET_HASH_REGEX); + Assets.setMapping(SAMPLE_FILE_PATH, context.definedHash, function(error) { + if (error) error += ' ('+JSON.stringify([SAMPLE_FILE_PATH, context.definedHash])+')'; + expect(error).toBe(null); + context.definedPath = SAMPLE_FILE_PATH; + print('context.definedPath = ' + context.definedPath); + done(); + }); + }); + + it("Assets.getMapping", function (done) { + expect(context.definedHash).toMatch(IS_ASSET_HASH_REGEX, 'asfasdf'); + expect(context.definedPath).toMatch(/^\//, 'asfasdf'); + Assets.getMapping(context.definedPath, function(error, hash) { + if (error) error += ' ('+JSON.stringify([context.definedPath])+')'; + expect(error).toBe(null); + expect(hash).toMatch(IS_ASSET_HASH_REGEX); + expect(hash).toEqual(context.definedHash); + done(); + }); + }); + + it('.getMapping(nullHash)', function(done) { + // FIXME: characterization test -- current behavior is that downloadData silently fails + // when given an asset that doesn't exist + Assets.downloadData(NULL_HASH, function(result) { + throw new Error("this callback 'should' not be triggered"); + }); + setTimeout(function() { done(); }, ATP_TIMEOUT_MS); + }); + + it('.downloadData(hash)', function(done) { + expect(context.definedHash).toMatch(IS_ASSET_HASH_REGEX, 'asfasdf'); + Assets.downloadData('atp:' + context.definedHash, function(result) { + expect(result).toEqual(context.definedContent); + done(); + }); + }); + + it('.downloadData(nullHash)', function(done) { + // FIXME: characterization test -- current behavior is that downloadData silently fails + // when given an asset doesn't exist + Assets.downloadData(NULL_HASH, function(result) { + throw new Error("this callback 'should' not be triggered"); + }); + setTimeout(function() { done(); }, ATP_TIMEOUT_MS); + }); + + describe('exceptions', function() { + describe('.getAsset', function() { + it('-- invalid data.gz', function(done) { + expect(context.memo.jsonPath).toBeDefined(); + Assets.getAsset({ + url: context.memo.jsonPath, + responseType: 'json', + decompress: true, + }, function(error, result) { + expect(error).toMatch(/gunzip error/); + done(); + }); + }); + }); + describe('.setMapping', function() { + it('-- invalid path', function(done) { + Assets.setMapping('foo', NULL_HASH, function(error/*, result*/) { + expect(error).toEqual('Path is invalid'); + /* expect(result).not.toMatch(IS_ASSET_URL); */ + done(); + }); + }); + it('-- invalid hash', function(done) { + Assets.setMapping(SAMPLE_FILE_PATH, 'bar', function(error/*, result*/) { + expect(error).toEqual('Hash is invalid'); + /* expect(result).not.toMatch(IS_ASSET_URL); */ + done(); + }); + }); + }); + describe('.getMapping', function() { + it('-- invalid path throws immediate', function() { + expect(function() { + Assets.getMapping('foo', function(error, hash) { + throw new Error("should never make it here..."); + }); + throw new Error("should never make it here..."); + }).toThrowError(/invalid.*path/i); + }); + it('-- non-existing path', function(done) { + Assets.getMapping('/foo/bar/'+Date.now(), function(error, hash) { + expect(error).toEqual('Asset not found'); + expect(hash).not.toMatch(IS_ASSET_HASH_REGEX); + done(); + }); + }); + }); + }); +}); + +// ---------------------------------------------------------------------------- +// this stuff allows the unit tests to be loaded indepenently and/or as part of testRunner.js execution +function run() {} +function instrument_testrunner(force) { + if (force || typeof describe === 'undefined') { + var oldPrint = print; + window = new OverlayWebWindow({ + title: 'assetUnitTests.js', + width: 640, + height: 480, + source: 'about:blank', + }); + Script.scriptEnding.connect(window, 'close'); + window.closed.connect(Script, 'stop'); + // wait for window (ready) and test runner (ran) both to initialize + var ready = false; + var ran = false; + window.webEventReceived.connect(function() { ready = true; maybeRun(); }); + run = function() { ran = true; maybeRun(); }; + + window.setURL([ + 'data:text/html;text,', + '', + '
', + '', + '', + ].join('\n')); + print = function() { + var str = [].slice.call(arguments).join(' '); + if (ready) { + window.emitScriptEvent(str + '\n'); + } else { + oldPrint('!ready', str); + } + }; + + // Include testing library + Script.include('../../libraries/jasmine/jasmine.js'); + Script.include('../../libraries/jasmine/hifi-boot.js'); + + function maybeRun() { + if (!ran || !ready) { + return oldPrint('doRun -- waiting for both script and web window to become available'); + } + if (!force) { + // invoke Script.stop (after any async tests complete) + jasmine.getEnv().addReporter({ jasmineDone: Script.stop }); + } else { + jasmine.getEnv().addReporter({ jasmineDone: function() { print("JASMINE DONE"); } }); + } + + // Run the tests + jasmine.getEnv().execute(); + }; + } +} +run(); diff --git a/tools/atp-client/src/ATPClientApp.cpp b/tools/atp-client/src/ATPClientApp.cpp index 9fd1bf8d4f..307c913a96 100644 --- a/tools/atp-client/src/ATPClientApp.cpp +++ b/tools/atp-client/src/ATPClientApp.cpp @@ -361,7 +361,7 @@ void ATPClientApp::lookupAsset() { request->start(); } -void ATPClientApp::download(AssetHash hash) { +void ATPClientApp::download(AssetUtils::AssetHash hash) { auto assetClient = DependencyManager::get(); auto assetRequest = new AssetRequest(hash); diff --git a/tools/atp-client/src/ATPClientApp.h b/tools/atp-client/src/ATPClientApp.h index a3904d6e50..42760ceca9 100644 --- a/tools/atp-client/src/ATPClientApp.h +++ b/tools/atp-client/src/ATPClientApp.h @@ -45,7 +45,7 @@ private: void setMapping(QString hash); void lookupAsset(); void listAssets(); - void download(AssetHash hash); + void download(AssetUtils::AssetHash hash); void finish(int exitCode); bool _verbose;