mirror of
https://github.com/overte-org/overte.git
synced 2025-04-16 23:26:25 +02:00
Merge branch 'master' into 20851
Conflicts: interface/resources/qml/dialogs/MessageDialog.qml
This commit is contained in:
commit
5faa7b3e87
132 changed files with 4324 additions and 1206 deletions
|
@ -12,19 +12,21 @@
|
|||
|
||||
#include "AssetServer.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QString>
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QCryptographicHash>
|
||||
#include <QtCore/QDateTime>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QJsonDocument>
|
||||
#include <QtCore/QString>
|
||||
|
||||
#include <ServerPathUtils.h>
|
||||
|
||||
#include "NetworkLogging.h"
|
||||
#include "NodeType.h"
|
||||
#include "SendAssetTask.h"
|
||||
#include "UploadAssetTask.h"
|
||||
#include <ServerPathUtils.h>
|
||||
|
||||
const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server";
|
||||
|
||||
|
@ -42,6 +44,7 @@ AssetServer::AssetServer(ReceivedMessage& message) :
|
|||
packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet");
|
||||
packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo");
|
||||
packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload");
|
||||
packetReceiver.registerListener(PacketType::AssetMappingOperation, this, "handleAssetMappingOperation");
|
||||
}
|
||||
|
||||
void AssetServer::run() {
|
||||
|
@ -56,6 +59,8 @@ void AssetServer::run() {
|
|||
ThreadedAssignment::commonInit(ASSET_SERVER_LOGGING_TARGET_NAME, NodeType::AssetServer);
|
||||
}
|
||||
|
||||
static const QString ASSET_FILES_SUBDIR = "files";
|
||||
|
||||
void AssetServer::completeSetup() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
|
@ -96,85 +101,208 @@ void AssetServer::completeSetup() {
|
|||
|
||||
qDebug() << "Creating resources directory";
|
||||
_resourcesDirectory.mkpath(".");
|
||||
_filesDirectory = _resourcesDirectory;
|
||||
|
||||
bool noExistingAssets = !_resourcesDirectory.exists() || _resourcesDirectory.entryList(QDir::Files).size() == 0;
|
||||
|
||||
if (noExistingAssets) {
|
||||
qDebug() << "Asset resources directory empty, searching for existing asset resources to migrate";
|
||||
QString oldDataDirectory = QCoreApplication::applicationDirPath();
|
||||
|
||||
const QString OLD_RESOURCES_PATH = "assets";
|
||||
|
||||
auto oldResourcesDirectory = QDir(oldDataDirectory).filePath("resources/" + OLD_RESOURCES_PATH);
|
||||
|
||||
|
||||
if (QDir(oldResourcesDirectory).exists()) {
|
||||
qDebug() << "Existing assets found in " << oldResourcesDirectory << ", copying to " << _resourcesDirectory;
|
||||
|
||||
|
||||
QDir resourcesParentDirectory = _resourcesDirectory.filePath("..");
|
||||
if (!resourcesParentDirectory.exists()) {
|
||||
qDebug() << "Creating data directory " << resourcesParentDirectory.absolutePath();
|
||||
resourcesParentDirectory.mkpath(".");
|
||||
}
|
||||
|
||||
auto files = QDir(oldResourcesDirectory).entryList(QDir::Files);
|
||||
|
||||
for (auto& file : files) {
|
||||
auto from = oldResourcesDirectory + QDir::separator() + file;
|
||||
auto to = _resourcesDirectory.absoluteFilePath(file);
|
||||
qDebug() << "\tCopying from " << from << " to " << to;
|
||||
QFile::copy(from, to);
|
||||
}
|
||||
|
||||
}
|
||||
if (!_resourcesDirectory.mkpath(ASSET_FILES_SUBDIR) || !_filesDirectory.cd(ASSET_FILES_SUBDIR)) {
|
||||
qCritical() << "Unable to create file directory for asset-server files. Stopping assignment.";
|
||||
setFinished(true);
|
||||
return;
|
||||
}
|
||||
qDebug() << "Serving files from: " << _resourcesDirectory.path();
|
||||
|
||||
// Scan for new files
|
||||
qDebug() << "Looking for new files in asset directory";
|
||||
auto files = _resourcesDirectory.entryInfoList(QDir::Files);
|
||||
QRegExp filenameRegex { "^[a-f0-9]{" + QString::number(SHA256_HASH_HEX_LENGTH) + "}(\\..+)?$" };
|
||||
for (const auto& fileInfo : files) {
|
||||
auto filename = fileInfo.fileName();
|
||||
if (!filenameRegex.exactMatch(filename)) {
|
||||
qDebug() << "Found file: " << filename;
|
||||
if (!fileInfo.isReadable()) {
|
||||
qDebug() << "\tCan't open file for reading: " << filename;
|
||||
continue;
|
||||
}
|
||||
// load whatever mappings we currently have from the local file
|
||||
loadMappingsFromFile();
|
||||
|
||||
// Read file
|
||||
QFile file { fileInfo.absoluteFilePath() };
|
||||
file.open(QFile::ReadOnly);
|
||||
QByteArray data = file.readAll();
|
||||
qInfo() << "Serving files from: " << _filesDirectory.path();
|
||||
|
||||
auto hash = hashData(data);
|
||||
auto hexHash = hash.toHex();
|
||||
// Check the asset directory to output some information about what we have
|
||||
auto files = _filesDirectory.entryList(QDir::Files);
|
||||
|
||||
qDebug() << "\tMoving " << filename << " to " << hexHash;
|
||||
|
||||
file.rename(_resourcesDirectory.absoluteFilePath(hexHash) + "." + fileInfo.suffix());
|
||||
}
|
||||
}
|
||||
QRegExp hashFileRegex { ASSET_HASH_REGEX_STRING };
|
||||
auto hashedFiles = files.filter(hashFileRegex);
|
||||
|
||||
qInfo() << "There are" << hashedFiles.size() << "asset files in the asset directory.";
|
||||
|
||||
performMappingMigration();
|
||||
|
||||
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
|
||||
}
|
||||
|
||||
void AssetServer::performMappingMigration() {
|
||||
QRegExp hashFileRegex { "^[a-f0-9]{" + QString::number(SHA256_HASH_HEX_LENGTH) + "}(\\.[\\w]+)+$" };
|
||||
|
||||
auto files = _resourcesDirectory.entryInfoList(QDir::Files);
|
||||
|
||||
for (const auto& fileInfo : files) {
|
||||
if (hashFileRegex.exactMatch(fileInfo.fileName())) {
|
||||
// we have a pre-mapping file that we should migrate to the new mapping system
|
||||
qDebug() << "Migrating pre-mapping file" << fileInfo.fileName();
|
||||
|
||||
// rename the file to the same name with no extension
|
||||
QFile oldFile { fileInfo.absoluteFilePath() };
|
||||
|
||||
auto oldAbsolutePath = fileInfo.absoluteFilePath();
|
||||
auto oldFilename = fileInfo.fileName();
|
||||
auto hash = oldFilename.left(SHA256_HASH_HEX_LENGTH);
|
||||
auto fullExtension = oldFilename.mid(oldFilename.indexOf('.'));
|
||||
|
||||
qDebug() << "\tMoving" << oldAbsolutePath << "to" << oldAbsolutePath.replace(fullExtension, "");
|
||||
|
||||
bool renamed = oldFile.copy(_filesDirectory.filePath(hash));
|
||||
if (!renamed) {
|
||||
qWarning() << "\tCould not migrate pre-mapping file" << fileInfo.fileName();
|
||||
} else {
|
||||
qDebug() << "\tRenamed pre-mapping file" << fileInfo.fileName();
|
||||
|
||||
// add a new mapping with the old extension and a truncated version of the hash
|
||||
const int TRUNCATED_HASH_NUM_CHAR = 16;
|
||||
auto fakeFileName = "/" + hash.left(TRUNCATED_HASH_NUM_CHAR) + fullExtension;
|
||||
|
||||
qDebug() << "\tAdding a migration mapping from" << fakeFileName << "to" << hash;
|
||||
|
||||
auto it = _fileMappings.find(fakeFileName);
|
||||
if (it == _fileMappings.end()) {
|
||||
_fileMappings[fakeFileName] = hash;
|
||||
|
||||
if (writeMappingsToFile()) {
|
||||
// mapping added and persisted, we can remove the migrated file
|
||||
oldFile.remove();
|
||||
qDebug() << "\tMigration completed for" << oldFilename;
|
||||
}
|
||||
} else {
|
||||
qDebug() << "\tCould not add migration mapping for" << hash << "since a mapping for" << fakeFileName
|
||||
<< "already exists.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AssetServer::handleAssetMappingOperation(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
MessageID messageID;
|
||||
message->readPrimitive(&messageID);
|
||||
|
||||
AssetMappingOperationType operationType;
|
||||
message->readPrimitive(&operationType);
|
||||
|
||||
auto replyPacket = NLPacketList::create(PacketType::AssetMappingOperationReply, QByteArray(), true, true);
|
||||
replyPacket->writePrimitive(messageID);
|
||||
|
||||
switch (operationType) {
|
||||
case AssetMappingOperationType::Get: {
|
||||
handleGetMappingOperation(*message, senderNode, *replyPacket);
|
||||
break;
|
||||
}
|
||||
case AssetMappingOperationType::GetAll: {
|
||||
handleGetAllMappingOperation(*message, senderNode, *replyPacket);
|
||||
break;
|
||||
}
|
||||
case AssetMappingOperationType::Set: {
|
||||
handleSetMappingOperation(*message, senderNode, *replyPacket);
|
||||
break;
|
||||
}
|
||||
case AssetMappingOperationType::Delete: {
|
||||
handleDeleteMappingsOperation(*message, senderNode, *replyPacket);
|
||||
break;
|
||||
}
|
||||
case AssetMappingOperationType::Rename: {
|
||||
handleRenameMappingOperation(*message, senderNode, *replyPacket);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->sendPacketList(std::move(replyPacket), *senderNode);
|
||||
}
|
||||
|
||||
void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
|
||||
QString assetPath = message.readString();
|
||||
|
||||
auto it = _fileMappings.find(assetPath);
|
||||
if (it != _fileMappings.end()) {
|
||||
auto assetHash = it->toString();
|
||||
replyPacket.writePrimitive(AssetServerError::NoError);
|
||||
replyPacket.write(QByteArray::fromHex(assetHash.toUtf8()));
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::AssetNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
|
||||
replyPacket.writePrimitive(AssetServerError::NoError);
|
||||
|
||||
auto count = _fileMappings.size();
|
||||
|
||||
replyPacket.writePrimitive(count);
|
||||
|
||||
for (auto it = _fileMappings.cbegin(); it != _fileMappings.cend(); ++ it) {
|
||||
replyPacket.writeString(it.key());
|
||||
replyPacket.write(QByteArray::fromHex(it.value().toString().toUtf8()));
|
||||
}
|
||||
}
|
||||
|
||||
void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
|
||||
if (senderNode->getCanRez()) {
|
||||
QString assetPath = message.readString();
|
||||
|
||||
auto assetHash = message.read(SHA256_HASH_LENGTH).toHex();
|
||||
|
||||
if (setMapping(assetPath, assetHash)) {
|
||||
replyPacket.writePrimitive(AssetServerError::NoError);
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::MappingOperationFailed);
|
||||
}
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::PermissionDenied);
|
||||
}
|
||||
}
|
||||
|
||||
void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
|
||||
if (senderNode->getCanRez()) {
|
||||
int numberOfDeletedMappings { 0 };
|
||||
message.readPrimitive(&numberOfDeletedMappings);
|
||||
|
||||
QStringList mappingsToDelete;
|
||||
|
||||
for (int i = 0; i < numberOfDeletedMappings; ++i) {
|
||||
mappingsToDelete << message.readString();
|
||||
}
|
||||
|
||||
if (deleteMappings(mappingsToDelete)) {
|
||||
replyPacket.writePrimitive(AssetServerError::NoError);
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::MappingOperationFailed);
|
||||
}
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::PermissionDenied);
|
||||
}
|
||||
}
|
||||
|
||||
void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
|
||||
if (senderNode->getCanRez()) {
|
||||
QString oldPath = message.readString();
|
||||
QString newPath = message.readString();
|
||||
|
||||
if (renameMapping(oldPath, newPath)) {
|
||||
replyPacket.writePrimitive(AssetServerError::NoError);
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::MappingOperationFailed);
|
||||
}
|
||||
} else {
|
||||
replyPacket.writePrimitive(AssetServerError::PermissionDenied);
|
||||
}
|
||||
}
|
||||
|
||||
void AssetServer::handleAssetGetInfo(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
QByteArray assetHash;
|
||||
MessageID messageID;
|
||||
uint8_t extensionLength;
|
||||
|
||||
if (message->getSize() < qint64(SHA256_HASH_LENGTH + sizeof(messageID) + sizeof(extensionLength))) {
|
||||
if (message->getSize() < qint64(SHA256_HASH_LENGTH + sizeof(messageID))) {
|
||||
qDebug() << "ERROR bad file request";
|
||||
return;
|
||||
}
|
||||
|
||||
message->readPrimitive(&messageID);
|
||||
assetHash = message->readWithoutCopy(SHA256_HASH_LENGTH);
|
||||
message->readPrimitive(&extensionLength);
|
||||
QByteArray extension = message->read(extensionLength);
|
||||
|
||||
auto replyPacket = NLPacket::create(PacketType::AssetGetInfoReply);
|
||||
|
||||
|
@ -183,8 +311,8 @@ void AssetServer::handleAssetGetInfo(QSharedPointer<ReceivedMessage> message, Sh
|
|||
replyPacket->writePrimitive(messageID);
|
||||
replyPacket->write(assetHash);
|
||||
|
||||
QString fileName = QString(hexHash) + "." + extension;
|
||||
QFileInfo fileInfo { _resourcesDirectory.filePath(fileName) };
|
||||
QString fileName = QString(hexHash);
|
||||
QFileInfo fileInfo { _filesDirectory.filePath(fileName) };
|
||||
|
||||
if (fileInfo.exists() && fileInfo.isReadable()) {
|
||||
qDebug() << "Opening file: " << fileInfo.filePath();
|
||||
|
@ -201,39 +329,39 @@ void AssetServer::handleAssetGetInfo(QSharedPointer<ReceivedMessage> message, Sh
|
|||
|
||||
void AssetServer::handleAssetGet(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
|
||||
auto minSize = qint64(sizeof(MessageID) + SHA256_HASH_LENGTH + sizeof(uint8_t) + sizeof(DataOffset) + sizeof(DataOffset));
|
||||
|
||||
auto minSize = qint64(sizeof(MessageID) + SHA256_HASH_LENGTH + sizeof(DataOffset) + sizeof(DataOffset));
|
||||
|
||||
if (message->getSize() < minSize) {
|
||||
qDebug() << "ERROR bad file request";
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue task
|
||||
auto task = new SendAssetTask(message, senderNode, _resourcesDirectory);
|
||||
auto task = new SendAssetTask(message, senderNode, _filesDirectory);
|
||||
_taskPool.start(task);
|
||||
}
|
||||
|
||||
void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
|
||||
|
||||
if (senderNode->getCanRez()) {
|
||||
qDebug() << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID());
|
||||
|
||||
auto task = new UploadAssetTask(message, senderNode, _resourcesDirectory);
|
||||
|
||||
auto task = new UploadAssetTask(message, senderNode, _filesDirectory);
|
||||
_taskPool.start(task);
|
||||
} else {
|
||||
// this is a node the domain told us is not allowed to rez entities
|
||||
// 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));
|
||||
|
||||
|
||||
MessageID messageID;
|
||||
message->readPrimitive(&messageID);
|
||||
|
||||
|
||||
// write the message ID and a permission denied error
|
||||
permissionErrorPacket->writePrimitive(messageID);
|
||||
permissionErrorPacket->writePrimitive(AssetServerError::PermissionDenied);
|
||||
|
||||
|
||||
// send off the packet
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->sendPacket(std::move(permissionErrorPacket), *senderNode);
|
||||
|
@ -242,19 +370,19 @@ void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, Sha
|
|||
|
||||
void AssetServer::sendStatsPacket() {
|
||||
QJsonObject serverStats;
|
||||
|
||||
|
||||
auto stats = DependencyManager::get<NodeList>()->sampleStatsForAllConnections();
|
||||
|
||||
|
||||
for (const auto& stat : stats) {
|
||||
QJsonObject nodeStats;
|
||||
auto endTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(stat.second.endTime);
|
||||
QDateTime date = QDateTime::fromMSecsSinceEpoch(endTimeMs.count());
|
||||
|
||||
|
||||
static const float USEC_PER_SEC = 1000000.0f;
|
||||
static const float MEGABITS_PER_BYTE = 8.0f / 1000000.0f; // Bytes => Mbits
|
||||
float elapsed = (float)(stat.second.endTime - stat.second.startTime).count() / USEC_PER_SEC; // sec
|
||||
float megabitsPerSecPerByte = MEGABITS_PER_BYTE / elapsed; // Bytes => Mb/s
|
||||
|
||||
|
||||
QJsonObject connectionStats;
|
||||
connectionStats["1. Last Heard"] = date.toString();
|
||||
connectionStats["2. Est. Max (P/s)"] = stat.second.estimatedBandwith;
|
||||
|
@ -264,10 +392,10 @@ void AssetServer::sendStatsPacket() {
|
|||
connectionStats["6. Up (Mb/s)"] = stat.second.sentBytes * megabitsPerSecPerByte;
|
||||
connectionStats["7. Down (Mb/s)"] = stat.second.receivedBytes * megabitsPerSecPerByte;
|
||||
nodeStats["Connection Stats"] = connectionStats;
|
||||
|
||||
|
||||
using Events = udt::ConnectionStats::Stats::Event;
|
||||
const auto& events = stat.second.events;
|
||||
|
||||
|
||||
QJsonObject upstreamStats;
|
||||
upstreamStats["1. Sent (P/s)"] = stat.second.sendRate;
|
||||
upstreamStats["2. Sent Packets"] = stat.second.sentPackets;
|
||||
|
@ -279,7 +407,7 @@ void AssetServer::sendStatsPacket() {
|
|||
upstreamStats["8. Sent ACK2"] = events[Events::SentACK2];
|
||||
upstreamStats["9. Retransmitted"] = events[Events::Retransmission];
|
||||
nodeStats["Upstream Stats"] = upstreamStats;
|
||||
|
||||
|
||||
QJsonObject downstreamStats;
|
||||
downstreamStats["1. Recvd (P/s)"] = stat.second.receiveRate;
|
||||
downstreamStats["2. Recvd Packets"] = stat.second.receivedPackets;
|
||||
|
@ -290,7 +418,7 @@ void AssetServer::sendStatsPacket() {
|
|||
downstreamStats["7. Recvd ACK2"] = events[Events::ReceivedACK2];
|
||||
downstreamStats["8. Duplicates"] = events[Events::Duplicate];
|
||||
nodeStats["Downstream Stats"] = downstreamStats;
|
||||
|
||||
|
||||
QString uuid;
|
||||
auto nodelist = DependencyManager::get<NodeList>();
|
||||
if (stat.first == nodelist->getDomainHandler().getSockAddr()) {
|
||||
|
@ -301,10 +429,274 @@ void AssetServer::sendStatsPacket() {
|
|||
uuid = uuidStringWithoutCurlyBraces(node ? node->getUUID() : QUuid());
|
||||
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuid;
|
||||
}
|
||||
|
||||
|
||||
serverStats[uuid] = nodeStats;
|
||||
}
|
||||
|
||||
|
||||
// send off the stats packets
|
||||
ThreadedAssignment::addPacketStatsAndSendStatsPacket(serverStats);
|
||||
}
|
||||
|
||||
static const QString MAP_FILE_NAME = "map.json";
|
||||
|
||||
void AssetServer::loadMappingsFromFile() {
|
||||
|
||||
auto mapFilePath = _resourcesDirectory.absoluteFilePath(MAP_FILE_NAME);
|
||||
|
||||
QFile mapFile { mapFilePath };
|
||||
if (mapFile.exists()) {
|
||||
if (mapFile.open(QIODevice::ReadOnly)) {
|
||||
QJsonParseError error;
|
||||
|
||||
auto jsonDocument = QJsonDocument::fromJson(mapFile.readAll(), &error);
|
||||
|
||||
if (error.error == QJsonParseError::NoError) {
|
||||
_fileMappings = jsonDocument.object().toVariantHash();
|
||||
|
||||
// remove any mappings that don't match the expected format
|
||||
auto it = _fileMappings.begin();
|
||||
while (it != _fileMappings.end()) {
|
||||
bool shouldDrop = false;
|
||||
|
||||
if (!isValidPath(it.key())) {
|
||||
qWarning() << "Will not keep mapping for" << it.key() << "since it is not a valid path.";
|
||||
shouldDrop = true;
|
||||
}
|
||||
|
||||
if (!isValidHash(it.value().toString())) {
|
||||
qWarning() << "Will not keep mapping for" << it.key() << "since it does not have a valid hash.";
|
||||
shouldDrop = true;
|
||||
}
|
||||
|
||||
if (shouldDrop) {
|
||||
it = _fileMappings.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
qInfo() << "Loaded" << _fileMappings.count() << "mappings from map file at" << mapFilePath;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
qCritical() << "Failed to read mapping file at" << mapFilePath << "- assignment will not continue.";
|
||||
setFinished(true);
|
||||
} else {
|
||||
qInfo() << "No existing mappings loaded from file since no file was found at" << mapFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetServer::writeMappingsToFile() {
|
||||
auto mapFilePath = _resourcesDirectory.absoluteFilePath(MAP_FILE_NAME);
|
||||
|
||||
QFile mapFile { mapFilePath };
|
||||
if (mapFile.open(QIODevice::WriteOnly)) {
|
||||
auto jsonObject = QJsonObject::fromVariantHash(_fileMappings);
|
||||
QJsonDocument jsonDocument { jsonObject };
|
||||
|
||||
if (mapFile.write(jsonDocument.toJson()) != -1) {
|
||||
qDebug() << "Wrote JSON mappings to file at" << mapFilePath;
|
||||
return true;
|
||||
} else {
|
||||
qWarning() << "Failed to write JSON mappings to file at" << mapFilePath;
|
||||
}
|
||||
} else {
|
||||
qWarning() << "Failed to open map file at" << mapFilePath;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AssetServer::setMapping(AssetPath path, AssetHash hash) {
|
||||
path = path.trimmed();
|
||||
|
||||
if (!isValidPath(path)) {
|
||||
qWarning() << "Cannot set a mapping for invalid path:" << path << "=>" << hash;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidHash(hash)) {
|
||||
qWarning() << "Cannot set a mapping for invalid hash" << path << "=>" << hash;
|
||||
return false;
|
||||
}
|
||||
|
||||
// remember what the old mapping was in case persistence fails
|
||||
auto oldMapping = _fileMappings.value(path).toString();
|
||||
|
||||
// update the in memory QHash
|
||||
_fileMappings[path] = hash;
|
||||
|
||||
// attempt to write to file
|
||||
if (writeMappingsToFile()) {
|
||||
// persistence succeeded, we are good to go
|
||||
qDebug() << "Set mapping:" << path << "=>" << hash;
|
||||
return true;
|
||||
} else {
|
||||
// failed to persist this mapping to file - put back the old one in our in-memory representation
|
||||
if (oldMapping.isEmpty()) {
|
||||
_fileMappings.remove(path);
|
||||
} else {
|
||||
_fileMappings[path] = oldMapping;
|
||||
}
|
||||
|
||||
qWarning() << "Failed to persist mapping:" << path << "=>" << hash;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool pathIsFolder(const AssetPath& path) {
|
||||
return path.endsWith('/');
|
||||
}
|
||||
|
||||
bool AssetServer::deleteMappings(AssetPathList& paths) {
|
||||
// take a copy of the current mappings in case persistence of these deletes fails
|
||||
auto oldMappings = _fileMappings;
|
||||
|
||||
// enumerate the paths to delete and remove them all
|
||||
for (auto& path : paths) {
|
||||
|
||||
path = path.trimmed();
|
||||
|
||||
// figure out if this path will delete a file or folder
|
||||
if (pathIsFolder(path)) {
|
||||
// enumerate the in memory file mappings and remove anything that matches
|
||||
auto it = _fileMappings.begin();
|
||||
auto sizeBefore = _fileMappings.size();
|
||||
|
||||
while (it != _fileMappings.end()) {
|
||||
if (it.key().startsWith(path)) {
|
||||
it = _fileMappings.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
auto sizeNow = _fileMappings.size();
|
||||
if (sizeBefore != sizeNow) {
|
||||
qDebug() << "Deleted" << sizeBefore - sizeNow << "mappings in folder: " << path;
|
||||
} else {
|
||||
qDebug() << "Did not find any mappings to delete in folder:" << path;
|
||||
}
|
||||
|
||||
} else {
|
||||
auto oldMapping = _fileMappings.take(path);
|
||||
if (!oldMapping.isNull()) {
|
||||
qDebug() << "Deleted a mapping:" << path << "=>" << oldMapping.toString();
|
||||
} else {
|
||||
qDebug() << "Unable to delete a mapping that was not found:" << path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deleted the old mappings, attempt to persist to file
|
||||
if (writeMappingsToFile()) {
|
||||
// persistence succeeded we are good to go
|
||||
return true;
|
||||
} else {
|
||||
qWarning() << "Failed to persist deleted mappings, rolling back";
|
||||
|
||||
// we didn't delete the previous mapping, put it back in our in-memory representation
|
||||
_fileMappings = oldMappings;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) {
|
||||
oldPath = oldPath.trimmed();
|
||||
newPath = newPath.trimmed();
|
||||
|
||||
if (!isValidPath(oldPath) || !isValidPath(newPath)) {
|
||||
qWarning() << "Cannot perform rename with invalid paths - both should have leading forward slashes:"
|
||||
<< oldPath << "=>" << newPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// figure out if this rename is for a file or folder
|
||||
if (pathIsFolder(oldPath)) {
|
||||
if (!pathIsFolder(newPath)) {
|
||||
// we were asked to rename a path to a folder to a path that isn't a folder, this is a fail
|
||||
qWarning() << "Cannot rename mapping from folder path" << oldPath << "to file path" << newPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// take a copy of the old mappings
|
||||
auto oldMappings = _fileMappings;
|
||||
|
||||
// iterate the current mappings and adjust any that matches the renamed folder
|
||||
auto it = oldMappings.begin();
|
||||
|
||||
while (it != oldMappings.end()) {
|
||||
if (it.key().startsWith(oldPath)) {
|
||||
auto newKey = it.key();
|
||||
newKey.replace(0, oldPath.size(), newPath);
|
||||
|
||||
// remove the old version from the in memory file mappings
|
||||
_fileMappings.remove(it.key());
|
||||
_fileMappings.insert(newKey, it.value());
|
||||
}
|
||||
|
||||
++it;
|
||||
}
|
||||
|
||||
if (writeMappingsToFile()) {
|
||||
// persisted the changed mappings, return success
|
||||
qDebug() << "Renamed folder mapping:" << oldPath << "=>" << newPath;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// couldn't persist the renamed paths, rollback and return failure
|
||||
_fileMappings = oldMappings;
|
||||
|
||||
qWarning() << "Failed to persist renamed folder mapping:" << oldPath << "=>" << newPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (pathIsFolder(newPath)) {
|
||||
// we were asked to rename a path to a file to a path that is a folder, this is a fail
|
||||
qWarning() << "Cannot rename mapping from file path" << oldPath << "to folder path" << newPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// take the old hash to remove the old mapping
|
||||
auto oldSourceMapping = _fileMappings.take(oldPath).toString();
|
||||
|
||||
// in case we're overwriting, keep the current destination mapping for potential rollback
|
||||
auto oldDestinationMapping = _fileMappings.value(newPath);
|
||||
|
||||
if (!oldSourceMapping.isEmpty()) {
|
||||
_fileMappings[newPath] = oldSourceMapping;
|
||||
|
||||
if (writeMappingsToFile()) {
|
||||
// persisted the renamed mapping, return success
|
||||
qDebug() << "Renamed mapping:" << oldPath << "=>" << newPath;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
// we couldn't persist the renamed mapping, rollback and return failure
|
||||
_fileMappings[oldPath] = oldSourceMapping;
|
||||
|
||||
if (!oldDestinationMapping.isNull()) {
|
||||
// put back the overwritten mapping for the destination path
|
||||
_fileMappings[newPath] = oldDestinationMapping.toString();
|
||||
} else {
|
||||
// clear the new mapping
|
||||
_fileMappings.remove(newPath);
|
||||
}
|
||||
|
||||
qDebug() << "Failed to persist renamed mapping:" << oldPath << "=>" << newPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// failed to find a mapping that was to be renamed, return failure
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
#ifndef hifi_AssetServer_h
|
||||
#define hifi_AssetServer_h
|
||||
|
||||
#include <QDir>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QThreadPool>
|
||||
|
||||
#include <ThreadedAssignment.h>
|
||||
#include <QThreadPool>
|
||||
|
||||
#include "AssetUtils.h"
|
||||
#include "ReceivedMessage.h"
|
||||
|
@ -34,17 +34,39 @@ private slots:
|
|||
void handleAssetGetInfo(QSharedPointer<ReceivedMessage> packet, SharedNodePointer senderNode);
|
||||
void handleAssetGet(QSharedPointer<ReceivedMessage> packet, SharedNodePointer senderNode);
|
||||
void handleAssetUpload(QSharedPointer<ReceivedMessage> packetList, SharedNodePointer senderNode);
|
||||
void handleAssetMappingOperation(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
|
||||
void sendStatsPacket();
|
||||
|
||||
private:
|
||||
static void writeError(NLPacketList* packetList, AssetServerError error);
|
||||
using Mappings = QVariantHash;
|
||||
|
||||
void handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
void handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
void handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
void handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
void handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
|
||||
|
||||
// Mapping file operations must be called from main assignment thread only
|
||||
void loadMappingsFromFile();
|
||||
bool writeMappingsToFile();
|
||||
|
||||
/// Set the mapping for path to hash
|
||||
bool setMapping(AssetPath path, AssetHash hash);
|
||||
|
||||
/// Delete mapping `path`. Returns `true` if deletion of mappings succeeds, else `false`.
|
||||
bool deleteMappings(AssetPathList& paths);
|
||||
|
||||
/// Rename mapping from `oldPath` to `newPath`. Returns true if successful
|
||||
bool renameMapping(AssetPath oldPath, AssetPath newPath);
|
||||
|
||||
void performMappingMigration();
|
||||
|
||||
Mappings _fileMappings;
|
||||
|
||||
QDir _resourcesDirectory;
|
||||
QDir _filesDirectory;
|
||||
QThreadPool _taskPool;
|
||||
};
|
||||
|
||||
inline void writeError(NLPacketList* packetList, AssetServerError error) {
|
||||
packetList->writePrimitive(error);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -33,13 +33,10 @@ SendAssetTask::SendAssetTask(QSharedPointer<ReceivedMessage> message, const Shar
|
|||
|
||||
void SendAssetTask::run() {
|
||||
MessageID messageID;
|
||||
uint8_t extensionLength;
|
||||
DataOffset start, end;
|
||||
|
||||
_message->readPrimitive(&messageID);
|
||||
QByteArray assetHash = _message->read(SHA256_HASH_LENGTH);
|
||||
_message->readPrimitive(&extensionLength);
|
||||
QByteArray extension = _message->read(extensionLength);
|
||||
|
||||
// `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,
|
||||
|
@ -59,15 +56,15 @@ void SendAssetTask::run() {
|
|||
replyPacketList->writePrimitive(messageID);
|
||||
|
||||
if (end <= start) {
|
||||
writeError(replyPacketList.get(), AssetServerError::InvalidByteRange);
|
||||
replyPacketList->writePrimitive(AssetServerError::InvalidByteRange);
|
||||
} else {
|
||||
QString filePath = _resourcesDir.filePath(QString(hexHash) + "." + QString(extension));
|
||||
QString filePath = _resourcesDir.filePath(QString(hexHash));
|
||||
|
||||
QFile file { filePath };
|
||||
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
if (file.size() < end) {
|
||||
writeError(replyPacketList.get(), AssetServerError::InvalidByteRange);
|
||||
replyPacketList->writePrimitive(AssetServerError::InvalidByteRange);
|
||||
qCDebug(networking) << "Bad byte range: " << hexHash << " " << start << ":" << end;
|
||||
} else {
|
||||
auto size = end - start;
|
||||
|
@ -80,7 +77,7 @@ void SendAssetTask::run() {
|
|||
file.close();
|
||||
} else {
|
||||
qCDebug(networking) << "Asset not found: " << filePath << "(" << hexHash << ")";
|
||||
writeError(replyPacketList.get(), AssetServerError::AssetNotFound);
|
||||
replyPacketList->writePrimitive(AssetServerError::AssetNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,15 +37,10 @@ void UploadAssetTask::run() {
|
|||
MessageID messageID;
|
||||
buffer.read(reinterpret_cast<char*>(&messageID), sizeof(messageID));
|
||||
|
||||
uint8_t extensionLength;
|
||||
buffer.read(reinterpret_cast<char*>(&extensionLength), sizeof(extensionLength));
|
||||
|
||||
QByteArray extension = buffer.read(extensionLength);
|
||||
|
||||
uint64_t fileSize;
|
||||
buffer.read(reinterpret_cast<char*>(&fileSize), sizeof(fileSize));
|
||||
|
||||
qDebug() << "UploadAssetTask reading a file of " << fileSize << "bytes and extension" << extension << "from"
|
||||
qDebug() << "UploadAssetTask reading a file of " << fileSize << "bytes from"
|
||||
<< uuidStringWithoutCurlyBraces(_senderNode->getUUID());
|
||||
|
||||
auto replyPacket = NLPacket::create(PacketType::AssetUploadReply);
|
||||
|
@ -62,17 +57,47 @@ void UploadAssetTask::run() {
|
|||
qDebug() << "Hash for uploaded file from" << uuidStringWithoutCurlyBraces(_senderNode->getUUID())
|
||||
<< "is: (" << hexHash << ") ";
|
||||
|
||||
QFile file { _resourcesDir.filePath(QString(hexHash)) + "." + QString(extension) };
|
||||
QFile file { _resourcesDir.filePath(QString(hexHash)) };
|
||||
|
||||
bool existingCorrectFile = false;
|
||||
|
||||
if (file.exists()) {
|
||||
qDebug() << "[WARNING] This file already exists: " << hexHash;
|
||||
} else {
|
||||
file.open(QIODevice::WriteOnly);
|
||||
file.write(fileData);
|
||||
file.close();
|
||||
// check if the local file has the correct contents, otherwise we overwrite
|
||||
if (file.open(QIODevice::ReadOnly) && hashData(file.readAll()) == hash) {
|
||||
qDebug() << "Not overwriting existing verified file: " << hexHash;
|
||||
|
||||
existingCorrectFile = true;
|
||||
|
||||
replyPacket->writePrimitive(AssetServerError::NoError);
|
||||
replyPacket->write(hash);
|
||||
} else {
|
||||
qDebug() << "Overwriting an existing file whose contents did not match the expected hash: " << hexHash;
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
replyPacket->writePrimitive(AssetServerError::NoError);
|
||||
replyPacket->write(hash);
|
||||
|
||||
if (!existingCorrectFile) {
|
||||
if (file.open(QIODevice::WriteOnly) && file.write(fileData) == qint64(fileSize)) {
|
||||
qDebug() << "Wrote file" << hexHash << "to disk. Upload complete";
|
||||
file.close();
|
||||
|
||||
replyPacket->writePrimitive(AssetServerError::NoError);
|
||||
replyPacket->write(hash);
|
||||
} else {
|
||||
qWarning() << "Failed to upload or write to file" << hexHash << " - upload failed.";
|
||||
|
||||
// upload has failed - remove the file and return an error
|
||||
auto removed = file.remove();
|
||||
|
||||
if (!removed) {
|
||||
qWarning() << "Removal of failed upload file" << hexHash << "failed.";
|
||||
}
|
||||
|
||||
replyPacket->writePrimitive(AssetServerError::FileOperationFailed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
|
|
@ -213,7 +213,6 @@ function AttachedEntitiesManager() {
|
|||
var props = Entities.getEntityProperties(entityID);
|
||||
if (props.parentID == MyAvatar.sessionUUID) {
|
||||
grabData = getEntityCustomData('grabKey', entityID, {});
|
||||
grabbableData = getEntityCustomData('grabbableKey', entityID, {});
|
||||
var wearableData = getEntityCustomData('wearable', entityID, DEFAULT_WEARABLE_DATA);
|
||||
var currentJointName = MyAvatar.getJointNames()[props.parentJointIndex];
|
||||
wearableData.joints[currentJointName] = [props.localPosition, props.localRotation];
|
||||
|
|
|
@ -1006,7 +1006,7 @@ function MyController(hand) {
|
|||
|
||||
// else this thing isn't physical. grab it by reparenting it (but not if we've already
|
||||
// grabbed it).
|
||||
if (grabbableData.refCount < 1) {
|
||||
if (refCount < 1) {
|
||||
this.setState(this.state == STATE_SEARCHING ? STATE_NEAR_GRABBING : STATE_EQUIP);
|
||||
return;
|
||||
} else {
|
||||
|
@ -1120,7 +1120,6 @@ function MyController(hand) {
|
|||
var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation);
|
||||
|
||||
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES);
|
||||
var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA);
|
||||
|
||||
if (this.state == STATE_CONTINUE_DISTANCE_HOLDING && this.bumperSqueezed() &&
|
||||
this.hasPresetOffsets()) {
|
||||
|
@ -1307,7 +1306,6 @@ function MyController(hand) {
|
|||
|
||||
this.nearGrabbing = function() {
|
||||
var now = Date.now();
|
||||
var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA);
|
||||
|
||||
if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) {
|
||||
this.setState(STATE_RELEASE);
|
||||
|
@ -1330,10 +1328,9 @@ function MyController(hand) {
|
|||
var handRotation = (this.hand === RIGHT_HAND) ? MyAvatar.getRightPalmRotation() : MyAvatar.getLeftPalmRotation();
|
||||
var handPosition = this.getHandPosition();
|
||||
|
||||
var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA);
|
||||
|
||||
var hasPresetPosition = false;
|
||||
if (this.state != STATE_NEAR_GRABBING && this.hasPresetOffsets()) {
|
||||
var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA);
|
||||
// if an object is "equipped" and has a predefined offset, use it.
|
||||
this.ignoreIK = grabbableData.ignoreIK ? grabbableData.ignoreIK : false;
|
||||
this.offsetPosition = this.getPresetPosition();
|
||||
|
@ -1676,7 +1673,6 @@ function MyController(hand) {
|
|||
};
|
||||
|
||||
this.activateEntity = function(entityID, grabbedProperties, wasLoaded) {
|
||||
var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, entityID, DEFAULT_GRABBABLE_DATA);
|
||||
var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {});
|
||||
var now = Date.now();
|
||||
|
||||
|
|
60
examples/data_visualization/photo_sphere.js
Normal file
60
examples/data_visualization/photo_sphere.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
// photo_sphere.js
|
||||
//
|
||||
// Created by James B. Pollack @imgntn on 3/11/2015
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// This script creates a photo sphere around you.
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
var photoSphere, light;
|
||||
|
||||
//equirectangular
|
||||
var url = 'http://hifi-content.s3.amazonaws.com/james/projection_objects/IMG_9167.JPG';
|
||||
|
||||
var MODEL_URL = 'http://hifi-content.s3.amazonaws.com/james/projection_objects/photosphere2.fbx';
|
||||
|
||||
function createPhotoSphere() {
|
||||
|
||||
var textureString = 'photo:"' + url + '"'
|
||||
|
||||
var properties = {
|
||||
type: 'Model',
|
||||
modelURL: MODEL_URL,
|
||||
name: 'hifi-photo-sphere',
|
||||
dimensions: {
|
||||
x: 32,
|
||||
y: 32,
|
||||
z: 32
|
||||
},
|
||||
position: MyAvatar.position,
|
||||
textures: textureString
|
||||
}
|
||||
photoSphere = Entities.addEntity(properties);
|
||||
}
|
||||
|
||||
function createLight() {
|
||||
var properties = {
|
||||
name: 'hifi-photo-sphere-light',
|
||||
type: 'Light',
|
||||
dimensions: {
|
||||
x: 36,
|
||||
y: 36,
|
||||
z: 36,
|
||||
},
|
||||
intensity: 4.0,
|
||||
falloffRadius: 22,
|
||||
position: MyAvatar.position
|
||||
}
|
||||
light = Entities.addEntity(properties);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
Entities.deleteEntity(photoSphere);
|
||||
Entities.deleteEntity(light);
|
||||
}
|
||||
|
||||
Script.scriptEnding.connect(cleanup);
|
||||
createPhotoSphere();
|
||||
createLight();
|
|
@ -243,6 +243,41 @@
|
|||
|
||||
}
|
||||
|
||||
function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, defaultValue) {
|
||||
var properties = {};
|
||||
var parsedData = {};
|
||||
try {
|
||||
parsedData = JSON.parse(userDataElement.value);
|
||||
} catch(e) {}
|
||||
|
||||
if (!(groupName in parsedData)) {
|
||||
parsedData[groupName] = {}
|
||||
}
|
||||
delete parsedData[groupName][keyName];
|
||||
if (checkBoxElement.checked !== defaultValue) {
|
||||
parsedData[groupName][keyName] = checkBoxElement.checked;
|
||||
}
|
||||
|
||||
if (Object.keys(parsedData[groupName]).length == 0) {
|
||||
delete parsedData[groupName];
|
||||
}
|
||||
if (Object.keys(parsedData).length > 0) {
|
||||
properties['userData'] = JSON.stringify(parsedData);
|
||||
} else {
|
||||
properties['userData'] = '';
|
||||
}
|
||||
|
||||
userDataElement.value = properties['userData'];
|
||||
|
||||
EventBridge.emitWebEvent(
|
||||
JSON.stringify({
|
||||
type: "update",
|
||||
properties: properties,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function loaded() {
|
||||
openEventBridge(function() {
|
||||
var allSections = [];
|
||||
|
@ -305,6 +340,11 @@
|
|||
var elCollideMyAvatar = document.getElementById("property-collide-myAvatar");
|
||||
var elCollideOtherAvatar = document.getElementById("property-collide-otherAvatar");
|
||||
var elCollisionSoundURL = document.getElementById("property-collision-sound-url");
|
||||
|
||||
var elGrabbable = document.getElementById("property-grabbable");
|
||||
var elWantsTrigger = document.getElementById("property-wants-trigger");
|
||||
var elIgnoreIK = document.getElementById("property-ignore-ik");
|
||||
|
||||
var elLifetime = document.getElementById("property-lifetime");
|
||||
var elScriptURL = document.getElementById("property-script-url");
|
||||
var elScriptTimestamp = document.getElementById("property-script-timestamp");
|
||||
|
@ -408,7 +448,7 @@
|
|||
var elXTextureURL = document.getElementById("property-x-texture-url");
|
||||
var elYTextureURL = document.getElementById("property-y-texture-url");
|
||||
var elZTextureURL = document.getElementById("property-z-texture-url");
|
||||
|
||||
|
||||
var elPreviewCameraButton = document.getElementById("preview-camera-button");
|
||||
|
||||
if (window.EventBridge !== undefined) {
|
||||
|
@ -518,13 +558,30 @@
|
|||
elCollisionless.checked = properties.collisionless;
|
||||
elDynamic.checked = properties.dynamic;
|
||||
|
||||
|
||||
elCollideStatic.checked = properties.collidesWith.indexOf("static") > -1;
|
||||
elCollideKinematic.checked = properties.collidesWith.indexOf("kinematic") > -1;
|
||||
elCollideDynamic.checked = properties.collidesWith.indexOf("dynamic") > -1;
|
||||
elCollideMyAvatar.checked = properties.collidesWith.indexOf("myAvatar") > -1;
|
||||
elCollideOtherAvatar.checked = properties.collidesWith.indexOf("otherAvatar") > -1;
|
||||
|
||||
elGrabbable.checked = properties.dynamic;
|
||||
elWantsTrigger.checked = false;
|
||||
elIgnoreIK.checked = false;
|
||||
var parsedUserData = {}
|
||||
try {
|
||||
parsedUserData = JSON.parse(properties.userData);
|
||||
} catch(e) {}
|
||||
if ("grabbableKey" in parsedUserData) {
|
||||
if ("grabbable" in parsedUserData["grabbableKey"]) {
|
||||
elGrabbable.checked = parsedUserData["grabbableKey"].grabbable;
|
||||
}
|
||||
if ("wantsTrigger" in parsedUserData["grabbableKey"]) {
|
||||
elWantsTrigger.checked = parsedUserData["grabbableKey"].wantsTrigger;
|
||||
}
|
||||
if ("ignoreIK" in parsedUserData["grabbableKey"]) {
|
||||
elIgnoreIK.checked = parsedUserData["grabbableKey"].ignoreIK;
|
||||
}
|
||||
}
|
||||
|
||||
elCollisionSoundURL.value = properties.collisionSoundURL;
|
||||
elLifetime.value = properties.lifetime;
|
||||
|
@ -737,9 +794,6 @@
|
|||
elCollisionless.addEventListener('change', createEmitCheckedPropertyUpdateFunction('collisionless'));
|
||||
elDynamic.addEventListener('change', createEmitCheckedPropertyUpdateFunction('dynamic'));
|
||||
|
||||
|
||||
|
||||
|
||||
elCollideDynamic.addEventListener('change', function() {
|
||||
updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideDynamic, 'dynamic');
|
||||
});
|
||||
|
@ -758,6 +812,15 @@
|
|||
updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideOtherAvatar, 'otherAvatar');
|
||||
});
|
||||
|
||||
elGrabbable.addEventListener('change', function() {
|
||||
userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, properties.dynamic);
|
||||
});
|
||||
elWantsTrigger.addEventListener('change', function() {
|
||||
userDataChanger("grabbableKey", "wantsTrigger", elWantsTrigger, elUserData, false);
|
||||
});
|
||||
elIgnoreIK.addEventListener('change', function() {
|
||||
userDataChanger("grabbableKey", "ignoreIK", elIgnoreIK, elUserData, false);
|
||||
});
|
||||
|
||||
elCollisionSoundURL.addEventListener('change', createEmitTextPropertyUpdateFunction('collisionSoundURL'));
|
||||
|
||||
|
@ -954,7 +1017,7 @@
|
|||
action: "previewCamera"
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
window.onblur = function() {
|
||||
// Fake a change event
|
||||
var ev = document.createEvent("HTMLEvents");
|
||||
|
@ -1476,6 +1539,29 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class = "sub-section-header"> Grabbable: </div>
|
||||
<div class = "sub-props-checkbox-group">
|
||||
<div class="property">
|
||||
<span class="label">grabbable</span>
|
||||
<span class="value">
|
||||
<input type='checkbox' id="property-grabbable">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<span class="label">triggerable</span>
|
||||
<span class="value">
|
||||
<input type='checkbox' id="property-wants-trigger">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="property">
|
||||
<span class="label">ignore inverse-kinematics</span>
|
||||
<span class="value">
|
||||
<input type='checkbox' id="property-ignore-ik">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
|
|
@ -36,11 +36,11 @@ var AUDIO_LISTENER_MODE_CUSTOM = "Audio from custom position";
|
|||
|
||||
// be sure that the audio listener options are in the right order (same as the enumerator)
|
||||
var AUDIO_LISTENER_OPTIONS = [
|
||||
// MyAvatar.FROM_HEAD (0)
|
||||
// MyAvatar.audioListenerModeHead (0)
|
||||
AUDIO_LISTENER_MODE_FROM_HEAD,
|
||||
// MyAvatar.FROM_CAMERA (1)
|
||||
// MyAvatar.audioListenerModeCamera (1)
|
||||
AUDIO_LISTENER_MODE_FROM_CAMERA,
|
||||
// MyAvatar.CUSTOM (2)
|
||||
// MyAvatar.audioListenerCustom (2)
|
||||
AUDIO_LISTENER_MODE_CUSTOM
|
||||
];
|
||||
var AUDIO_STEREO_INPUT = "Stereo Input";
|
||||
|
|
22
examples/utilities/tools/render/BG.qml
Normal file
22
examples/utilities/tools/render/BG.qml
Normal file
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// BG.qml
|
||||
// examples/utilities/tools/render
|
||||
//
|
||||
// Created by Zach Pomerantz on 2/8/2016
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
|
||||
Item {
|
||||
Timer {
|
||||
running: true; repeat: true
|
||||
onTriggered: time.text = Render.getConfig("DrawBackgroundDeferred").gpuTime
|
||||
}
|
||||
|
||||
Text { id: time; font.pointSize: 20 }
|
||||
}
|
||||
|
21
examples/utilities/tools/render/debugBG.js
Normal file
21
examples/utilities/tools/render/debugBG.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// debugBG.js
|
||||
// examples/utilities/tools/render
|
||||
//
|
||||
// Zach Pomerantz, created on 1/27/2016.
|
||||
// Copyright 2016 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
|
||||
//
|
||||
|
||||
// Set up the qml ui
|
||||
var qml = Script.resolvePath('BG.qml');
|
||||
var window = new OverlayWindow({
|
||||
title: 'Background Timer',
|
||||
source: qml,
|
||||
width: 300
|
||||
});
|
||||
window.setPosition(25, 50);
|
||||
window.closed.connect(function() { Script.stop(); });
|
||||
|
520
interface/resources/qml/AssetServer.qml
Normal file
520
interface/resources/qml/AssetServer.qml
Normal file
|
@ -0,0 +1,520 @@
|
|||
//
|
||||
// AssetServer.qml
|
||||
//
|
||||
// Created by Clement on 3/1/16
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Dialogs 1.2 as OriginalDialogs
|
||||
import Qt.labs.settings 1.0
|
||||
|
||||
import "styles-uit"
|
||||
import "controls-uit" as HifiControls
|
||||
import "windows-uit"
|
||||
import "dialogs"
|
||||
|
||||
Window {
|
||||
id: root
|
||||
objectName: "AssetServer"
|
||||
title: "My Asset Server"
|
||||
resizable: true
|
||||
destroyOnInvisible: true
|
||||
x: 40; y: 40
|
||||
implicitWidth: 384; implicitHeight: 640
|
||||
minSize: Qt.vector2d(200, 300)
|
||||
|
||||
property int colorScheme: hifi.colorSchemes.dark
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
property var scripts: ScriptDiscoveryService;
|
||||
property var assetProxyModel: Assets.proxyModel;
|
||||
property var assetMappingsModel: Assets.mappingModel;
|
||||
property var currentDirectory;
|
||||
|
||||
Settings {
|
||||
category: "Overlay.AssetServer"
|
||||
property alias x: root.x
|
||||
property alias y: root.y
|
||||
property alias directory: root.currentDirectory
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
ApplicationInterface.uploadRequest.connect(uploadClicked);
|
||||
assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError);
|
||||
reload();
|
||||
}
|
||||
|
||||
function doDeleteFile(path) {
|
||||
console.log("Deleting " + path);
|
||||
|
||||
Assets.deleteMappings(path, function(err) {
|
||||
if (err) {
|
||||
console.log("Asset browser - error deleting path: ", path, err);
|
||||
|
||||
box = errorMessageBox("There was an error deleting:\n" + path + "\n" + err);
|
||||
box.selected.connect(reload);
|
||||
} else {
|
||||
console.log("Asset browser - finished deleting path: ", path);
|
||||
reload();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function doRenameFile(oldPath, newPath) {
|
||||
|
||||
if (newPath[0] != "/") {
|
||||
newPath = "/" + newPath;
|
||||
}
|
||||
|
||||
if (oldPath[oldPath.length - 1] == '/' && newPath[newPath.length - 1] != '/') {
|
||||
// this is a folder rename but the user neglected to add a trailing slash when providing a new path
|
||||
newPath = newPath + "/";
|
||||
}
|
||||
|
||||
if (Assets.isKnownFolder(newPath)) {
|
||||
box = errorMessageBox("Cannot overwrite existing directory.");
|
||||
box.selected.connect(reload);
|
||||
}
|
||||
|
||||
console.log("Asset browser - renaming " + oldPath + " to " + newPath);
|
||||
|
||||
Assets.renameMapping(oldPath, newPath, function(err) {
|
||||
if (err) {
|
||||
console.log("Asset browser - error renaming: ", oldPath, "=>", newPath, " - error ", err);
|
||||
box = errorMessageBox("There was an error renaming:\n" + oldPath + " to " + newPath + "\n" + err);
|
||||
box.selected.connect(reload);
|
||||
} else {
|
||||
console.log("Asset browser - finished rename: ", oldPath, "=>", newPath);
|
||||
}
|
||||
|
||||
reload();
|
||||
});
|
||||
}
|
||||
|
||||
function fileExists(path) {
|
||||
return Assets.isKnownMapping(path);
|
||||
}
|
||||
|
||||
function askForOverwrite(path, callback) {
|
||||
var object = desktop.messageBox({
|
||||
icon: hifi.icons.question,
|
||||
buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No,
|
||||
defaultButton: OriginalDialogs.StandardButton.No,
|
||||
title: "Overwrite File",
|
||||
text: path + "\n" + "This file already exists. Do you want to overwrite it?"
|
||||
});
|
||||
object.selected.connect(function(button) {
|
||||
if (button === OriginalDialogs.StandardButton.Yes) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function canAddToWorld(path) {
|
||||
var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i];
|
||||
|
||||
return supportedExtensions.reduce(function(total, current) {
|
||||
return total | new RegExp(current).test(path);
|
||||
}, false);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
Assets.mappingModel.refresh();
|
||||
treeView.selection.clear();
|
||||
}
|
||||
|
||||
function handleGetMappingsError(errorString) {
|
||||
errorMessageBox(
|
||||
"There was a problem retreiving the list of assets from your Asset Server.\n"
|
||||
+ errorString
|
||||
);
|
||||
}
|
||||
|
||||
function addToWorld() {
|
||||
var url = assetProxyModel.data(treeView.selection.currentIndex, 0x103);
|
||||
|
||||
if (!url || !canAddToWorld(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var name = assetProxyModel.data(treeView.selection.currentIndex);
|
||||
|
||||
console.log("Asset browser - adding asset " + url + " (" + name + ") to world.");
|
||||
|
||||
var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getFront(MyAvatar.orientation)));
|
||||
Entities.addModelEntity(name, url, addPosition);
|
||||
}
|
||||
|
||||
function copyURLToClipboard(index) {
|
||||
if (!index) {
|
||||
index = treeView.selection.currentIndex;
|
||||
}
|
||||
|
||||
var url = assetProxyModel.data(treeView.selection.currentIndex, 0x103);
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
Window.copyToClipboard(url);
|
||||
}
|
||||
|
||||
function renameEl(index, data) {
|
||||
if (!index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var path = assetProxyModel.data(index, 0x100);
|
||||
if (!path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var destinationPath = path.split('/');
|
||||
destinationPath[destinationPath.length - (path[path.length - 1] === '/' ? 2 : 1)] = data;
|
||||
destinationPath = destinationPath.join('/').trim();
|
||||
|
||||
if (path === destinationPath) {
|
||||
return;
|
||||
}
|
||||
if (!fileExists(destinationPath)) {
|
||||
doRenameFile(path, destinationPath);
|
||||
}
|
||||
}
|
||||
function renameFile(index) {
|
||||
if (!index) {
|
||||
index = treeView.selection.currentIndex;
|
||||
}
|
||||
|
||||
var path = assetProxyModel.data(index, 0x100);
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
var object = desktop.inputDialog({
|
||||
label: "Enter new path:",
|
||||
current: path,
|
||||
placeholderText: "Enter path here"
|
||||
});
|
||||
object.selected.connect(function(destinationPath) {
|
||||
destinationPath = destinationPath.trim();
|
||||
|
||||
if (path == destinationPath) {
|
||||
return;
|
||||
}
|
||||
if (fileExists(destinationPath)) {
|
||||
askForOverwrite(destinationPath, function() {
|
||||
doRenameFile(path, destinationPath);
|
||||
});
|
||||
} else {
|
||||
doRenameFile(path, destinationPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
function deleteFile(index) {
|
||||
if (!index) {
|
||||
index = treeView.selection.currentIndex;
|
||||
}
|
||||
var path = assetProxyModel.data(index, 0x100);
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
var isFolder = assetProxyModel.data(treeView.selection.currentIndex, 0x101);
|
||||
var typeString = isFolder ? 'folder' : 'file';
|
||||
|
||||
var object = desktop.messageBox({
|
||||
icon: hifi.icons.question,
|
||||
buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No,
|
||||
defaultButton: OriginalDialogs.StandardButton.Yes,
|
||||
title: "Delete",
|
||||
text: "You are about to delete the following " + typeString + ":\n" + path + "\nDo you want to continue?"
|
||||
});
|
||||
object.selected.connect(function(button) {
|
||||
if (button === OriginalDialogs.StandardButton.Yes) {
|
||||
doDeleteFile(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
property var uploadOpen: false;
|
||||
Timer {
|
||||
id: timer
|
||||
}
|
||||
function uploadClicked(fileUrl) {
|
||||
if (uploadOpen) {
|
||||
return;
|
||||
}
|
||||
uploadOpen = true;
|
||||
|
||||
function doUpload(url, dropping) {
|
||||
var fileUrl = fileDialogHelper.urlToPath(url);
|
||||
|
||||
var path = assetProxyModel.data(treeView.selection.currentIndex, 0x100);
|
||||
var directory = path ? path.slice(0, path.lastIndexOf('/') + 1) : "/";
|
||||
var filename = fileUrl.slice(fileUrl.lastIndexOf('/') + 1);
|
||||
|
||||
Assets.uploadFile(fileUrl, directory + filename,
|
||||
function() {
|
||||
// Upload started
|
||||
uploadSpinner.visible = true;
|
||||
uploadButton.enabled = false;
|
||||
uploadProgressLabel.text = "In progress...";
|
||||
},
|
||||
function(err, path) {
|
||||
print(err, path);
|
||||
if (!err) {
|
||||
uploadProgressLabel.text = "Upload Complete";
|
||||
timer.interval = 1000;
|
||||
timer.repeat = false;
|
||||
timer.triggered.connect(function() {
|
||||
uploadSpinner.visible = false;
|
||||
uploadButton.enabled = true;
|
||||
uploadOpen = false;
|
||||
});
|
||||
timer.start();
|
||||
console.log("Asset Browser - finished uploading: ", fileUrl);
|
||||
reload();
|
||||
} else {
|
||||
if (err > 0) {
|
||||
console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err);
|
||||
var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + Assets.getErrorString(err));
|
||||
box.selected.connect(reload);
|
||||
}
|
||||
uploadSpinner.visible = false;
|
||||
uploadButton.enabled = true;
|
||||
uploadOpen = false;
|
||||
}
|
||||
}, dropping);
|
||||
}
|
||||
|
||||
if (fileUrl) {
|
||||
doUpload(fileUrl, true);
|
||||
} else {
|
||||
var browser = desktop.fileDialog({
|
||||
selectDirectory: false,
|
||||
dir: currentDirectory
|
||||
});
|
||||
browser.canceled.connect(function() {
|
||||
uploadOpen = false;
|
||||
});
|
||||
browser.selectedFile.connect(function(url) {
|
||||
currentDirectory = browser.dir;
|
||||
doUpload(url, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function errorMessageBox(message) {
|
||||
return desktop.messageBox({
|
||||
icon: hifi.icons.warning,
|
||||
defaultButton: OriginalDialogs.StandardButton.Ok,
|
||||
title: "Error",
|
||||
text: message
|
||||
});
|
||||
}
|
||||
|
||||
Item {
|
||||
width: pane.contentWidth
|
||||
height: pane.height
|
||||
|
||||
HifiControls.ContentSection {
|
||||
id: assetDirectory
|
||||
name: "Asset Directory"
|
||||
spacing: hifi.dimensions.contentSpacing.y
|
||||
isFirst: true
|
||||
|
||||
Row {
|
||||
id: buttonRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: hifi.dimensions.contentSpacing.x
|
||||
|
||||
HifiControls.GlyphButton {
|
||||
glyph: hifi.glyphs.reload
|
||||
color: hifi.buttons.white
|
||||
colorScheme: root.colorScheme
|
||||
height: 26
|
||||
width: 26
|
||||
|
||||
onClicked: root.reload()
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
text: "ADD TO WORLD"
|
||||
color: hifi.buttons.white
|
||||
colorScheme: root.colorScheme
|
||||
height: 26
|
||||
width: 120
|
||||
|
||||
enabled: canAddToWorld(assetProxyModel.data(treeView.selection.currentIndex, 0x100))
|
||||
|
||||
onClicked: root.addToWorld()
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
text: "RENAME"
|
||||
color: hifi.buttons.white
|
||||
colorScheme: root.colorScheme
|
||||
height: 26
|
||||
width: 80
|
||||
|
||||
onClicked: root.renameFile()
|
||||
enabled: treeView.selection.hasSelection
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
id: deleteButton
|
||||
|
||||
text: "DELETE"
|
||||
color: hifi.buttons.red
|
||||
colorScheme: root.colorScheme
|
||||
height: 26
|
||||
width: 80
|
||||
|
||||
onClicked: root.deleteFile()
|
||||
enabled: treeView.selection.hasSelection
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: contextMenu
|
||||
title: "Edit"
|
||||
property var url: ""
|
||||
property var currentIndex: null
|
||||
|
||||
MenuItem {
|
||||
text: "Copy URL"
|
||||
onTriggered: {
|
||||
copyURLToClipboard(contextMenu.currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: "Rename"
|
||||
onTriggered: {
|
||||
renameFile(contextMenu.currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: "Delete"
|
||||
onTriggered: {
|
||||
deleteFile(contextMenu.currentIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.Tree {
|
||||
id: treeView
|
||||
anchors.top: assetDirectory.bottom
|
||||
anchors.bottom: uploadSection.top
|
||||
anchors.margins: 12
|
||||
anchors.bottomMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
treeModel: assetProxyModel
|
||||
canEdit: true
|
||||
colorScheme: root.colorScheme
|
||||
|
||||
modifyEl: renameEl
|
||||
|
||||
MouseArea {
|
||||
propagateComposedEvents: true
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
var index = treeView.indexAt(mouse.x, mouse.y);
|
||||
|
||||
treeView.selection.setCurrentIndex(index, 0x0002);
|
||||
|
||||
contextMenu.currentIndex = index;
|
||||
contextMenu.popup();
|
||||
}
|
||||
}
|
||||
}
|
||||
HifiControls.ContentSection {
|
||||
id: uploadSection
|
||||
name: "Upload A File"
|
||||
spacing: hifi.dimensions.contentSpacing.y
|
||||
anchors.bottom: parent.bottom
|
||||
height: 130
|
||||
|
||||
Item {
|
||||
height: parent.height
|
||||
width: parent.width
|
||||
HifiControls.Button {
|
||||
id: uploadButton
|
||||
anchors.right: parent.right
|
||||
|
||||
text: "Choose File"
|
||||
color: hifi.buttons.blue
|
||||
colorScheme: root.colorScheme
|
||||
height: 30
|
||||
width: 155
|
||||
|
||||
onClicked: uploadClickedTimer.running = true
|
||||
|
||||
// For some reason trigginer an API that enters
|
||||
// an internal event loop directly from the button clicked
|
||||
// trigger below causes the appliction to behave oddly.
|
||||
// Most likely because the button onClicked handling is never
|
||||
// completed until the function returns.
|
||||
// FIXME find a better way of handling the input dialogs that
|
||||
// doesn't trigger this.
|
||||
Timer {
|
||||
id: uploadClickedTimer
|
||||
interval: 5
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: uploadClicked();
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: uploadSpinner
|
||||
visible: false
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
width: 40
|
||||
height: 32
|
||||
|
||||
Image {
|
||||
id: image
|
||||
width: 24
|
||||
height: 24
|
||||
source: "../images/Loading-Outer-Ring.png"
|
||||
RotationAnimation on rotation {
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 2000
|
||||
}
|
||||
}
|
||||
Image {
|
||||
width: 24
|
||||
height: 24
|
||||
source: "../images/Loading-Inner-H.png"
|
||||
}
|
||||
HifiControls.Label {
|
||||
id: uploadProgressLabel
|
||||
anchors.left: image.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.verticalCenter: image.verticalCenter
|
||||
text: "In progress..."
|
||||
colorScheme: root.colorScheme
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -166,7 +166,8 @@ Item {
|
|||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
visible: root.expanded;
|
||||
text: "Downloads: ";
|
||||
text: "Downloads: " + root.downloads + "/" + root.downloadLimit +
|
||||
", Pending: " + root.downloadsPending;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,9 @@ import QtQuick.Controls.Styles 1.4
|
|||
import "../styles-uit"
|
||||
|
||||
Original.Button {
|
||||
id: button
|
||||
property int color: 0
|
||||
property int colorScheme: hifi.colorSchemes.light
|
||||
|
||||
width: 120
|
||||
height: 28
|
||||
|
||||
|
@ -24,27 +25,43 @@ Original.Button {
|
|||
|
||||
background: Rectangle {
|
||||
radius: hifi.buttons.radius
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.2
|
||||
color: enabled
|
||||
? (!pressed && button.color != hifi.buttons.black || (!hovered || pressed) && button.color == hifi.buttons.black
|
||||
? hifi.buttons.colorStart[button.color] : hifi.buttons.colorFinish[button.color])
|
||||
: hifi.buttons.colorStart[hifi.buttons.white]
|
||||
color: {
|
||||
if (!control.enabled) {
|
||||
hifi.buttons.disabledColorStart[control.colorScheme]
|
||||
} else if (control.pressed) {
|
||||
hifi.buttons.pressedColor[control.color]
|
||||
} else if (control.hovered) {
|
||||
hifi.buttons.hoveredColor[control.color]
|
||||
} else {
|
||||
hifi.buttons.colorStart[control.color]
|
||||
}
|
||||
}
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: enabled
|
||||
? ((!hovered || pressed) && button.color != hifi.buttons.black || !pressed && button.color == hifi.buttons.black
|
||||
? hifi.buttons.colorFinish[button.color] : hifi.buttons.colorStart[button.color])
|
||||
: hifi.buttons.colorFinish[hifi.buttons.white]
|
||||
color: {
|
||||
if (!control.enabled) {
|
||||
hifi.buttons.disabledColorFinish[control.colorScheme]
|
||||
} else if (control.pressed) {
|
||||
hifi.buttons.pressedColor[control.color]
|
||||
} else if (control.hovered) {
|
||||
hifi.buttons.hoveredColor[control.color]
|
||||
} else {
|
||||
hifi.buttons.colorFinish[control.color]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label: RalewayBold {
|
||||
font.capitalization: Font.AllUppercase
|
||||
color: enabled ? hifi.buttons.textColor[button.color] : hifi.colors.lightGrayText
|
||||
color: enabled ? hifi.buttons.textColor[control.color]
|
||||
: hifi.buttons.disabledTextColor[control.colorScheme]
|
||||
size: hifi.fontSizes.buttonLabel
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
|
|
@ -14,7 +14,7 @@ import QtGraphicalEffects 1.0
|
|||
import "../styles-uit"
|
||||
|
||||
Column {
|
||||
property string name: "Static Section"
|
||||
property string name: "Content Section"
|
||||
property bool isFirst: false
|
||||
property bool isCollapsible: false // Set at creation.
|
||||
property bool isCollapsed: false
|
||||
|
|
71
interface/resources/qml/controls-uit/GlyphButton.qml
Normal file
71
interface/resources/qml/controls-uit/GlyphButton.qml
Normal file
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// GlyphButton.qml
|
||||
//
|
||||
// Created by Clement on 3/7/16
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4 as Original
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
import "../styles-uit"
|
||||
|
||||
Original.Button {
|
||||
property int color: 0
|
||||
property int colorScheme: hifi.colorShemes.light
|
||||
property string glyph: ""
|
||||
|
||||
width: 120
|
||||
height: 28
|
||||
|
||||
style: ButtonStyle {
|
||||
|
||||
background: Rectangle {
|
||||
radius: hifi.buttons.radius
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.2
|
||||
color: {
|
||||
if (!control.enabled) {
|
||||
hifi.buttons.disabledColorStart[control.colorScheme]
|
||||
} else if (control.pressed) {
|
||||
hifi.buttons.pressedColor[control.color]
|
||||
} else if (control.hovered) {
|
||||
hifi.buttons.hoveredColor[control.color]
|
||||
} else {
|
||||
hifi.buttons.colorStart[control.color]
|
||||
}
|
||||
}
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: {
|
||||
if (!control.enabled) {
|
||||
hifi.buttons.disabledColorFinish[control.colorScheme]
|
||||
} else if (control.pressed) {
|
||||
hifi.buttons.pressedColor[control.color]
|
||||
} else if (control.hovered) {
|
||||
hifi.buttons.hoveredColor[control.color]
|
||||
} else {
|
||||
hifi.buttons.colorFinish[control.color]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label: HiFiGlyphs {
|
||||
color: enabled ? hifi.buttons.textColor[control.color]
|
||||
: hifi.buttons.disabledTextColor[control.colorScheme]
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: control.glyph
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,7 +38,7 @@ TextField {
|
|||
: (textField.focus ? hifi.colors.white : hifi.colors.lightGrayText)
|
||||
background: Rectangle {
|
||||
color: isLightColorScheme
|
||||
? (textField.focus ? hifi.colors.white : hifi.colors.lightGray)
|
||||
? (textField.focus ? hifi.colors.white : hifi.colors.textFieldLightBackground)
|
||||
: (textField.focus ? hifi.colors.black : hifi.colors.baseGrayShadow)
|
||||
border.color: hifi.colors.primaryHighlight
|
||||
border.width: textField.focus ? 1 : 0
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
import QtQml.Models 2.2
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
@ -18,10 +19,16 @@ TreeView {
|
|||
id: treeView
|
||||
|
||||
property var treeModel: ListModel { }
|
||||
property var canEdit: false
|
||||
property int colorScheme: hifi.colorSchemes.light
|
||||
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
|
||||
|
||||
property var modifyEl: function(index, data) { return false; }
|
||||
|
||||
model: treeModel
|
||||
selection: ItemSelectionModel {
|
||||
model: treeModel
|
||||
}
|
||||
|
||||
TableViewColumn {
|
||||
role: "display";
|
||||
|
@ -120,7 +127,9 @@ TreeView {
|
|||
: (styleData.alternate ? hifi.colors.tableRowDarkEven : hifi.colors.tableRowDarkOdd)
|
||||
}
|
||||
|
||||
itemDelegate: FiraSansSemiBold {
|
||||
itemDelegate: Loader {
|
||||
id: itemDelegateLoader
|
||||
|
||||
anchors {
|
||||
left: parent ? parent.left : undefined
|
||||
leftMargin: (2 + styleData.depth) * hifi.dimensions.tablePadding
|
||||
|
@ -129,11 +138,83 @@ TreeView {
|
|||
verticalCenter: parent ? parent.verticalCenter : undefined
|
||||
}
|
||||
|
||||
text: styleData.value
|
||||
size: hifi.fontSizes.tableText
|
||||
color: colorScheme == hifi.colorSchemes.light
|
||||
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
|
||||
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
|
||||
function getComponent() {
|
||||
if (treeView.canEdit && styleData.selected) {
|
||||
return textFieldComponent;
|
||||
} else {
|
||||
return labelComponent;
|
||||
}
|
||||
|
||||
}
|
||||
sourceComponent: getComponent()
|
||||
|
||||
Component {
|
||||
id: labelComponent
|
||||
FiraSansSemiBold {
|
||||
|
||||
text: styleData.value
|
||||
size: hifi.fontSizes.tableText
|
||||
color: colorScheme == hifi.colorSchemes.light
|
||||
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
|
||||
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: textFieldComponent
|
||||
|
||||
TextField {
|
||||
id: textField
|
||||
readOnly: !activeFocus
|
||||
|
||||
text: styleData.value
|
||||
|
||||
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
|
||||
font.family: firaSansSemiBold.name
|
||||
font.pixelSize: hifi.fontSizes.textFieldInput
|
||||
height: hifi.dimensions.tableRowHeight
|
||||
|
||||
style: TextFieldStyle {
|
||||
textColor: readOnly
|
||||
? hifi.colors.black
|
||||
: (treeView.isLightColorScheme ? hifi.colors.black : hifi.colors.white)
|
||||
background: Rectangle {
|
||||
visible: !readOnly
|
||||
color: treeView.isLightColorScheme ? hifi.colors.white : hifi.colors.black
|
||||
border.color: hifi.colors.primaryHighlight
|
||||
border.width: 1
|
||||
}
|
||||
selectedTextColor: hifi.colors.black
|
||||
selectionColor: hifi.colors.primaryHighlight
|
||||
padding.left: readOnly ? 0 : hifi.dimensions.textPadding
|
||||
padding.right: readOnly ? 0 : hifi.dimensions.textPadding
|
||||
}
|
||||
|
||||
validator: RegExpValidator {
|
||||
regExp: /[^/]+/
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
if (event.key == Qt.Key_Escape) {
|
||||
text = styleData.value;
|
||||
unfocusHelper.forceActiveFocus();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
onAccepted: {
|
||||
if (acceptableInput && styleData.selected) {
|
||||
if (!modifyEl(selection.currentIndex, text)) {
|
||||
text = styleData.value;
|
||||
}
|
||||
unfocusHelper.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: unfocusHelper
|
||||
visible: false
|
||||
}
|
||||
|
||||
onDoubleClicked: isExpanded(index) ? collapse(index) : expand(index)
|
||||
|
|
|
@ -75,7 +75,7 @@ ModalWindow {
|
|||
readonly property int maxHeight: 720
|
||||
|
||||
function resize() {
|
||||
var targetWidth = Math.max(titleWidth, mainTextContainer.width)
|
||||
var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth)
|
||||
var targetHeight = mainTextContainer.height + 3 * hifi.dimensions.contentSpacing.y
|
||||
+ (informativeTextContainer.text != "" ? informativeTextContainer.contentHeight + 3 * hifi.dimensions.contentSpacing.y : 0)
|
||||
+ buttons.height
|
||||
|
@ -99,6 +99,7 @@ ModalWindow {
|
|||
}
|
||||
lineHeight: 2
|
||||
lineHeightMode: Text.ProportionalHeight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
RalewaySemiBold {
|
||||
|
|
|
@ -34,8 +34,7 @@ ModalWindow {
|
|||
property var items;
|
||||
property string label
|
||||
property var result;
|
||||
// FIXME not current honored
|
||||
property var current;
|
||||
property alias current: textResult.text
|
||||
|
||||
// For text boxes
|
||||
property alias placeholderText: textResult.placeholderText
|
||||
|
|
|
@ -10,6 +10,7 @@ QtObject {
|
|||
readonly property string animDebugDrawPosition: "Debug Draw Position";
|
||||
readonly property string antialiasing: "Antialiasing";
|
||||
readonly property string assetMigration: "ATP Asset Migration";
|
||||
readonly property string assetServer = "Asset Server";
|
||||
readonly property string atmosphere: "Atmosphere";
|
||||
readonly property string attachments: "Attachments...";
|
||||
readonly property string audioNetworkStats: "Audio Network Stats";
|
||||
|
@ -154,7 +155,6 @@ QtObject {
|
|||
readonly property string toolWindow: "Tool Window";
|
||||
readonly property string transmitterDrive: "Transmitter Drive";
|
||||
readonly property string turnWithHead: "Turn using Head";
|
||||
readonly property string uploadAsset: "Upload File to Asset Server";
|
||||
readonly property string useAudioForMouth: "Use Audio for Mouth";
|
||||
readonly property string useCamera: "Use Camera";
|
||||
readonly property string velocityFilter: "Velocity Filter";
|
||||
|
|
|
@ -35,7 +35,7 @@ Item {
|
|||
glyph = hifi.glyphs.alert;
|
||||
break;
|
||||
case hifi.icons.critical:
|
||||
glyph = hifi.glyphs.critical;
|
||||
glyph = hifi.glyphs.error;
|
||||
break;
|
||||
case hifi.icons.placemark:
|
||||
glyph = hifi.glyphs.placemark;
|
||||
|
@ -113,6 +113,7 @@ Item {
|
|||
readonly property color dropDownLightFinish: "#afafaf"
|
||||
readonly property color dropDownDarkStart: "#7d7d7d"
|
||||
readonly property color dropDownDarkFinish: "#6b6a6b"
|
||||
readonly property color textFieldLightBackground: "#d4d4d4"
|
||||
}
|
||||
|
||||
Item {
|
||||
|
@ -164,32 +165,6 @@ Item {
|
|||
readonly property real disclosureButton: dimensions.largeScreen ? 20 : 15
|
||||
}
|
||||
|
||||
Item {
|
||||
id: glyphs
|
||||
readonly property string alert: "+"
|
||||
readonly property string backward: "E"
|
||||
readonly property string caratDn: "5"
|
||||
readonly property string caratR: "3"
|
||||
readonly property string caratUp: "6"
|
||||
readonly property string close: "w"
|
||||
readonly property string closeInverted: "x"
|
||||
readonly property string closeSmall: "C"
|
||||
readonly property string critical: "="
|
||||
readonly property string disclosureButtonCollapse: "M"
|
||||
readonly property string disclosureButtonExpand: "L"
|
||||
readonly property string disclosureCollapse: "Z"
|
||||
readonly property string disclosureExpand: "B"
|
||||
readonly property string forward: "D"
|
||||
readonly property string info: "["
|
||||
readonly property string noIcon: ""
|
||||
readonly property string pin: "y"
|
||||
readonly property string pinInverted: "z"
|
||||
readonly property string placemark: "U"
|
||||
readonly property string question: "]"
|
||||
readonly property string reloadSmall: "a"
|
||||
readonly property string resizeHandle: "A"
|
||||
}
|
||||
|
||||
Item {
|
||||
id: icons
|
||||
// Values per OffscreenUi::Icon
|
||||
|
@ -208,8 +183,13 @@ Item {
|
|||
readonly property int red: 2
|
||||
readonly property int black: 3
|
||||
readonly property var textColor: [ colors.darkGray, colors.white, colors.white, colors.white ]
|
||||
readonly property var colorStart: [ "#ffffff", "#00b4ef", "#d42043", "#343434" ]
|
||||
readonly property var colorFinish: [ "#afafaf", "#1080b8", "#94132e", "#000000" ]
|
||||
readonly property var colorStart: [ colors.white, colors.primaryHighlight, "#d42043", "#343434" ]
|
||||
readonly property var colorFinish: [ colors.lightGrayText, colors.blueAccent, "#94132e", colors.black ]
|
||||
readonly property var hoveredColor: [ colorStart[white], colorStart[blue], colorStart[red], colorFinish[black] ]
|
||||
readonly property var pressedColor: [ colorFinish[white], colorFinish[blue], colorFinish[red], colorStart[black] ]
|
||||
readonly property var disabledColorStart: [ colorStart[white], colors.baseGrayHighlight]
|
||||
readonly property var disabledColorFinish: [ colorFinish[white], colors.baseGrayShadow]
|
||||
readonly property var disabledTextColor: [ colors.lightGrayText, colors.baseGrayShadow]
|
||||
readonly property int radius: 5
|
||||
}
|
||||
|
||||
|
@ -217,4 +197,103 @@ Item {
|
|||
id: effects
|
||||
readonly property int fadeInDuration: 300
|
||||
}
|
||||
Item {
|
||||
id: glyphs
|
||||
readonly property string noIcon: ""
|
||||
readonly property string hmd: "b"
|
||||
readonly property string screen: "c"
|
||||
readonly property string keyboard: "d"
|
||||
readonly property string handControllers: "e"
|
||||
readonly property string headphonesMic: "f"
|
||||
readonly property string gamepad: "g"
|
||||
readonly property string headphones: "h"
|
||||
readonly property string mic: "i"
|
||||
readonly property string upload: "j"
|
||||
readonly property string script: "k"
|
||||
readonly property string text: "l"
|
||||
readonly property string cube: "m"
|
||||
readonly property string sphere: "n"
|
||||
readonly property string zone: "o"
|
||||
readonly property string light: "p"
|
||||
readonly property string web: "q"
|
||||
readonly property string web2: "r"
|
||||
readonly property string edit: "s"
|
||||
readonly property string market: "t"
|
||||
readonly property string directory: "u"
|
||||
readonly property string menu: "v"
|
||||
readonly property string close: "w"
|
||||
readonly property string closeInverted: "x"
|
||||
readonly property string pin: "y"
|
||||
readonly property string pinInverted: "z"
|
||||
readonly property string resizeHandle: "A"
|
||||
readonly property string disclosureExpand: "B"
|
||||
readonly property string reloadSmall: "a"
|
||||
readonly property string closeSmall: "C"
|
||||
readonly property string forward: "D"
|
||||
readonly property string backward: "E"
|
||||
readonly property string reload: "F"
|
||||
readonly property string unmuted: "G"
|
||||
readonly property string muted: "H"
|
||||
readonly property string minimize: "I"
|
||||
readonly property string maximize: "J"
|
||||
readonly property string maximizeInverted: "K"
|
||||
readonly property string disclosureButtonExpand: "L"
|
||||
readonly property string disclosureButtonCollapse: "M"
|
||||
readonly property string scriptStop: "N"
|
||||
readonly property string scriptReload: "O"
|
||||
readonly property string scriptRun: "P"
|
||||
readonly property string scriptNew: "Q"
|
||||
readonly property string hifiForum: "2"
|
||||
readonly property string hifiLogoSmall: "S"
|
||||
readonly property string avatar1: "T"
|
||||
readonly property string placemark: "U"
|
||||
readonly property string box: "V"
|
||||
readonly property string community: "0"
|
||||
readonly property string grabHandle: "X"
|
||||
readonly property string search: "Y"
|
||||
readonly property string disclosureCollapse: "Z"
|
||||
readonly property string scriptUpload: "R"
|
||||
readonly property string code: "W"
|
||||
readonly property string avatar: "<"
|
||||
readonly property string arrowsH: ":"
|
||||
readonly property string arrowsV: ";"
|
||||
readonly property string arrows: "`"
|
||||
readonly property string compress: "!"
|
||||
readonly property string expand: "\""
|
||||
readonly property string placemark1: "#"
|
||||
readonly property string circle: "$"
|
||||
readonly property string handPointer: "9"
|
||||
readonly property string plusSquareO: "%"
|
||||
readonly property string sliders: "&"
|
||||
readonly property string square: "'"
|
||||
readonly property string alignCenter: "8"
|
||||
readonly property string alignJustify: ")"
|
||||
readonly property string alignLeft: "*"
|
||||
readonly property string alignRight: "^"
|
||||
readonly property string bars: "7"
|
||||
readonly property string circleSlash: ","
|
||||
readonly property string sync: "()"
|
||||
readonly property string key: "-"
|
||||
readonly property string link: "."
|
||||
readonly property string location: "/"
|
||||
readonly property string caratR: "3"
|
||||
readonly property string caratL: "4"
|
||||
readonly property string caratDn: "5"
|
||||
readonly property string caratUp: "6"
|
||||
readonly property string folderLg: ">"
|
||||
readonly property string folderSm: "?"
|
||||
readonly property string levelUp: "1"
|
||||
readonly property string info: "["
|
||||
readonly property string question: "]"
|
||||
readonly property string alert: "+"
|
||||
readonly property string home: "_"
|
||||
readonly property string error: "="
|
||||
readonly property string settings: "@"
|
||||
readonly property string trash: "{"
|
||||
readonly property string objectGroup: ""
|
||||
readonly property string cm: "}"
|
||||
readonly property string msvg79: "~"
|
||||
readonly property string deg: "\\"
|
||||
readonly property string px: "|"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -141,6 +141,7 @@
|
|||
#include "ModelPackager.h"
|
||||
#include "PluginContainerProxy.h"
|
||||
#include "scripting/AccountScriptingInterface.h"
|
||||
#include "scripting/AssetMappingsScriptingInterface.h"
|
||||
#include "scripting/AudioDeviceScriptingInterface.h"
|
||||
#include "scripting/ClipboardScriptingInterface.h"
|
||||
#include "scripting/DesktopScriptingInterface.h"
|
||||
|
@ -158,7 +159,6 @@
|
|||
#include "Stars.h"
|
||||
#include "ui/AddressBarDialog.h"
|
||||
#include "ui/AvatarInputs.h"
|
||||
#include "ui/AssetUploadDialogFactory.h"
|
||||
#include "ui/DialogsManager.h"
|
||||
#include "ui/LoginDialog.h"
|
||||
#include "ui/overlays/Cube3DOverlay.h"
|
||||
|
@ -1295,6 +1295,7 @@ void Application::initializeUi() {
|
|||
rootContext->setContextProperty("Quat", new Quat());
|
||||
rootContext->setContextProperty("Vec3", new Vec3());
|
||||
rootContext->setContextProperty("Uuid", new ScriptUUID());
|
||||
rootContext->setContextProperty("Assets", new AssetMappingsScriptingInterface());
|
||||
|
||||
rootContext->setContextProperty("AvatarList", DependencyManager::get<AvatarManager>().data());
|
||||
|
||||
|
@ -2058,18 +2059,21 @@ void Application::keyPressEvent(QKeyEvent* event) {
|
|||
if (isShifted) {
|
||||
Menu::getInstance()->triggerOption(MenuOption::MiniMirror);
|
||||
} else {
|
||||
Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror));
|
||||
if (!Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) {
|
||||
bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror);
|
||||
Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked);
|
||||
if (isMirrorChecked) {
|
||||
Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true);
|
||||
}
|
||||
cameraMenuChanged();
|
||||
}
|
||||
break;
|
||||
case Qt::Key_P:
|
||||
Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, !Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson));
|
||||
Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson));
|
||||
cameraMenuChanged();
|
||||
break;
|
||||
case Qt::Key_P: {
|
||||
bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson);
|
||||
Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, !isFirstPersonChecked);
|
||||
Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, isFirstPersonChecked);
|
||||
cameraMenuChanged();
|
||||
break;
|
||||
}
|
||||
|
||||
case Qt::Key_Slash:
|
||||
Menu::getInstance()->triggerOption(MenuOption::Stats);
|
||||
|
@ -4015,9 +4019,6 @@ void Application::nodeAdded(SharedNodePointer node) {
|
|||
if (node->getType() == NodeType::AvatarMixer) {
|
||||
// new avatar mixer, send off our identity packet right away
|
||||
getMyAvatar()->sendIdentityPacket();
|
||||
} else if (node->getType() == NodeType::AssetServer) {
|
||||
// the addition of an asset-server always re-enables the upload to asset server menu option
|
||||
Menu::getInstance()->getActionForOption(MenuOption::UploadAsset)->setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4067,10 +4068,6 @@ void Application::nodeKilled(SharedNodePointer node) {
|
|||
} else if (node->getType() == NodeType::AvatarMixer) {
|
||||
// our avatar mixer has gone away - clear the hash of avatars
|
||||
DependencyManager::get<AvatarManager>()->clearOtherAvatars();
|
||||
} else if (node->getType() == NodeType::AssetServer
|
||||
&& !DependencyManager::get<NodeList>()->soloNodeOfType(NodeType::AssetServer)) {
|
||||
// this was our last asset server - disable the menu option to upload an asset
|
||||
Menu::getInstance()->getActionForOption(MenuOption::UploadAsset)->setEnabled(false);
|
||||
}
|
||||
}
|
||||
void Application::trackIncomingOctreePacket(ReceivedMessage& message, SharedNodePointer sendingNode, bool wasStatsPacket) {
|
||||
|
@ -4296,7 +4293,10 @@ bool Application::acceptURL(const QString& urlString, bool defaultUpload) {
|
|||
}
|
||||
}
|
||||
|
||||
return defaultUpload && askToUploadAsset(urlString);
|
||||
if (defaultUpload) {
|
||||
toggleAssetServerWidget(urlString);
|
||||
}
|
||||
return defaultUpload;
|
||||
}
|
||||
|
||||
void Application::setSessionUUID(const QUuid& sessionUUID) {
|
||||
|
@ -4324,8 +4324,8 @@ bool Application::askToSetAvatarUrl(const QString& url) {
|
|||
|
||||
case FSTReader::HEAD_AND_BODY_MODEL:
|
||||
ok = QMessageBox::Ok == OffscreenUi::question("Set Avatar",
|
||||
"Would you like to use '" + modelName + "' for your avatar?",
|
||||
QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok);
|
||||
"Would you like to use '" + modelName + "' for your avatar?",
|
||||
QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -4358,79 +4358,6 @@ bool Application::askToLoadScript(const QString& scriptFilenameOrURL) {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool Application::askToUploadAsset(const QString& filename) {
|
||||
if (!DependencyManager::get<NodeList>()->getThisNodeCanRez()) {
|
||||
OffscreenUi::warning(_window, "Failed Upload",
|
||||
QString("You don't have upload rights on that domain.\n\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
QUrl url { filename };
|
||||
if (auto upload = DependencyManager::get<AssetClient>()->createUpload(url.toLocalFile())) {
|
||||
|
||||
QMessageBox messageBox;
|
||||
messageBox.setWindowTitle("Asset upload");
|
||||
messageBox.setText("You are about to upload the following file to the asset server:\n" +
|
||||
url.toDisplayString());
|
||||
messageBox.setInformativeText("Do you want to continue?");
|
||||
messageBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
|
||||
messageBox.setDefaultButton(QMessageBox::Ok);
|
||||
|
||||
// Option to drop model in world for models
|
||||
if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive) || filename.endsWith(OBJ_EXTENSION, Qt::CaseInsensitive)) {
|
||||
auto checkBox = new QCheckBox(&messageBox);
|
||||
checkBox->setText("Add to scene");
|
||||
messageBox.setCheckBox(checkBox);
|
||||
}
|
||||
|
||||
if (messageBox.exec() != QMessageBox::Ok) {
|
||||
upload->deleteLater();
|
||||
return false;
|
||||
}
|
||||
|
||||
// connect to the finished signal so we know when the AssetUpload is done
|
||||
if (messageBox.checkBox() && (messageBox.checkBox()->checkState() == Qt::Checked)) {
|
||||
// Custom behavior for models
|
||||
QObject::connect(upload, &AssetUpload::finished, this, &Application::modelUploadFinished);
|
||||
} else {
|
||||
QObject::connect(upload, &AssetUpload::finished,
|
||||
&AssetUploadDialogFactory::getInstance(),
|
||||
&AssetUploadDialogFactory::handleUploadFinished);
|
||||
}
|
||||
|
||||
// start the upload now
|
||||
upload->start();
|
||||
return true;
|
||||
}
|
||||
|
||||
// display a message box with the error
|
||||
OffscreenUi::warning(_window, "Failed Upload", QString("Failed to upload %1.\n\n").arg(filename));
|
||||
return false;
|
||||
}
|
||||
|
||||
void Application::modelUploadFinished(AssetUpload* upload, const QString& hash) {
|
||||
auto filename = QFileInfo(upload->getFilename()).fileName();
|
||||
|
||||
if ((upload->getError() == AssetUpload::NoError) &&
|
||||
(FBX_EXTENSION.endsWith(upload->getExtension(), Qt::CaseInsensitive) ||
|
||||
OBJ_EXTENSION.endsWith(upload->getExtension(), Qt::CaseInsensitive))) {
|
||||
|
||||
auto entities = DependencyManager::get<EntityScriptingInterface>();
|
||||
|
||||
EntityItemProperties properties;
|
||||
properties.setType(EntityTypes::Model);
|
||||
properties.setModelURL(QString("%1:%2.%3").arg(URL_SCHEME_ATP).arg(hash).arg(upload->getExtension()));
|
||||
properties.setPosition(_myCamera.getPosition() + _myCamera.getOrientation() * Vectors::FRONT * 2.0f);
|
||||
properties.setName(QUrl(upload->getFilename()).fileName());
|
||||
|
||||
entities->addEntity(properties);
|
||||
|
||||
upload->deleteLater();
|
||||
} else {
|
||||
AssetUploadDialogFactory::getInstance().handleUploadFinished(upload, hash);
|
||||
}
|
||||
}
|
||||
|
||||
bool Application::askToWearAvatarAttachmentUrl(const QString& url) {
|
||||
|
||||
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
|
||||
|
@ -4530,6 +4457,22 @@ void Application::toggleRunningScriptsWidget() {
|
|||
//}
|
||||
}
|
||||
|
||||
void Application::toggleAssetServerWidget(QString filePath) {
|
||||
if (!DependencyManager::get<NodeList>()->getThisNodeCanRez()) {
|
||||
return;
|
||||
}
|
||||
|
||||
static const QUrl url { "AssetServer.qml" };
|
||||
|
||||
auto startUpload = [=](QQmlContext* context, QObject* newObject){
|
||||
if (!filePath.isEmpty()) {
|
||||
emit uploadRequest(filePath);
|
||||
}
|
||||
};
|
||||
DependencyManager::get<OffscreenUi>()->show(url, "AssetServer", startUpload);
|
||||
startUpload(nullptr, nullptr);
|
||||
}
|
||||
|
||||
void Application::packageModel() {
|
||||
ModelPackager::package();
|
||||
}
|
||||
|
|
|
@ -231,6 +231,8 @@ signals:
|
|||
void beforeAboutToQuit();
|
||||
void activeDisplayPluginChanged();
|
||||
|
||||
void uploadRequest(QString path);
|
||||
|
||||
public slots:
|
||||
QVector<EntityItemID> pasteEntities(float x, float y, float z);
|
||||
bool exportEntities(const QString& filename, const QVector<EntityItemID>& entityIDs);
|
||||
|
@ -242,6 +244,7 @@ public slots:
|
|||
Q_INVOKABLE void loadScriptURLDialog();
|
||||
void toggleLogDialog();
|
||||
void toggleRunningScriptsWidget();
|
||||
void toggleAssetServerWidget(QString filePath = "");
|
||||
|
||||
void handleLocalServerConnection();
|
||||
void readArgumentsFromLocalSocket();
|
||||
|
@ -303,8 +306,6 @@ private slots:
|
|||
bool acceptSnapshot(const QString& urlString);
|
||||
bool askToSetAvatarUrl(const QString& url);
|
||||
bool askToLoadScript(const QString& scriptFilenameOrURL);
|
||||
bool askToUploadAsset(const QString& asset);
|
||||
void modelUploadFinished(AssetUpload* upload, const QString& hash);
|
||||
|
||||
bool askToWearAvatarAttachmentUrl(const QString& url);
|
||||
void displayAvatarAttachmentWarning(const QString& message) const;
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
#include "MainWindow.h"
|
||||
#include "render/DrawStatus.h"
|
||||
#include "scripting/MenuScriptingInterface.h"
|
||||
#include "ui/AssetUploadDialogFactory.h"
|
||||
#include "ui/DialogsManager.h"
|
||||
#include "ui/StandAloneJSConsole.h"
|
||||
#include "InterfaceLogging.h"
|
||||
|
@ -92,7 +91,7 @@ Menu::Menu() {
|
|||
redoAction->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_Z);
|
||||
addActionToQMenuAndActionHash(editMenu, redoAction);
|
||||
|
||||
// Edit > Running Sccripts
|
||||
// Edit > Running Scripts
|
||||
addActionToQMenuAndActionHash(editMenu, MenuOption::RunningScripts, Qt::CTRL | Qt::Key_J,
|
||||
qApp, SLOT(toggleRunningScriptsWidget()));
|
||||
|
||||
|
@ -128,10 +127,20 @@ Menu::Menu() {
|
|||
SLOT(toggleConsole()),
|
||||
QAction::NoRole, UNSPECIFIED_POSITION, "Advanced");
|
||||
|
||||
editMenu->addSeparator();
|
||||
|
||||
// Edit > My Asset Server
|
||||
auto assetServerAction = addActionToQMenuAndActionHash(editMenu, MenuOption::AssetServer,
|
||||
Qt::CTRL | Qt::SHIFT | Qt::Key_A,
|
||||
qApp, SLOT(toggleAssetServerWidget()));
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
QObject::connect(nodeList.data(), &NodeList::canRezChanged, assetServerAction, &QAction::setEnabled);
|
||||
assetServerAction->setEnabled(nodeList->getThisNodeCanRez());
|
||||
|
||||
// Edit > Reload All Content [advanced]
|
||||
addActionToQMenuAndActionHash(editMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches()),
|
||||
QAction::NoRole, UNSPECIFIED_POSITION, "Advanced");
|
||||
|
||||
|
||||
|
||||
// Edit > Package Model... [advanced]
|
||||
addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0,
|
||||
|
@ -354,17 +363,6 @@ Menu::Menu() {
|
|||
|
||||
// Developer > Assets >>>
|
||||
MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets");
|
||||
auto& assetDialogFactory = AssetUploadDialogFactory::getInstance();
|
||||
assetDialogFactory.setDialogParent(this);
|
||||
QAction* assetUpload = addActionToQMenuAndActionHash(assetDeveloperMenu,
|
||||
MenuOption::UploadAsset,
|
||||
0,
|
||||
&assetDialogFactory,
|
||||
SLOT(showDialog()));
|
||||
|
||||
// disable the asset upload action by default - it gets enabled only if asset server becomes present
|
||||
assetUpload->setEnabled(false);
|
||||
|
||||
auto& atpMigrator = ATPAssetMigrator::getInstance();
|
||||
atpMigrator.setDialogParent(this);
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ namespace MenuOption {
|
|||
const QString AnimDebugDrawDefaultPose = "Debug Draw Default Pose";
|
||||
const QString AnimDebugDrawPosition= "Debug Draw Position";
|
||||
const QString AssetMigration = "ATP Asset Migration";
|
||||
const QString AssetServer = "My Asset Server";
|
||||
const QString Attachments = "Attachments...";
|
||||
const QString AudioNetworkStats = "Audio Network Stats";
|
||||
const QString AudioNoiseReduction = "Audio Noise Reduction";
|
||||
|
@ -168,7 +169,6 @@ namespace MenuOption {
|
|||
const QString ToolWindow = "Tool Window";
|
||||
const QString TransmitterDrive = "Transmitter Drive";
|
||||
const QString TurnWithHead = "Turn using Head";
|
||||
const QString UploadAsset = "Upload File to Asset Server";
|
||||
const QString UseAudioForMouth = "Use Audio for Mouth";
|
||||
const QString UseCamera = "Use Camera";
|
||||
const QString UseAnimPreAndPostRotations = "Use Anim Pre and Post Rotations";
|
||||
|
|
|
@ -24,9 +24,9 @@
|
|||
#include <AssetClient.h>
|
||||
#include <AssetUpload.h>
|
||||
#include <ResourceManager.h>
|
||||
#include <MappingRequest.h>
|
||||
|
||||
#include "OffscreenUi.h"
|
||||
#include "../ui/AssetUploadDialogFactory.h"
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(asset_migrator);
|
||||
Q_LOGGING_CATEGORY(asset_migrator, "hf.asset_migrator");
|
||||
|
@ -38,18 +38,18 @@ ATPAssetMigrator& ATPAssetMigrator::getInstance() {
|
|||
|
||||
static const QString ENTITIES_OBJECT_KEY = "Entities";
|
||||
static const QString MODEL_URL_KEY = "modelURL";
|
||||
static const QString COMPOUND_SHAPE_URL_KEY = "compoundShapeURL";
|
||||
static const QString MESSAGE_BOX_TITLE = "ATP Asset Migration";
|
||||
|
||||
void ATPAssetMigrator::loadEntityServerFile() {
|
||||
auto filename = QFileDialog::getOpenFileName(_dialogParent, "Select an entity-server content file to migrate",
|
||||
QString(), QString("Entity-Server Content (*.gz)"));
|
||||
auto filename = OffscreenUi::getOpenFileName(_dialogParent, tr("Select an entity-server content file to migrate"), QString(), tr("Entity-Server Content (*.gz)"));
|
||||
|
||||
if (!filename.isEmpty()) {
|
||||
qCDebug(asset_migrator) << "Selected filename for ATP asset migration: " << filename;
|
||||
|
||||
static const QString MIGRATION_CONFIRMATION_TEXT {
|
||||
"The ATP Asset Migration process will scan the selected entity-server file, upload discovered resources to the"\
|
||||
" current asset-server and then save a new entity-server file with the ATP URLs.\n\nAre you ready to"\
|
||||
"The ATP Asset Migration process will scan the selected entity-server file,\nupload discovered resources to the"\
|
||||
" current asset-server\nand then save a new entity-server file with the ATP URLs.\n\nAre you ready to"\
|
||||
" continue?\n\nMake sure you are connected to the right domain."
|
||||
};
|
||||
|
||||
|
@ -77,57 +77,75 @@ void ATPAssetMigrator::loadEntityServerFile() {
|
|||
for (auto jsonValue : _entitiesArray) {
|
||||
QJsonObject entityObject = jsonValue.toObject();
|
||||
QString modelURLString = entityObject.value(MODEL_URL_KEY).toString();
|
||||
|
||||
if (!modelURLString.isEmpty()) {
|
||||
QUrl modelURL = QUrl(modelURLString);
|
||||
|
||||
if (!_ignoredUrls.contains(modelURL)
|
||||
&& (modelURL.scheme() == URL_SCHEME_HTTP || modelURL.scheme() == URL_SCHEME_HTTPS
|
||||
|| modelURL.scheme() == URL_SCHEME_FILE || modelURL.scheme() == URL_SCHEME_FTP)) {
|
||||
|
||||
if (_pendingReplacements.contains(modelURL)) {
|
||||
// we already have a request out for this asset, just store the QJsonValueRef
|
||||
// so we can do the hash replacement when the request comes back
|
||||
_pendingReplacements.insert(modelURL, jsonValue);
|
||||
} else if (_uploadedAssets.contains(modelURL)) {
|
||||
// we already have a hash for this asset
|
||||
// so just do the replacement immediately
|
||||
entityObject[MODEL_URL_KEY] = _uploadedAssets.value(modelURL).toString();
|
||||
jsonValue = entityObject;
|
||||
} else if (wantsToMigrateResource(modelURL)) {
|
||||
auto request = ResourceManager::createResourceRequest(this, modelURL);
|
||||
|
||||
if (request) {
|
||||
qCDebug(asset_migrator) << "Requesting" << modelURL << "for ATP asset migration";
|
||||
|
||||
// add this combination of QUrl and QJsonValueRef to our multi hash so we can change the URL
|
||||
// to an ATP one once ready
|
||||
_pendingReplacements.insert(modelURL, jsonValue);
|
||||
|
||||
connect(request, &ResourceRequest::finished, this, [=]() {
|
||||
if (request->getResult() == ResourceRequest::Success) {
|
||||
migrateResource(request);
|
||||
QString compoundURLString = entityObject.value(COMPOUND_SHAPE_URL_KEY).toString();
|
||||
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
bool isModelURL = (i == 0);
|
||||
quint8 replacementType = i;
|
||||
auto migrationURLString = (isModelURL) ? modelURLString : compoundURLString;
|
||||
|
||||
if (!migrationURLString.isEmpty()) {
|
||||
QUrl migrationURL = QUrl(migrationURLString);
|
||||
|
||||
if (!_ignoredUrls.contains(migrationURL)
|
||||
&& (migrationURL.scheme() == URL_SCHEME_HTTP || migrationURL.scheme() == URL_SCHEME_HTTPS
|
||||
|| migrationURL.scheme() == URL_SCHEME_FILE || migrationURL.scheme() == URL_SCHEME_FTP)) {
|
||||
|
||||
if (_pendingReplacements.contains(migrationURL)) {
|
||||
// we already have a request out for this asset, just store the QJsonValueRef
|
||||
// so we can do the hash replacement when the request comes back
|
||||
_pendingReplacements.insert(migrationURL, { jsonValue, replacementType });
|
||||
} else if (_uploadedAssets.contains(migrationURL)) {
|
||||
// we already have a hash for this asset
|
||||
// so just do the replacement immediately
|
||||
if (isModelURL) {
|
||||
entityObject[MODEL_URL_KEY] = _uploadedAssets.value(migrationURL).toString();
|
||||
} else {
|
||||
OffscreenUi::warning(_dialogParent, "Error",
|
||||
QString("Could not retrieve asset at %1").arg(modelURL.toString()));
|
||||
entityObject[COMPOUND_SHAPE_URL_KEY] = _uploadedAssets.value(migrationURL).toString();
|
||||
}
|
||||
request->deleteLater();
|
||||
});
|
||||
|
||||
request->send();
|
||||
} else {
|
||||
OffscreenUi::warning(_dialogParent, "Error",
|
||||
QString("Could not create request for asset at %1").arg(modelURL.toString()));
|
||||
|
||||
jsonValue = entityObject;
|
||||
} else if (wantsToMigrateResource(migrationURL)) {
|
||||
auto request = ResourceManager::createResourceRequest(this, migrationURL);
|
||||
|
||||
if (request) {
|
||||
qCDebug(asset_migrator) << "Requesting" << migrationURL << "for ATP asset migration";
|
||||
|
||||
// add this combination of QUrl and QJsonValueRef to our multi hash so we can change the URL
|
||||
// to an ATP one once ready
|
||||
_pendingReplacements.insert(migrationURL, { jsonValue, (isModelURL ? 0 : 1)});
|
||||
|
||||
connect(request, &ResourceRequest::finished, this, [=]() {
|
||||
if (request->getResult() == ResourceRequest::Success) {
|
||||
migrateResource(request);
|
||||
} else {
|
||||
++_errorCount;
|
||||
_pendingReplacements.remove(migrationURL);
|
||||
qWarning() << "Could not retrieve asset at" << migrationURL.toString();
|
||||
|
||||
checkIfFinished();
|
||||
}
|
||||
request->deleteLater();
|
||||
});
|
||||
|
||||
request->send();
|
||||
} else {
|
||||
++_errorCount;
|
||||
qWarning() << "Count not create request for asset at" << migrationURL.toString();
|
||||
}
|
||||
|
||||
} else {
|
||||
_ignoredUrls.insert(migrationURL);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
_ignoredUrls.insert(modelURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_doneReading = true;
|
||||
|
||||
checkIfFinished();
|
||||
|
||||
} else {
|
||||
OffscreenUi::warning(_dialogParent, "Error",
|
||||
|
@ -140,76 +158,109 @@ void ATPAssetMigrator::migrateResource(ResourceRequest* request) {
|
|||
// use an asset client to upload the asset
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
|
||||
QFileInfo assetInfo { request->getUrl().fileName() };
|
||||
|
||||
auto upload = assetClient->createUpload(request->getData(), assetInfo.completeSuffix());
|
||||
|
||||
if (upload) {
|
||||
// add this URL to our hash of AssetUpload to original URL
|
||||
_originalURLs.insert(upload, request->getUrl());
|
||||
|
||||
qCDebug(asset_migrator) << "Starting upload of asset from" << request->getUrl();
|
||||
|
||||
// connect to the finished signal so we know when the AssetUpload is done
|
||||
QObject::connect(upload, &AssetUpload::finished, this, &ATPAssetMigrator::assetUploadFinished);
|
||||
|
||||
// start the upload now
|
||||
upload->start();
|
||||
} else {
|
||||
// show a QMessageBox to say that there is no local asset server
|
||||
QString messageBoxText = QString("Could not upload \n\n%1\n\nbecause you are currently not connected" \
|
||||
" to a local asset-server.").arg(assetInfo.fileName());
|
||||
|
||||
QMessageBox::information(_dialogParent, "Failed to Upload", messageBoxText);
|
||||
}
|
||||
auto upload = assetClient->createUpload(request->getData());
|
||||
|
||||
// add this URL to our hash of AssetUpload to original URL
|
||||
_originalURLs.insert(upload, request->getUrl());
|
||||
|
||||
qCDebug(asset_migrator) << "Starting upload of asset from" << request->getUrl();
|
||||
|
||||
// connect to the finished signal so we know when the AssetUpload is done
|
||||
QObject::connect(upload, &AssetUpload::finished, this, &ATPAssetMigrator::assetUploadFinished);
|
||||
|
||||
// start the upload now
|
||||
upload->start();
|
||||
}
|
||||
|
||||
void ATPAssetMigrator::assetUploadFinished(AssetUpload *upload, const QString& hash) {
|
||||
// remove this migrationURL from the key for the AssetUpload pointer
|
||||
auto migrationURL = _originalURLs.take(upload);
|
||||
|
||||
if (upload->getError() == AssetUpload::NoError) {
|
||||
|
||||
const auto& modelURL = _originalURLs[upload];
|
||||
|
||||
// successfully uploaded asset - make any required replacements found in the pending replacements
|
||||
auto values = _pendingReplacements.values(modelURL);
|
||||
|
||||
|
||||
QString atpURL = getATPUrl(hash, upload->getExtension()).toString();
|
||||
|
||||
for (auto value : values) {
|
||||
// replace the modelURL in this QJsonValueRef with the hash
|
||||
QJsonObject valueObject = value.toObject();
|
||||
valueObject[MODEL_URL_KEY] = atpURL;
|
||||
value = valueObject;
|
||||
}
|
||||
|
||||
// add this URL to our list of uploaded assets
|
||||
_uploadedAssets.insert(modelURL, atpURL);
|
||||
|
||||
// pull the replaced models from _pendingReplacements
|
||||
_pendingReplacements.remove(modelURL);
|
||||
|
||||
// are we out of pending replacements? if so it is time to save the entity-server file
|
||||
if (_doneReading && _pendingReplacements.empty()) {
|
||||
saveEntityServerFile();
|
||||
|
||||
// reset after the attempted save, success or fail
|
||||
reset();
|
||||
}
|
||||
// use the path of the migrationURL to add a mapping in the Asset Server
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto setMappingRequest = assetClient->createSetMappingRequest(migrationURL.path(), hash);
|
||||
|
||||
connect(setMappingRequest, &SetMappingRequest::finished, this, &ATPAssetMigrator::setMappingFinished);
|
||||
|
||||
// add this migrationURL with the key for the SetMappingRequest pointer
|
||||
_originalURLs[setMappingRequest] = migrationURL;
|
||||
|
||||
setMappingRequest->start();
|
||||
} else {
|
||||
AssetUploadDialogFactory::showErrorDialog(upload, _dialogParent);
|
||||
// this is a fail for this modelURL, remove it from pending replacements
|
||||
_pendingReplacements.remove(migrationURL);
|
||||
|
||||
++_errorCount;
|
||||
qWarning() << "Failed to upload" << migrationURL << "- error was" << upload->getErrorString();
|
||||
}
|
||||
|
||||
checkIfFinished();
|
||||
|
||||
upload->deleteLater();
|
||||
}
|
||||
|
||||
void ATPAssetMigrator::setMappingFinished(SetMappingRequest* request) {
|
||||
// take the migrationURL for this SetMappingRequest
|
||||
auto migrationURL = _originalURLs.take(request);
|
||||
|
||||
if (request->getError() == MappingRequest::NoError) {
|
||||
|
||||
// successfully uploaded asset - make any required replacements found in the pending replacements
|
||||
auto values = _pendingReplacements.values(migrationURL);
|
||||
|
||||
QString atpURL = QString("atp:%1").arg(request->getPath());
|
||||
|
||||
for (auto value : values) {
|
||||
// replace the modelURL in this QJsonValueRef with the hash
|
||||
QJsonObject valueObject = value.first.toObject();
|
||||
|
||||
if (value.second == 0) {
|
||||
valueObject[MODEL_URL_KEY] = atpURL;
|
||||
} else {
|
||||
valueObject[COMPOUND_SHAPE_URL_KEY] = atpURL;
|
||||
}
|
||||
|
||||
value.first = valueObject;
|
||||
}
|
||||
|
||||
// add this URL to our list of uploaded assets
|
||||
_uploadedAssets.insert(migrationURL, atpURL);
|
||||
|
||||
// pull the replaced urls from _pendingReplacements
|
||||
_pendingReplacements.remove(migrationURL);
|
||||
} else {
|
||||
// this is a fail for this migrationURL, remove it from pending replacements
|
||||
_pendingReplacements.remove(migrationURL);
|
||||
|
||||
++_errorCount;
|
||||
qWarning() << "Error setting mapping for" << migrationURL << "- error was " << request->getErrorString();
|
||||
}
|
||||
|
||||
checkIfFinished();
|
||||
|
||||
request->deleteLater();
|
||||
}
|
||||
|
||||
void ATPAssetMigrator::checkIfFinished() {
|
||||
// are we out of pending replacements? if so it is time to save the entity-server file
|
||||
if (_doneReading && _pendingReplacements.empty()) {
|
||||
saveEntityServerFile();
|
||||
|
||||
// reset after the attempted save, success or fail
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
bool ATPAssetMigrator::wantsToMigrateResource(const QUrl& url) {
|
||||
static bool hasAskedForCompleteMigration { false };
|
||||
static bool wantsCompleteMigration { false };
|
||||
|
||||
if (!hasAskedForCompleteMigration) {
|
||||
// this is the first resource migration - ask the user if they just want to migrate everything
|
||||
static const QString COMPLETE_MIGRATION_TEXT { "Do you want to migrate all assets found in this entity-server file?\n\n"\
|
||||
"Select \"Yes\" to upload all discovered assets to the current asset-server immediately.\n\n"\
|
||||
static const QString COMPLETE_MIGRATION_TEXT { "Do you want to migrate all assets found in this entity-server file?\n"\
|
||||
"Select \"Yes\" to upload all discovered assets to the current asset-server immediately.\n"\
|
||||
"Select \"No\" to be prompted for each discovered asset."
|
||||
};
|
||||
|
||||
|
@ -236,7 +287,7 @@ bool ATPAssetMigrator::wantsToMigrateResource(const QUrl& url) {
|
|||
|
||||
void ATPAssetMigrator::saveEntityServerFile() {
|
||||
// show a dialog to ask the user where they want to save the file
|
||||
QString saveName = QFileDialog::getSaveFileName(_dialogParent, "Save Migrated Entities File");
|
||||
QString saveName = OffscreenUi::getSaveFileName(_dialogParent, "Save Migrated Entities File");
|
||||
|
||||
QFile saveFile { saveName };
|
||||
|
||||
|
@ -251,9 +302,16 @@ void ATPAssetMigrator::saveEntityServerFile() {
|
|||
|
||||
saveFile.write(jsonDataForFile);
|
||||
saveFile.close();
|
||||
|
||||
QMessageBox::information(_dialogParent, "Success",
|
||||
QString("Your new entities file has been saved at %1").arg(saveName));
|
||||
|
||||
QString infoMessage = QString("Your new entities file has been saved at\n%1.").arg(saveName);
|
||||
|
||||
if (_errorCount > 0) {
|
||||
infoMessage += QString("\nThere were %1 models that could not be migrated.\n").arg(_errorCount);
|
||||
infoMessage += "Check the warnings in your log for details.\n";
|
||||
infoMessage += "You can re-attempt migration on those models\nby restarting this process with the newly saved file.";
|
||||
}
|
||||
|
||||
OffscreenUi::information(_dialogParent, "Success", infoMessage);
|
||||
} else {
|
||||
OffscreenUi::warning(_dialogParent, "Error", "Could not gzip JSON data for new entities file.");
|
||||
}
|
||||
|
@ -271,4 +329,5 @@ void ATPAssetMigrator::reset() {
|
|||
_uploadedAssets.clear();
|
||||
_originalURLs.clear();
|
||||
_ignoredUrls.clear();
|
||||
_errorCount = 0;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
class AssetUpload;
|
||||
class ResourceRequest;
|
||||
class SetMappingRequest;
|
||||
|
||||
class ATPAssetMigrator : public QObject {
|
||||
Q_OBJECT
|
||||
|
@ -32,8 +33,11 @@ public slots:
|
|||
void loadEntityServerFile();
|
||||
private slots:
|
||||
void assetUploadFinished(AssetUpload* upload, const QString& hash);
|
||||
void setMappingFinished(SetMappingRequest* request);
|
||||
private:
|
||||
void migrateResource(ResourceRequest* request);
|
||||
|
||||
void checkIfFinished();
|
||||
void saveEntityServerFile();
|
||||
|
||||
void reset();
|
||||
|
@ -44,11 +48,14 @@ private:
|
|||
QJsonArray _entitiesArray;
|
||||
|
||||
bool _doneReading { false };
|
||||
|
||||
QMultiHash<QUrl, QJsonValueRef> _pendingReplacements;
|
||||
|
||||
using JSONTypePair = std::pair<QJsonValueRef, quint8>;
|
||||
|
||||
QMultiHash<QUrl, JSONTypePair> _pendingReplacements;
|
||||
QHash<QUrl, QUrl> _uploadedAssets;
|
||||
QHash<AssetUpload*, QUrl> _originalURLs;
|
||||
QHash<QObject*, QUrl> _originalURLs;
|
||||
QSet<QUrl> _ignoredUrls;
|
||||
int _errorCount { 0 };
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -972,7 +972,6 @@ void Avatar::renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, g
|
|||
glm::vec3 perpCos = glm::normalize(glm::cross(axis, perpSin));
|
||||
perpSin = glm::cross(perpCos, axis);
|
||||
|
||||
float anglea = 0.0f;
|
||||
float angleb = 0.0f;
|
||||
QVector<glm::vec3> points;
|
||||
|
||||
|
@ -980,7 +979,7 @@ void Avatar::renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, g
|
|||
|
||||
// the rectangles that comprise the sides of the cone section are
|
||||
// referenced by "a" and "b" in one dimension, and "1", and "2" in the other dimension.
|
||||
anglea = angleb;
|
||||
int anglea = angleb;
|
||||
angleb = ((float)(i+1) / (float)NUM_BODY_CONE_SIDES) * TWO_PI;
|
||||
|
||||
float sa = sinf(anglea);
|
||||
|
|
|
@ -57,7 +57,7 @@ class Avatar : public AvatarData {
|
|||
Q_PROPERTY(glm::vec3 skeletonOffset READ getSkeletonOffset WRITE setSkeletonOffset)
|
||||
|
||||
public:
|
||||
Avatar(RigPointer rig = nullptr);
|
||||
explicit Avatar(RigPointer rig = nullptr);
|
||||
~Avatar();
|
||||
|
||||
typedef render::Payload<AvatarData> Payload;
|
||||
|
|
|
@ -308,7 +308,6 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) {
|
|||
hand = _hand;
|
||||
}
|
||||
|
||||
ok = true;
|
||||
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
|
||||
holderID = myAvatar->getSessionUUID();
|
||||
|
||||
|
|
|
@ -63,11 +63,11 @@ void AvatarManager::registerMetaTypes(QScriptEngine* engine) {
|
|||
}
|
||||
|
||||
AvatarManager::AvatarManager(QObject* parent) :
|
||||
_avatarFades()
|
||||
_avatarFades(),
|
||||
_myAvatar(std::make_shared<MyAvatar>(std::make_shared<Rig>()))
|
||||
{
|
||||
// register a meta type for the weak pointer we'll use for the owning avatar mixer for each avatar
|
||||
qRegisterMetaType<QWeakPointer<Node> >("NodeWeakPointer");
|
||||
_myAvatar = std::make_shared<MyAvatar>(std::make_shared<Rig>());
|
||||
|
||||
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
||||
packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket");
|
||||
|
|
|
@ -71,8 +71,8 @@ public slots:
|
|||
void updateAvatarRenderStatus(bool shouldRenderAvatars);
|
||||
|
||||
private:
|
||||
AvatarManager(QObject* parent = 0);
|
||||
AvatarManager(const AvatarManager& other);
|
||||
explicit AvatarManager(QObject* parent = 0);
|
||||
explicit AvatarManager(const AvatarManager& other);
|
||||
|
||||
void simulateAvatarFades(float deltaTime);
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
#include <display-plugins/DisplayPlugin.h>
|
||||
#include "InterfaceLogging.h"
|
||||
|
||||
AvatarUpdate::AvatarUpdate() : GenericThread(), _lastAvatarUpdate(0) {
|
||||
AvatarUpdate::AvatarUpdate() : GenericThread(), _lastAvatarUpdate(0), _isHMDMode(false) {
|
||||
setObjectName("Avatar Update"); // GenericThread::initialize uses this to set the thread name.
|
||||
Settings settings;
|
||||
const int DEFAULT_TARGET_AVATAR_SIMRATE = 60;
|
||||
|
|
|
@ -28,7 +28,7 @@ class Avatar;
|
|||
|
||||
class Head : public HeadData {
|
||||
public:
|
||||
Head(Avatar* owningAvatar);
|
||||
explicit Head(Avatar* owningAvatar);
|
||||
|
||||
void init();
|
||||
void reset();
|
||||
|
|
|
@ -1522,9 +1522,9 @@ glm::vec3 MyAvatar::applyKeyboardMotor(float deltaTime, const glm::vec3& localVe
|
|||
// (1) braking --> short timescale (aggressive motor assertion)
|
||||
// (2) pushing --> medium timescale (mild motor assertion)
|
||||
// (3) inactive --> long timescale (gentle friction for low speeds)
|
||||
float MIN_KEYBOARD_MOTOR_TIMESCALE = 0.125f;
|
||||
float MAX_KEYBOARD_MOTOR_TIMESCALE = 0.4f;
|
||||
float MIN_KEYBOARD_BRAKE_SPEED = 0.3f;
|
||||
const float MIN_KEYBOARD_MOTOR_TIMESCALE = 0.125f;
|
||||
const float MAX_KEYBOARD_MOTOR_TIMESCALE = 0.4f;
|
||||
const float MIN_KEYBOARD_BRAKE_SPEED = 0.3f;
|
||||
float timescale = MAX_KEYBOARD_MOTOR_TIMESCALE;
|
||||
bool isThrust = (glm::length2(_thrust) > EPSILON);
|
||||
if (_isPushing || isThrust ||
|
||||
|
@ -1787,7 +1787,7 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition,
|
|||
<< newOrientation.x << ", " << newOrientation.y << ", " << newOrientation.z << ", " << newOrientation.w;
|
||||
|
||||
// orient the user to face the target
|
||||
glm::quat quatOrientation = newOrientation;
|
||||
glm::quat quatOrientation = cancelOutRollAndPitch(newOrientation);
|
||||
|
||||
if (shouldFaceLocation) {
|
||||
quatOrientation = newOrientation * glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
|
|
|
@ -63,9 +63,9 @@ class MyAvatar : public Avatar {
|
|||
Q_PROPERTY(AudioListenerMode audioListenerMode READ getAudioListenerMode WRITE setAudioListenerMode)
|
||||
Q_PROPERTY(glm::vec3 customListenPosition READ getCustomListenPosition WRITE setCustomListenPosition)
|
||||
Q_PROPERTY(glm::quat customListenOrientation READ getCustomListenOrientation WRITE setCustomListenOrientation)
|
||||
Q_PROPERTY(AudioListenerMode FROM_HEAD READ getAudioListenerModeHead)
|
||||
Q_PROPERTY(AudioListenerMode FROM_CAMERA READ getAudioListenerModeCamera)
|
||||
Q_PROPERTY(AudioListenerMode CUSTOM READ getAudioListenerModeCustom)
|
||||
Q_PROPERTY(AudioListenerMode audioListenerModeHead READ getAudioListenerModeHead)
|
||||
Q_PROPERTY(AudioListenerMode audioListenerModeCamera READ getAudioListenerModeCamera)
|
||||
Q_PROPERTY(AudioListenerMode audioListenerModeCustom READ getAudioListenerModeCustom)
|
||||
//TODO: make gravity feature work Q_PROPERTY(glm::vec3 gravity READ getGravity WRITE setGravity)
|
||||
|
||||
|
||||
|
@ -84,7 +84,7 @@ class MyAvatar : public Avatar {
|
|||
Q_PROPERTY(float energy READ getEnergy WRITE setEnergy)
|
||||
|
||||
public:
|
||||
MyAvatar(RigPointer rig);
|
||||
explicit MyAvatar(RigPointer rig);
|
||||
~MyAvatar();
|
||||
|
||||
virtual void simulateAttachments(float deltaTime) override;
|
||||
|
|
|
@ -46,14 +46,15 @@ void MyCharacterController::updateShapeIfNecessary() {
|
|||
// NOTE: _shapeLocalOffset is already computed
|
||||
|
||||
if (_radius > 0.0f) {
|
||||
// HACK: use some simple mass property defaults for now
|
||||
float mass = 100.0f;
|
||||
btVector3 inertia(30.0f, 8.0f, 30.0f);
|
||||
|
||||
// create RigidBody if it doesn't exist
|
||||
if (!_rigidBody) {
|
||||
|
||||
// HACK: use some simple mass property defaults for now
|
||||
const float DEFAULT_AVATAR_MASS = 100.0f;
|
||||
const btVector3 DEFAULT_AVATAR_INERTIA_TENSOR(30.0f, 8.0f, 30.0f);
|
||||
|
||||
btCollisionShape* shape = new btCapsuleShape(_radius, 2.0f * _halfHeight);
|
||||
_rigidBody = new btRigidBody(mass, nullptr, shape, inertia);
|
||||
_rigidBody = new btRigidBody(DEFAULT_AVATAR_MASS, nullptr, shape, DEFAULT_AVATAR_INERTIA_TENSOR);
|
||||
} else {
|
||||
btCollisionShape* shape = _rigidBody->getCollisionShape();
|
||||
if (shape) {
|
||||
|
|
|
@ -21,7 +21,7 @@ class MyAvatar;
|
|||
|
||||
class MyCharacterController : public CharacterController {
|
||||
public:
|
||||
MyCharacterController(MyAvatar* avatar);
|
||||
explicit MyCharacterController(MyAvatar* avatar);
|
||||
~MyCharacterController ();
|
||||
|
||||
virtual void updateShapeIfNecessary() override;
|
||||
|
|
|
@ -39,7 +39,7 @@ int main(int argc, const char* argv[]) {
|
|||
static const DWORD BUG_SPLAT_FLAGS = MDSF_PREVENTHIJACKING | MDSF_USEGUARDMEMORY;
|
||||
static const char* BUG_SPLAT_DATABASE = "interface_alpha";
|
||||
static const char* BUG_SPLAT_APPLICATION_NAME = "Interface";
|
||||
MiniDmpSender mpSender { BUG_SPLAT_DATABASE, BUG_SPLAT_APPLICATION_NAME, BuildInfo::VERSION.toLatin1().constData(),
|
||||
MiniDmpSender mpSender { BUG_SPLAT_DATABASE, BUG_SPLAT_APPLICATION_NAME, qPrintable(BuildInfo::VERSION),
|
||||
nullptr, BUG_SPLAT_FLAGS };
|
||||
#endif
|
||||
|
||||
|
@ -143,6 +143,14 @@ int main(int argc, const char* argv[]) {
|
|||
|
||||
QObject::connect(&server, &QLocalServer::newConnection, &app, &Application::handleLocalServerConnection);
|
||||
|
||||
#ifdef HAS_BUGSPLAT
|
||||
AccountManager& accountManager = AccountManager::getInstance();
|
||||
mpSender.setDefaultUserName(qPrintable(accountManager.getAccountInfo().getUsername()));
|
||||
QObject::connect(&accountManager, &AccountManager::usernameChanged, &app, [&mpSender](const QString& newUsername) {
|
||||
mpSender.setDefaultUserName(qPrintable(newUsername));
|
||||
});
|
||||
#endif
|
||||
|
||||
QTranslator translator;
|
||||
translator.load("i18n/interface_en");
|
||||
app.installTranslator(&translator);
|
||||
|
|
284
interface/src/scripting/AssetMappingsScriptingInterface.cpp
Normal file
284
interface/src/scripting/AssetMappingsScriptingInterface.cpp
Normal file
|
@ -0,0 +1,284 @@
|
|||
//
|
||||
// AssetMappingsScriptingInterface.cpp
|
||||
// libraries/script-engine/src
|
||||
//
|
||||
// Created by Ryan Huffman on 2016-03-09.
|
||||
// Copyright 2016 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 "AssetMappingsScriptingInterface.h"
|
||||
|
||||
#include <QtScript/QScriptEngine>
|
||||
#include <QtCore/QFile>
|
||||
|
||||
#include <AssetRequest.h>
|
||||
#include <AssetUpload.h>
|
||||
#include <MappingRequest.h>
|
||||
#include <NetworkLogging.h>
|
||||
#include <OffscreenUi.h>
|
||||
|
||||
AssetMappingsScriptingInterface::AssetMappingsScriptingInterface() {
|
||||
_proxyModel.setSourceModel(&_assetMappingModel);
|
||||
_proxyModel.setSortRole(Qt::DisplayRole);
|
||||
_proxyModel.setDynamicSortFilter(true);
|
||||
_proxyModel.sort(0);
|
||||
}
|
||||
|
||||
void AssetMappingsScriptingInterface::setMapping(QString path, QString hash, QJSValue callback) {
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto request = assetClient->createSetMappingRequest(path, hash);
|
||||
|
||||
connect(request, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable {
|
||||
if (callback.isCallable()) {
|
||||
QJSValueList args { request->getErrorString(), request->getPath() };
|
||||
callback.call(args);
|
||||
}
|
||||
|
||||
request->deleteLater();
|
||||
});
|
||||
|
||||
request->start();
|
||||
}
|
||||
|
||||
void AssetMappingsScriptingInterface::getMapping(QString path, QJSValue callback) {
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto request = assetClient->createGetMappingRequest(path);
|
||||
|
||||
connect(request, &GetMappingRequest::finished, this, [this, callback](GetMappingRequest* request) mutable {
|
||||
if (callback.isCallable()) {
|
||||
QJSValueList args { request->getErrorString() };
|
||||
callback.call(args);
|
||||
}
|
||||
|
||||
request->deleteLater();
|
||||
});
|
||||
|
||||
request->start();
|
||||
}
|
||||
|
||||
void AssetMappingsScriptingInterface::uploadFile(QString path, QString mapping, QJSValue startedCallback, QJSValue completedCallback, bool dropEvent) {
|
||||
static const QString helpText =
|
||||
"Upload your asset to a specific folder by entering the full path. Specifying\n"
|
||||
"a new folder name will automatically create that folder for you.";
|
||||
static const QString dropHelpText =
|
||||
"This file will be added to your Asset Server.\n"
|
||||
"Use the field below to place your file in a specific folder or to rename it.\n"
|
||||
"Specifying a new folder name will automatically create that folder for you.";
|
||||
|
||||
auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
||||
auto result = offscreenUi->inputDialog(OffscreenUi::ICON_INFORMATION, "Specify Asset Path",
|
||||
dropEvent ? dropHelpText : helpText, mapping);
|
||||
|
||||
if (!result.isValid()) {
|
||||
completedCallback.call({ -1 });
|
||||
return;
|
||||
}
|
||||
mapping = result.toString();
|
||||
mapping = mapping.trimmed();
|
||||
|
||||
if (mapping[0] != '/') {
|
||||
mapping = "/" + mapping;
|
||||
}
|
||||
|
||||
// Check for override
|
||||
if (isKnownMapping(mapping)) {
|
||||
auto message = mapping + "\n" + "This file already exists. Do you want to overwrite it?";
|
||||
auto button = offscreenUi->messageBox(OffscreenUi::ICON_QUESTION, "Overwrite File", message,
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||
if (button == QMessageBox::No) {
|
||||
completedCallback.call({ -1 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
startedCallback.call();
|
||||
|
||||
auto upload = DependencyManager::get<AssetClient>()->createUpload(path);
|
||||
QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable {
|
||||
if (upload->getError() != AssetUpload::NoError) {
|
||||
if (completedCallback.isCallable()) {
|
||||
QJSValueList args { upload->getErrorString() };
|
||||
completedCallback.call(args);
|
||||
}
|
||||
} else {
|
||||
setMapping(mapping, hash, completedCallback);
|
||||
}
|
||||
|
||||
upload->deleteLater();
|
||||
});
|
||||
|
||||
upload->start();
|
||||
}
|
||||
|
||||
void AssetMappingsScriptingInterface::deleteMappings(QStringList paths, QJSValue callback) {
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto request = assetClient->createDeleteMappingsRequest(paths);
|
||||
|
||||
connect(request, &DeleteMappingsRequest::finished, this, [this, callback](DeleteMappingsRequest* request) mutable {
|
||||
if (callback.isCallable()) {
|
||||
QJSValueList args { request->getErrorString() };
|
||||
callback.call(args);
|
||||
}
|
||||
|
||||
request->deleteLater();
|
||||
});
|
||||
|
||||
request->start();
|
||||
}
|
||||
|
||||
void AssetMappingsScriptingInterface::getAllMappings(QJSValue callback) {
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto request = assetClient->createGetAllMappingsRequest();
|
||||
|
||||
connect(request, &GetAllMappingsRequest::finished, this, [this, callback](GetAllMappingsRequest* request) mutable {
|
||||
auto mappings = request->getMappings();
|
||||
auto map = callback.engine()->newObject();
|
||||
|
||||
for (auto& kv : mappings ) {
|
||||
map.setProperty(kv.first, kv.second);
|
||||
}
|
||||
|
||||
if (callback.isCallable()) {
|
||||
QJSValueList args { request->getErrorString(), map };
|
||||
callback.call(args);
|
||||
}
|
||||
|
||||
request->deleteLater();
|
||||
});
|
||||
|
||||
request->start();
|
||||
}
|
||||
|
||||
void AssetMappingsScriptingInterface::renameMapping(QString oldPath, QString newPath, QJSValue callback) {
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto request = assetClient->createRenameMappingRequest(oldPath, newPath);
|
||||
|
||||
connect(request, &RenameMappingRequest::finished, this, [this, callback](RenameMappingRequest* request) mutable {
|
||||
if (callback.isCallable()) {
|
||||
QJSValueList args { request->getErrorString() };
|
||||
callback.call(args);
|
||||
}
|
||||
|
||||
request->deleteLater();
|
||||
});
|
||||
|
||||
request->start();
|
||||
}
|
||||
|
||||
bool AssetMappingModel::isKnownFolder(QString path) const {
|
||||
if (!path.endsWith("/")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto existingPaths = _pathToItemMap.keys();
|
||||
for (auto& entry : existingPaths) {
|
||||
if (entry.startsWith(path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int assetMappingModelMetatypeId = qRegisterMetaType<AssetMappingModel*>("AssetMappingModel*");
|
||||
|
||||
void AssetMappingModel::refresh() {
|
||||
qDebug() << "Refreshing asset mapping model";
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto request = assetClient->createGetAllMappingsRequest();
|
||||
|
||||
connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) mutable {
|
||||
if (request->getError() == MappingRequest::NoError) {
|
||||
auto mappings = request->getMappings();
|
||||
auto existingPaths = _pathToItemMap.keys();
|
||||
for (auto& mapping : mappings) {
|
||||
auto& path = mapping.first;
|
||||
auto parts = path.split("/");
|
||||
auto length = parts.length();
|
||||
|
||||
existingPaths.removeOne(mapping.first);
|
||||
|
||||
QString fullPath = "/";
|
||||
|
||||
QStandardItem* lastItem = nullptr;
|
||||
|
||||
// start index at 1 to avoid empty string from leading slash
|
||||
for (int i = 1; i < length; ++i) {
|
||||
fullPath += (i == 1 ? "" : "/") + parts[i];
|
||||
|
||||
auto it = _pathToItemMap.find(fullPath);
|
||||
if (it == _pathToItemMap.end()) {
|
||||
auto item = new QStandardItem(parts[i]);
|
||||
bool isFolder = i < length - 1;
|
||||
item->setData(isFolder ? fullPath + "/" : fullPath, Qt::UserRole);
|
||||
item->setData(isFolder, Qt::UserRole + 1);
|
||||
item->setData(parts[i], Qt::UserRole + 2);
|
||||
item->setData("atp:" + fullPath, Qt::UserRole + 3);
|
||||
item->setData(fullPath, Qt::UserRole + 4);
|
||||
if (lastItem) {
|
||||
lastItem->setChild(lastItem->rowCount(), 0, item);
|
||||
} else {
|
||||
appendRow(item);
|
||||
}
|
||||
|
||||
lastItem = item;
|
||||
_pathToItemMap[fullPath] = lastItem;
|
||||
} else {
|
||||
lastItem = it.value();
|
||||
}
|
||||
}
|
||||
|
||||
Q_ASSERT(fullPath == path);
|
||||
}
|
||||
|
||||
// Remove folders from list
|
||||
auto it = existingPaths.begin();
|
||||
while (it != existingPaths.end()) {
|
||||
Q_ASSERT(_pathToItemMap.contains(*it));
|
||||
auto item = _pathToItemMap[*it];
|
||||
if (item && item->data(Qt::UserRole + 1).toBool()) {
|
||||
it = existingPaths.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& path : existingPaths) {
|
||||
Q_ASSERT(_pathToItemMap.contains(path));
|
||||
|
||||
auto item = _pathToItemMap[path];
|
||||
|
||||
while (item) {
|
||||
// During each iteration, delete item
|
||||
QStandardItem* nextItem = nullptr;
|
||||
|
||||
auto fullPath = item->data(Qt::UserRole + 4).toString();
|
||||
auto parent = item->parent();
|
||||
if (parent) {
|
||||
parent->removeRow(item->row());
|
||||
if (parent->rowCount() > 0) {
|
||||
// The parent still contains children, set the nextItem to null so we stop processing
|
||||
nextItem = nullptr;
|
||||
} else {
|
||||
nextItem = parent;
|
||||
}
|
||||
} else {
|
||||
auto removed = removeRow(item->row());
|
||||
Q_ASSERT(removed);
|
||||
}
|
||||
|
||||
Q_ASSERT(_pathToItemMap.contains(fullPath));
|
||||
_pathToItemMap.remove(fullPath);
|
||||
|
||||
item = nextItem;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emit errorGettingMappings(request->getErrorString());
|
||||
}
|
||||
});
|
||||
|
||||
request->start();
|
||||
}
|
70
interface/src/scripting/AssetMappingsScriptingInterface.h
Normal file
70
interface/src/scripting/AssetMappingsScriptingInterface.h
Normal file
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// AssetMappingsScriptingInterface.h
|
||||
// libraries/script-engine/src
|
||||
//
|
||||
// Created by Ryan Huffman on 2016-03-09.
|
||||
// Copyright 2016 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
|
||||
|
||||
#ifndef hifi_AssetMappingsScriptingInterface_h
|
||||
#define hifi_AssetMappingsScriptingInterface_h
|
||||
|
||||
#include <QtCore/QObject>
|
||||
#include <QtScript/QScriptValue>
|
||||
|
||||
#include <AssetClient.h>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
|
||||
|
||||
class AssetMappingModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
public:
|
||||
Q_INVOKABLE void refresh();
|
||||
|
||||
bool isKnownMapping(QString path) const { return _pathToItemMap.contains(path); }
|
||||
bool isKnownFolder(QString path) const;
|
||||
|
||||
signals:
|
||||
void errorGettingMappings(QString errorString);
|
||||
|
||||
private:
|
||||
QHash<QString, QStandardItem*> _pathToItemMap;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(AssetMappingModel*);
|
||||
|
||||
class AssetMappingsScriptingInterface : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(AssetMappingModel* mappingModel READ getAssetMappingModel CONSTANT)
|
||||
Q_PROPERTY(QAbstractProxyModel* proxyModel READ getProxyModel CONSTANT)
|
||||
public:
|
||||
AssetMappingsScriptingInterface();
|
||||
|
||||
Q_INVOKABLE AssetMappingModel* getAssetMappingModel() { return &_assetMappingModel; }
|
||||
Q_INVOKABLE QAbstractProxyModel* getProxyModel() { return &_proxyModel; }
|
||||
|
||||
Q_INVOKABLE bool isKnownMapping(QString path) const { return _assetMappingModel.isKnownMapping(path); }
|
||||
Q_INVOKABLE bool isKnownFolder(QString path) const { return _assetMappingModel.isKnownFolder(path); }
|
||||
|
||||
Q_INVOKABLE void setMapping(QString path, QString hash, QJSValue callback = QJSValue());
|
||||
Q_INVOKABLE void getMapping(QString path, QJSValue callback = QJSValue());
|
||||
Q_INVOKABLE void uploadFile(QString path, QString mapping, QJSValue startedCallback = QJSValue(), QJSValue completedCallback = QJSValue(), bool dropEvent = false);
|
||||
Q_INVOKABLE void deleteMappings(QStringList paths, QJSValue callback);
|
||||
Q_INVOKABLE void deleteMapping(QString path, QJSValue callback) { deleteMappings(QStringList(path), callback = QJSValue()); }
|
||||
Q_INVOKABLE void getAllMappings(QJSValue callback = QJSValue());
|
||||
Q_INVOKABLE void renameMapping(QString oldPath, QString newPath, QJSValue callback = QJSValue());
|
||||
|
||||
protected:
|
||||
QSet<AssetRequest*> _pendingRequests;
|
||||
AssetMappingModel _assetMappingModel;
|
||||
QSortFilterProxyModel _proxyModel;
|
||||
};
|
||||
|
||||
|
||||
#endif // hifi_AssetMappingsScriptingInterface_h
|
|
@ -9,6 +9,7 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QtCore/QDir>
|
||||
#include <QMessageBox>
|
||||
#include <QScriptValue>
|
||||
|
@ -139,3 +140,8 @@ int WindowScriptingInterface::getX() {
|
|||
int WindowScriptingInterface::getY() {
|
||||
return qApp->getWindow()->y();
|
||||
}
|
||||
|
||||
void WindowScriptingInterface::copyToClipboard(const QString& text) {
|
||||
qDebug() << "Copying";
|
||||
QApplication::clipboard()->setText(text);
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ public slots:
|
|||
QScriptValue prompt(const QString& message = "", const QString& defaultText = "");
|
||||
QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
||||
QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
||||
void copyToClipboard(const QString& text);
|
||||
|
||||
signals:
|
||||
void domainChanged(const QString& domainHostname);
|
||||
|
|
|
@ -284,7 +284,8 @@ void ApplicationOverlay::buildFramebufferObject() {
|
|||
|
||||
// If the overlay framebuffer still has no color attachment, no textures were available for rendering, so build a new one
|
||||
if (!_overlayFramebuffer->getRenderBuffer(0)) {
|
||||
auto colorBuffer = gpu::TexturePointer(gpu::Texture::create2D(COLOR_FORMAT, width, height, DEFAULT_SAMPLER));
|
||||
const gpu::Sampler OVERLAY_SAMPLER(gpu::Sampler::FILTER_MIN_MAG_LINEAR, gpu::Sampler::WRAP_CLAMP);
|
||||
auto colorBuffer = gpu::TexturePointer(gpu::Texture::create2D(COLOR_FORMAT, width, height, OVERLAY_SAMPLER));
|
||||
_overlayFramebuffer->setRenderBuffer(0, colorBuffer);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
//
|
||||
// AssetUploadDialogFactory.cpp
|
||||
// interface/src/ui
|
||||
//
|
||||
// Created by Stephen Birarda on 2015-08-26.
|
||||
// Copyright 2015 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include "AssetUploadDialogFactory.h"
|
||||
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtWidgets/QDialogButtonBox>
|
||||
#include <QtWidgets/QFileDialog>
|
||||
#include <QtWidgets/QLabel>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include <QtWidgets/QLineEdit>
|
||||
#include <QtWidgets/QVBoxLayout>
|
||||
|
||||
#include <AssetClient.h>
|
||||
#include <AssetUpload.h>
|
||||
#include <AssetUtils.h>
|
||||
#include <NodeList.h>
|
||||
#include <OffscreenUi.h>
|
||||
#include <ResourceManager.h>
|
||||
|
||||
AssetUploadDialogFactory& AssetUploadDialogFactory::getInstance() {
|
||||
static AssetUploadDialogFactory staticInstance;
|
||||
return staticInstance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void AssetUploadDialogFactory::showDialog() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
if (nodeList->getThisNodeCanRez()) {
|
||||
auto filename = QFileDialog::getOpenFileName(_dialogParent, "Select a file to upload");
|
||||
|
||||
if (!filename.isEmpty()) {
|
||||
qDebug() << "Selected filename for upload to asset-server: " << filename;
|
||||
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto upload = assetClient->createUpload(filename);
|
||||
|
||||
if (upload) {
|
||||
// connect to the finished signal so we know when the AssetUpload is done
|
||||
QObject::connect(upload, &AssetUpload::finished, this, &AssetUploadDialogFactory::handleUploadFinished);
|
||||
|
||||
// start the upload now
|
||||
upload->start();
|
||||
} else {
|
||||
// show a QMessageBox to say that there is no local asset server
|
||||
QString messageBoxText = QString("Could not upload \n\n%1\n\nbecause you are currently not connected" \
|
||||
" to a local asset-server.").arg(QFileInfo(filename).fileName());
|
||||
|
||||
QMessageBox::information(_dialogParent, "Failed to Upload", messageBoxText);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// we don't have permission to upload to asset server in this domain - show the permission denied error
|
||||
showErrorDialog(nullptr, _dialogParent, AssetUpload::PERMISSION_DENIED_ERROR);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void AssetUploadDialogFactory::handleUploadFinished(AssetUpload* upload, const QString& hash) {
|
||||
if (upload->getError() == AssetUpload::NoError) {
|
||||
// show message box for successful upload, with copiable text for ATP hash
|
||||
QDialog* hashCopyDialog = new QDialog(_dialogParent);
|
||||
|
||||
// delete the dialog on close
|
||||
hashCopyDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
// set the window title
|
||||
hashCopyDialog->setWindowTitle(tr("Successful Asset Upload"));
|
||||
|
||||
// setup a layout for the contents of the dialog
|
||||
QVBoxLayout* boxLayout = new QVBoxLayout;
|
||||
|
||||
// set the label text (this shows above the text box)
|
||||
QLabel* lineEditLabel = new QLabel;
|
||||
lineEditLabel->setText(QString("ATP URL for %1").arg(QFileInfo(upload->getFilename()).fileName()));
|
||||
|
||||
// setup the line edit to hold the copiable text
|
||||
QLineEdit* lineEdit = new QLineEdit;
|
||||
|
||||
QString atpURL = QString("%1:%2.%3").arg(URL_SCHEME_ATP).arg(hash).arg(upload->getExtension());
|
||||
|
||||
// set the ATP URL as the text value so it's copiable
|
||||
lineEdit->insert(atpURL);
|
||||
|
||||
// figure out what size this line edit should be using font metrics
|
||||
QFontMetrics textMetrics { lineEdit->font() };
|
||||
|
||||
// set the fixed width on the line edit
|
||||
// pad it by 10 to cover the border and some extra space on the right side (for clicking)
|
||||
static const int LINE_EDIT_RIGHT_PADDING { 10 };
|
||||
|
||||
lineEdit->setFixedWidth(textMetrics.width(atpURL) + LINE_EDIT_RIGHT_PADDING );
|
||||
|
||||
// left align the ATP URL line edit
|
||||
lineEdit->home(true);
|
||||
|
||||
// add the label and line edit to the dialog
|
||||
boxLayout->addWidget(lineEditLabel);
|
||||
boxLayout->addWidget(lineEdit);
|
||||
|
||||
// setup an OK button to close the dialog
|
||||
QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, hashCopyDialog, &QDialog::close);
|
||||
boxLayout->addWidget(buttonBox);
|
||||
|
||||
// set the new layout on the dialog
|
||||
hashCopyDialog->setLayout(boxLayout);
|
||||
|
||||
// show the new dialog
|
||||
hashCopyDialog->show();
|
||||
} else {
|
||||
// display a message box with the error
|
||||
showErrorDialog(upload, _dialogParent);
|
||||
}
|
||||
|
||||
upload->deleteLater();
|
||||
}
|
||||
|
||||
void AssetUploadDialogFactory::showErrorDialog(AssetUpload* upload, QWidget* dialogParent, const QString& overrideMessage) {
|
||||
QString filename;
|
||||
|
||||
if (upload) {
|
||||
filename = QFileInfo { upload->getFilename() }.fileName();
|
||||
}
|
||||
|
||||
QString errorMessage = overrideMessage;
|
||||
|
||||
if (errorMessage.isEmpty() && upload) {
|
||||
errorMessage = upload->getErrorString();
|
||||
}
|
||||
|
||||
QString dialogMessage;
|
||||
|
||||
if (upload) {
|
||||
dialogMessage += QString("Failed to upload %1.\n\n").arg(filename);
|
||||
}
|
||||
|
||||
dialogMessage += errorMessage;
|
||||
|
||||
OffscreenUi::warning(dialogParent, "Failed Upload", dialogMessage);
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
//
|
||||
// AssetUploadDialogFactory.h
|
||||
// interface/src/ui
|
||||
//
|
||||
// Created by Stephen Birarda on 2015-08-26.
|
||||
// Copyright 2015 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef hifi_AssetUploadDialogFactory_h
|
||||
#define hifi_AssetUploadDialogFactory_h
|
||||
|
||||
#include <QtCore/QObject>
|
||||
|
||||
class AssetUpload;
|
||||
|
||||
class AssetUploadDialogFactory : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
AssetUploadDialogFactory(const AssetUploadDialogFactory& other) = delete;
|
||||
AssetUploadDialogFactory& operator=(const AssetUploadDialogFactory& rhs) = delete;
|
||||
|
||||
static AssetUploadDialogFactory& getInstance();
|
||||
static void showErrorDialog(AssetUpload* upload, QWidget* dialogParent, const QString& overrideMessage = QString());
|
||||
|
||||
void setDialogParent(QWidget* dialogParent) { _dialogParent = dialogParent; }
|
||||
|
||||
public slots:
|
||||
void showDialog();
|
||||
void handleUploadFinished(AssetUpload* upload, const QString& hash);
|
||||
|
||||
private:
|
||||
AssetUploadDialogFactory() = default;
|
||||
|
||||
|
||||
|
||||
QWidget* _dialogParent { nullptr };
|
||||
};
|
||||
|
||||
#endif // hifi_AssetUploadDialogFactory_h
|
|
@ -193,6 +193,7 @@ void Stats::updateStats(bool force) {
|
|||
}
|
||||
|
||||
STAT_UPDATE(downloads, ResourceCache::getLoadingRequests().size());
|
||||
STAT_UPDATE(downloadLimit, ResourceCache::getRequestLimit())
|
||||
STAT_UPDATE(downloadsPending, ResourceCache::getPendingRequestCount());
|
||||
// TODO fix to match original behavior
|
||||
//stringstream downloads;
|
||||
|
|
|
@ -56,6 +56,7 @@ class Stats : public QQuickItem {
|
|||
STATS_PROPERTY(int, audioMixerKbps, 0)
|
||||
STATS_PROPERTY(int, audioMixerPps, 0)
|
||||
STATS_PROPERTY(int, downloads, 0)
|
||||
STATS_PROPERTY(int, downloadLimit, 0)
|
||||
STATS_PROPERTY(int, downloadsPending, 0)
|
||||
STATS_PROPERTY(int, triangles, 0)
|
||||
STATS_PROPERTY(int, quads, 0)
|
||||
|
@ -135,6 +136,7 @@ signals:
|
|||
void audioMixerKbpsChanged();
|
||||
void audioMixerPpsChanged();
|
||||
void downloadsChanged();
|
||||
void downloadLimitChanged();
|
||||
void downloadsPendingChanged();
|
||||
void trianglesChanged();
|
||||
void quadsChanged();
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
class AnimExpression {
|
||||
public:
|
||||
friend class AnimTests;
|
||||
AnimExpression(const QString& str);
|
||||
explicit AnimExpression(const QString& str);
|
||||
protected:
|
||||
struct Token {
|
||||
enum Type {
|
||||
|
@ -49,8 +49,8 @@ protected:
|
|||
Comma,
|
||||
Error
|
||||
};
|
||||
Token(Type type) : type {type} {}
|
||||
Token(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {}
|
||||
explicit Token(Type type) : type {type} {}
|
||||
explicit Token(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {}
|
||||
explicit Token(int val) : type {Type::Int}, intVal {val} {}
|
||||
explicit Token(bool val) : type {Type::Bool}, intVal {val} {}
|
||||
explicit Token(float val) : type {Type::Float}, floatVal {val} {}
|
||||
|
@ -82,7 +82,7 @@ protected:
|
|||
Modulus,
|
||||
UnaryMinus
|
||||
};
|
||||
OpCode(Type type) : type {type} {}
|
||||
explicit OpCode(Type type) : type {type} {}
|
||||
explicit OpCode(const QStringRef& strRef) : type {Type::Identifier}, strVal {strRef.toString()} {}
|
||||
explicit OpCode(const QString& str) : type {Type::Identifier}, strVal {str} {}
|
||||
explicit OpCode(int val) : type {Type::Int}, intVal {val} {}
|
||||
|
|
|
@ -23,6 +23,8 @@ AnimInverseKinematics::AnimInverseKinematics(const QString& id) : AnimNode(AnimN
|
|||
|
||||
AnimInverseKinematics::~AnimInverseKinematics() {
|
||||
clearConstraints();
|
||||
_accumulators.clear();
|
||||
_targetVarVec.clear();
|
||||
}
|
||||
|
||||
void AnimInverseKinematics::loadDefaultPoses(const AnimPoseVec& poses) {
|
||||
|
@ -394,6 +396,17 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
|
|||
}
|
||||
_relativePoses[i].trans = underPoses[i].trans;
|
||||
}
|
||||
|
||||
if (!_relativePoses.empty()) {
|
||||
// Sometimes the underpose itself can violate the constraints. Rather than
|
||||
// clamp the animation we dynamically expand each constraint to accomodate it.
|
||||
std::map<int, RotationConstraint*>::iterator constraintItr = _constraints.begin();
|
||||
while (constraintItr != _constraints.end()) {
|
||||
int index = constraintItr->first;
|
||||
constraintItr->second->dynamicallyAdjustLimits(_relativePoses[index].rot);
|
||||
++constraintItr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_relativePoses.empty()) {
|
||||
|
|
|
@ -25,7 +25,7 @@ class RotationConstraint;
|
|||
class AnimInverseKinematics : public AnimNode {
|
||||
public:
|
||||
|
||||
AnimInverseKinematics(const QString& id);
|
||||
explicit AnimInverseKinematics(const QString& id);
|
||||
virtual ~AnimInverseKinematics() override;
|
||||
|
||||
void loadDefaultPoses(const AnimPoseVec& poses);
|
||||
|
|
|
@ -25,7 +25,7 @@ class AnimNodeLoader : public QObject {
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AnimNodeLoader(const QUrl& url);
|
||||
explicit AnimNodeLoader(const QUrl& url);
|
||||
|
||||
signals:
|
||||
void success(AnimNode::Pointer node);
|
||||
|
|
|
@ -23,8 +23,8 @@ public:
|
|||
using Pointer = std::shared_ptr<AnimSkeleton>;
|
||||
using ConstPointer = std::shared_ptr<const AnimSkeleton>;
|
||||
|
||||
AnimSkeleton(const FBXGeometry& fbxGeometry);
|
||||
AnimSkeleton(const std::vector<FBXJoint>& joints);
|
||||
explicit AnimSkeleton(const FBXGeometry& fbxGeometry);
|
||||
explicit AnimSkeleton(const std::vector<FBXJoint>& joints);
|
||||
int nameToJointIndex(const QString& jointName) const;
|
||||
const QString& getJointName(int jointIndex) const;
|
||||
int getNumJoints() const;
|
||||
|
|
|
@ -110,7 +110,7 @@ protected:
|
|||
|
||||
public:
|
||||
|
||||
AnimStateMachine(const QString& id);
|
||||
explicit AnimStateMachine(const QString& id);
|
||||
virtual ~AnimStateMachine() override;
|
||||
|
||||
virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override;
|
||||
|
|
|
@ -37,12 +37,12 @@ public:
|
|||
static const AnimVariant False;
|
||||
|
||||
AnimVariant() : _type(Type::Bool) { memset(&_val, 0, sizeof(_val)); }
|
||||
AnimVariant(bool value) : _type(Type::Bool) { _val.boolVal = value; }
|
||||
AnimVariant(int value) : _type(Type::Int) { _val.intVal = value; }
|
||||
AnimVariant(float value) : _type(Type::Float) { _val.floats[0] = value; }
|
||||
AnimVariant(const glm::vec3& value) : _type(Type::Vec3) { *reinterpret_cast<glm::vec3*>(&_val) = value; }
|
||||
AnimVariant(const glm::quat& value) : _type(Type::Quat) { *reinterpret_cast<glm::quat*>(&_val) = value; }
|
||||
AnimVariant(const QString& value) : _type(Type::String) { _stringVal = value; }
|
||||
explicit AnimVariant(bool value) : _type(Type::Bool) { _val.boolVal = value; }
|
||||
explicit AnimVariant(int value) : _type(Type::Int) { _val.intVal = value; }
|
||||
explicit AnimVariant(float value) : _type(Type::Float) { _val.floats[0] = value; }
|
||||
explicit AnimVariant(const glm::vec3& value) : _type(Type::Vec3) { *reinterpret_cast<glm::vec3*>(&_val) = value; }
|
||||
explicit AnimVariant(const glm::quat& value) : _type(Type::Quat) { *reinterpret_cast<glm::quat*>(&_val) = value; }
|
||||
explicit AnimVariant(const QString& value) : _type(Type::String) { _stringVal = value; }
|
||||
|
||||
bool isBool() const { return _type == Type::Bool; }
|
||||
bool isInt() const { return _type == Type::Int; }
|
||||
|
@ -250,7 +250,7 @@ public:
|
|||
qCDebug(animation) << " " << pair.first << "=" << pair.second.getString();
|
||||
break;
|
||||
default:
|
||||
assert("AnimVariant::Type" == "valid");
|
||||
assert(("invalid AnimVariant::Type", false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ protected:
|
|||
virtual QSharedPointer<Resource> createResource(const QUrl& url,
|
||||
const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra);
|
||||
private:
|
||||
AnimationCache(QObject* parent = NULL);
|
||||
explicit AnimationCache(QObject* parent = NULL);
|
||||
virtual ~AnimationCache() { }
|
||||
|
||||
};
|
||||
|
@ -51,7 +51,7 @@ class Animation : public Resource {
|
|||
|
||||
public:
|
||||
|
||||
Animation(const QUrl& url);
|
||||
explicit Animation(const QUrl& url);
|
||||
|
||||
const FBXGeometry& getGeometry() const { return *_geometry; }
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ AnimationLoop::AnimationLoop(const AnimationDetails& animationDetails) :
|
|||
_lastFrame(animationDetails.lastFrame),
|
||||
_running(animationDetails.running),
|
||||
_currentFrame(animationDetails.currentFrame),
|
||||
_maxFrameIndexHint(MAXIMUM_POSSIBLE_FRAME),
|
||||
_resetOnRunning(true),
|
||||
_lastSimulated(usecTimestampNow())
|
||||
{
|
||||
|
@ -55,6 +56,7 @@ AnimationLoop::AnimationLoop(float fps, bool loop, bool hold, bool startAutomati
|
|||
_lastFrame(lastFrame),
|
||||
_running(running),
|
||||
_currentFrame(currentFrame),
|
||||
_maxFrameIndexHint(MAXIMUM_POSSIBLE_FRAME),
|
||||
_resetOnRunning(true),
|
||||
_lastSimulated(usecTimestampNow())
|
||||
{
|
||||
|
|
|
@ -19,7 +19,7 @@ public:
|
|||
static const float MAXIMUM_POSSIBLE_FRAME;
|
||||
|
||||
AnimationLoop();
|
||||
AnimationLoop(const AnimationDetails& animationDetails);
|
||||
explicit AnimationLoop(const AnimationDetails& animationDetails);
|
||||
AnimationLoop(float fps, bool loop, bool hold, bool startAutomatically, float firstFrame,
|
||||
float lastFrame, bool running, float currentFrame);
|
||||
|
||||
|
|
|
@ -37,17 +37,7 @@ static bool isEqual(const glm::quat& p, const glm::quat& q) {
|
|||
return 1.0f - fabsf(glm::dot(p, q)) <= EPSILON;
|
||||
}
|
||||
|
||||
#ifdef NDEBUG
|
||||
#define ASSERT(cond)
|
||||
#else
|
||||
#define ASSERT(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
int* ptr = nullptr; \
|
||||
*ptr = 10; \
|
||||
} \
|
||||
} while (0)
|
||||
#endif
|
||||
#define ASSERT(cond) assert(cond)
|
||||
|
||||
// 2 meter tall dude
|
||||
const glm::vec3 DEFAULT_RIGHT_EYE_POS(-0.3f, 0.9f, 0.0f);
|
||||
|
|
|
@ -83,6 +83,7 @@ public:
|
|||
Hover
|
||||
};
|
||||
|
||||
Rig() {}
|
||||
virtual ~Rig() {}
|
||||
|
||||
void destroyAnimGraph();
|
||||
|
|
|
@ -31,6 +31,10 @@ public:
|
|||
/// \return true if this constraint is part of lower spine
|
||||
virtual bool isLowerSpine() const { return false; }
|
||||
|
||||
/// \param rotation rotation to allow
|
||||
/// \brief clear previous adjustment and adjust constraint limits to allow rotation
|
||||
virtual void dynamicallyAdjustLimits(const glm::quat& rotation) {}
|
||||
|
||||
protected:
|
||||
glm::quat _referenceRotation = glm::quat();
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#include <math.h>
|
||||
|
||||
#include <GeometryUtil.h>
|
||||
#include <GLMHelpers.h>
|
||||
#include <NumericalConstants.h>
|
||||
|
||||
|
||||
|
@ -24,32 +25,152 @@ const int LAST_CLAMP_NO_BOUNDARY = 0;
|
|||
const int LAST_CLAMP_HIGH_BOUNDARY = 1;
|
||||
|
||||
SwingTwistConstraint::SwingLimitFunction::SwingLimitFunction() {
|
||||
setCone(PI);
|
||||
_minDots.push_back(-1.0f);
|
||||
_minDots.push_back(-1.0f);
|
||||
|
||||
_minDotIndexA = -1;
|
||||
_minDotIndexB = -1;
|
||||
}
|
||||
|
||||
void SwingTwistConstraint::SwingLimitFunction::setCone(float maxAngle) {
|
||||
_minDots.clear();
|
||||
float minDot = glm::clamp(maxAngle, MIN_MINDOT, MAX_MINDOT);
|
||||
_minDots.push_back(minDot);
|
||||
// push the first value to the back to establish cyclic boundary conditions
|
||||
_minDots.push_back(minDot);
|
||||
}
|
||||
// In order to support the dynamic adjustment to swing limits we require
|
||||
// that minDots have a minimum number of elements:
|
||||
const int MIN_NUM_DOTS = 8;
|
||||
|
||||
void SwingTwistConstraint::SwingLimitFunction::setMinDots(const std::vector<float>& minDots) {
|
||||
uint32_t numDots = (uint32_t)minDots.size();
|
||||
int numDots = (int)minDots.size();
|
||||
_minDots.clear();
|
||||
if (numDots == 0) {
|
||||
// push two copies of MIN_MINDOT
|
||||
_minDots.push_back(MIN_MINDOT);
|
||||
// push multiple copies of MIN_MINDOT
|
||||
for (int i = 0; i < MIN_NUM_DOTS; ++i) {
|
||||
_minDots.push_back(MIN_MINDOT);
|
||||
}
|
||||
// push one more for cyclic boundary conditions
|
||||
_minDots.push_back(MIN_MINDOT);
|
||||
} else {
|
||||
_minDots.reserve(numDots);
|
||||
for (uint32_t i = 0; i < numDots; ++i) {
|
||||
_minDots.push_back(glm::clamp(minDots[i], MIN_MINDOT, MAX_MINDOT));
|
||||
// for minimal fidelity in the dynamic adjustment we expand the swing limit data until
|
||||
// we have enough data points
|
||||
int trueNumDots = numDots;
|
||||
int numFiller = 0;
|
||||
while(trueNumDots < MIN_NUM_DOTS) {
|
||||
numFiller++;
|
||||
trueNumDots += numDots;
|
||||
}
|
||||
// push the first value to the back to establish cyclic boundary conditions
|
||||
_minDots.reserve(trueNumDots);
|
||||
|
||||
for (int i = 0; i < numDots; ++i) {
|
||||
// push the next value
|
||||
_minDots.push_back(glm::clamp(minDots[i], MIN_MINDOT, MAX_MINDOT));
|
||||
|
||||
if (numFiller > 0) {
|
||||
// compute endpoints of line segment
|
||||
float nearDot = glm::clamp(minDots[i], MIN_MINDOT, MAX_MINDOT);
|
||||
int k = (i + 1) % numDots;
|
||||
float farDot = glm::clamp(minDots[k], MIN_MINDOT, MAX_MINDOT);
|
||||
|
||||
// fill the gap with interpolated values
|
||||
for (int j = 0; j < numFiller; ++j) {
|
||||
float delta = (float)(j + 1) / float(numFiller + 1);
|
||||
_minDots.push_back((1.0f - delta) * nearDot + delta * farDot);
|
||||
}
|
||||
}
|
||||
}
|
||||
// push the first value to the back to for cyclic boundary conditions
|
||||
_minDots.push_back(_minDots[0]);
|
||||
}
|
||||
_minDotIndexA = -1;
|
||||
_minDotIndexB = -1;
|
||||
}
|
||||
|
||||
/// \param angle radian angle to update
|
||||
/// \param minDotAdjustment minimum dot limit at that angle
|
||||
void SwingTwistConstraint::SwingLimitFunction::dynamicallyAdjustMinDots(float theta, float minDotAdjustment) {
|
||||
// What does "dynamic adjustment" mean?
|
||||
//
|
||||
// Consider a limitFunction that looks like this:
|
||||
//
|
||||
// 1+
|
||||
// | valid space
|
||||
// |
|
||||
// +-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
// |
|
||||
// | invalid space
|
||||
// 0+------------------------------------------------
|
||||
// 0 pi/2 pi 3pi/2 2pi
|
||||
// theta --->
|
||||
//
|
||||
// If we wanted to modify the envelope to accept a single invalid point X
|
||||
// then we would need to modify neighboring values A and B accordingly:
|
||||
//
|
||||
// 1+ adjustment for X at some thetaX
|
||||
// | |
|
||||
// | |
|
||||
// +-----+. V .+-----+-----+-----+-----+
|
||||
// | - -
|
||||
// | ' A--X--B '
|
||||
// 0+------------------------------------------------
|
||||
// 0 pi/2 pi 3pi/2 2pi
|
||||
//
|
||||
// The code below computes the values of A and B such that the line between them
|
||||
// passes through the point X, and we get reasonable interpolation for nearby values
|
||||
// of theta. The old AB values are saved for later restore.
|
||||
|
||||
if (_minDotIndexA > -1) {
|
||||
// retstore old values
|
||||
_minDots[_minDotIndexA] = _minDotA;
|
||||
_minDots[_minDotIndexB] = _minDotB;
|
||||
|
||||
// handle cyclic boundary conditions
|
||||
int lastIndex = (int)_minDots.size() - 1;
|
||||
if (_minDotIndexA == 0) {
|
||||
_minDots[lastIndex] = _minDotA;
|
||||
} else if (_minDotIndexB == lastIndex) {
|
||||
_minDots[0] = _minDotB;
|
||||
}
|
||||
}
|
||||
|
||||
// extract the positive normalized fractional part of the theta
|
||||
float integerPart;
|
||||
float normalizedAngle = modff(theta / TWO_PI, &integerPart);
|
||||
if (normalizedAngle < 0.0f) {
|
||||
normalizedAngle += 1.0f;
|
||||
}
|
||||
|
||||
// interpolate between the two nearest points in the curve
|
||||
float delta = modff(normalizedAngle * (float)(_minDots.size() - 1), &integerPart);
|
||||
int indexA = (int)(integerPart);
|
||||
int indexB = (indexA + 1) % _minDots.size();
|
||||
float interpolatedDot = _minDots[indexA] * (1.0f - delta) + _minDots[indexB] * delta;
|
||||
|
||||
if (minDotAdjustment < interpolatedDot) {
|
||||
// minDotAdjustment is outside the existing bounds so we must modify
|
||||
|
||||
// remember the indices
|
||||
_minDotIndexA = indexA;
|
||||
_minDotIndexB = indexB;
|
||||
|
||||
// save the old minDots
|
||||
_minDotA = _minDots[_minDotIndexA];
|
||||
_minDotB = _minDots[_minDotIndexB];
|
||||
|
||||
// compute replacement values to _minDots that will provide a line segment
|
||||
// that passes through minDotAdjustment while balancing the distortion between A and B.
|
||||
// Note: the derivation of these formulae is left as an exercise to the reader.
|
||||
float twiceUndershoot = 2.0f * (minDotAdjustment - interpolatedDot);
|
||||
_minDots[_minDotIndexA] -= twiceUndershoot * (delta + 0.5f) * (delta - 1.0f);
|
||||
_minDots[_minDotIndexB] -= twiceUndershoot * delta * (delta - 1.5f);
|
||||
|
||||
// handle cyclic boundary conditions
|
||||
int lastIndex = (int)_minDots.size() - 1;
|
||||
if (_minDotIndexA == 0) {
|
||||
_minDots[lastIndex] = _minDots[_minDotIndexA];
|
||||
} else if (_minDotIndexB == lastIndex) {
|
||||
_minDots[0] = _minDots[_minDotIndexB];
|
||||
}
|
||||
} else {
|
||||
// minDotAdjustment is inside bounds so there is nothing to do
|
||||
_minDotIndexA = -1;
|
||||
_minDotIndexB = -1;
|
||||
}
|
||||
}
|
||||
|
||||
float SwingTwistConstraint::SwingLimitFunction::getMinDot(float theta) const {
|
||||
|
@ -90,15 +211,14 @@ void SwingTwistConstraint::setSwingLimits(const std::vector<glm::vec3>& swungDir
|
|||
};
|
||||
std::vector<SwingLimitData> limits;
|
||||
|
||||
uint32_t numLimits = (uint32_t)swungDirections.size();
|
||||
int numLimits = (int)swungDirections.size();
|
||||
limits.reserve(numLimits);
|
||||
|
||||
// compute the limit pairs: <theta, minDot>
|
||||
const glm::vec3 yAxis = glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
for (uint32_t i = 0; i < numLimits; ++i) {
|
||||
for (int i = 0; i < numLimits; ++i) {
|
||||
float directionLength = glm::length(swungDirections[i]);
|
||||
if (directionLength > EPSILON) {
|
||||
glm::vec3 swingAxis = glm::cross(yAxis, swungDirections[i]);
|
||||
glm::vec3 swingAxis = glm::cross(Vectors::UNIT_Y, swungDirections[i]);
|
||||
float theta = atan2f(-swingAxis.z, swingAxis.x);
|
||||
if (theta < 0.0f) {
|
||||
theta += TWO_PI;
|
||||
|
@ -108,7 +228,7 @@ void SwingTwistConstraint::setSwingLimits(const std::vector<glm::vec3>& swungDir
|
|||
}
|
||||
|
||||
std::vector<float> minDots;
|
||||
numLimits = (uint32_t)limits.size();
|
||||
numLimits = (int)limits.size();
|
||||
if (numLimits == 0) {
|
||||
// trivial case: nearly free constraint
|
||||
std::vector<float> minDots;
|
||||
|
@ -126,10 +246,10 @@ void SwingTwistConstraint::setSwingLimits(const std::vector<glm::vec3>& swungDir
|
|||
|
||||
// extrapolate evenly distributed limits for fast lookup table
|
||||
float deltaTheta = TWO_PI / (float)(numLimits);
|
||||
uint32_t rightIndex = 0;
|
||||
for (uint32_t i = 0; i < numLimits; ++i) {
|
||||
int rightIndex = 0;
|
||||
for (int i = 0; i < numLimits; ++i) {
|
||||
float theta = (float)i * deltaTheta;
|
||||
uint32_t leftIndex = (rightIndex - 1) % numLimits;
|
||||
int leftIndex = (rightIndex - 1 + numLimits) % numLimits;
|
||||
while (rightIndex < numLimits && theta > limits[rightIndex]._theta) {
|
||||
leftIndex = rightIndex++;
|
||||
}
|
||||
|
@ -165,51 +285,57 @@ void SwingTwistConstraint::setTwistLimits(float minTwist, float maxTwist) {
|
|||
_maxTwist = glm::max(minTwist, maxTwist);
|
||||
|
||||
_lastTwistBoundary = LAST_CLAMP_NO_BOUNDARY;
|
||||
_twistAdjusted = false;
|
||||
}
|
||||
|
||||
// private
|
||||
float SwingTwistConstraint::handleTwistBoundaryConditions(float twistAngle) const {
|
||||
// adjust measured twistAngle according to clamping history
|
||||
switch (_lastTwistBoundary) {
|
||||
case LAST_CLAMP_LOW_BOUNDARY:
|
||||
// clamp to min
|
||||
if (twistAngle > _maxTwist) {
|
||||
twistAngle -= TWO_PI;
|
||||
}
|
||||
break;
|
||||
case LAST_CLAMP_HIGH_BOUNDARY:
|
||||
// clamp to max
|
||||
if (twistAngle < _minTwist) {
|
||||
twistAngle += TWO_PI;
|
||||
}
|
||||
break;
|
||||
default: // LAST_CLAMP_NO_BOUNDARY
|
||||
// clamp to nearest boundary
|
||||
float midBoundary = 0.5f * (_maxTwist + _minTwist + TWO_PI);
|
||||
if (twistAngle > midBoundary) {
|
||||
// lower boundary is closer --> phase down one cycle
|
||||
twistAngle -= TWO_PI;
|
||||
} else if (twistAngle < midBoundary - TWO_PI) {
|
||||
// higher boundary is closer --> phase up one cycle
|
||||
twistAngle += TWO_PI;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return twistAngle;
|
||||
}
|
||||
|
||||
bool SwingTwistConstraint::apply(glm::quat& rotation) const {
|
||||
// decompose the rotation into first twist about yAxis, then swing about something perp
|
||||
const glm::vec3 yAxis(0.0f, 1.0f, 0.0f);
|
||||
// NOTE: rotation = postRotation * referenceRotation
|
||||
glm::quat postRotation = rotation * glm::inverse(_referenceRotation);
|
||||
glm::quat swingRotation, twistRotation;
|
||||
swingTwistDecomposition(postRotation, yAxis, swingRotation, twistRotation);
|
||||
swingTwistDecomposition(postRotation, Vectors::UNIT_Y, swingRotation, twistRotation);
|
||||
// NOTE: postRotation = swingRotation * twistRotation
|
||||
|
||||
// compute twistAngle
|
||||
// compute raw twistAngle
|
||||
float twistAngle = 2.0f * acosf(fabsf(twistRotation.w));
|
||||
const glm::vec3 xAxis = glm::vec3(1.0f, 0.0f, 0.0f);
|
||||
glm::vec3 twistedX = twistRotation * xAxis;
|
||||
twistAngle *= copysignf(1.0f, glm::dot(glm::cross(xAxis, twistedX), yAxis));
|
||||
glm::vec3 twistedX = twistRotation * Vectors::UNIT_X;
|
||||
twistAngle *= copysignf(1.0f, glm::dot(glm::cross(Vectors::UNIT_X, twistedX), Vectors::UNIT_Y));
|
||||
|
||||
bool somethingClamped = false;
|
||||
if (_minTwist != _maxTwist) {
|
||||
// adjust measured twistAngle according to clamping history
|
||||
switch (_lastTwistBoundary) {
|
||||
case LAST_CLAMP_LOW_BOUNDARY:
|
||||
// clamp to min
|
||||
if (twistAngle > _maxTwist) {
|
||||
twistAngle -= TWO_PI;
|
||||
}
|
||||
break;
|
||||
case LAST_CLAMP_HIGH_BOUNDARY:
|
||||
// clamp to max
|
||||
if (twistAngle < _minTwist) {
|
||||
twistAngle += TWO_PI;
|
||||
}
|
||||
break;
|
||||
default: // LAST_CLAMP_NO_BOUNDARY
|
||||
// clamp to nearest boundary
|
||||
float midBoundary = 0.5f * (_maxTwist + _minTwist + TWO_PI);
|
||||
if (twistAngle > midBoundary) {
|
||||
// lower boundary is closer --> phase down one cycle
|
||||
twistAngle -= TWO_PI;
|
||||
} else if (twistAngle < midBoundary - TWO_PI) {
|
||||
// higher boundary is closer --> phase up one cycle
|
||||
twistAngle += TWO_PI;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// twist limits apply --> figure out which limit we're hitting, if any
|
||||
twistAngle = handleTwistBoundaryConditions(twistAngle);
|
||||
|
||||
// clamp twistAngle
|
||||
float clampedTwistAngle = glm::clamp(twistAngle, _minTwist, _maxTwist);
|
||||
|
@ -226,15 +352,15 @@ bool SwingTwistConstraint::apply(glm::quat& rotation) const {
|
|||
|
||||
// clamp the swing
|
||||
// The swingAxis is always perpendicular to the reference axis (yAxis in the constraint's frame).
|
||||
glm::vec3 swungY = swingRotation * yAxis;
|
||||
glm::vec3 swingAxis = glm::cross(yAxis, swungY);
|
||||
glm::vec3 swungY = swingRotation * Vectors::UNIT_Y;
|
||||
glm::vec3 swingAxis = glm::cross(Vectors::UNIT_Y, swungY);
|
||||
float axisLength = glm::length(swingAxis);
|
||||
if (axisLength > EPSILON) {
|
||||
// The limit of swing is a function of "theta" which can be computed from the swingAxis
|
||||
// (which is in the constraint's ZX plane).
|
||||
float theta = atan2f(-swingAxis.z, swingAxis.x);
|
||||
float minDot = _swingLimitFunction.getMinDot(theta);
|
||||
if (glm::dot(swungY, yAxis) < minDot) {
|
||||
if (glm::dot(swungY, Vectors::UNIT_Y) < minDot) {
|
||||
// The swing limits are violated so we extract the angle from midDot and
|
||||
// use it to supply a new rotation.
|
||||
swingAxis /= axisLength;
|
||||
|
@ -245,13 +371,53 @@ bool SwingTwistConstraint::apply(glm::quat& rotation) const {
|
|||
|
||||
if (somethingClamped) {
|
||||
// update the rotation
|
||||
twistRotation = glm::angleAxis(twistAngle, yAxis);
|
||||
twistRotation = glm::angleAxis(twistAngle, Vectors::UNIT_Y);
|
||||
rotation = swingRotation * twistRotation * _referenceRotation;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SwingTwistConstraint::dynamicallyAdjustLimits(const glm::quat& rotation) {
|
||||
glm::quat postRotation = rotation * glm::inverse(_referenceRotation);
|
||||
glm::quat swingRotation, twistRotation;
|
||||
|
||||
swingTwistDecomposition(postRotation, Vectors::UNIT_Y, swingRotation, twistRotation);
|
||||
|
||||
// adjust swing limits
|
||||
glm::vec3 swungY = swingRotation * Vectors::UNIT_Y;
|
||||
glm::vec3 swingAxis = glm::cross(Vectors::UNIT_Y, swungY);
|
||||
float theta = atan2f(-swingAxis.z, swingAxis.x);
|
||||
_swingLimitFunction.dynamicallyAdjustMinDots(theta, swungY.y);
|
||||
|
||||
// restore twist limits
|
||||
if (_twistAdjusted) {
|
||||
_minTwist = _oldMinTwist;
|
||||
_maxTwist = _oldMaxTwist;
|
||||
_twistAdjusted = false;
|
||||
}
|
||||
|
||||
if (_minTwist != _maxTwist) {
|
||||
// compute twistAngle
|
||||
float twistAngle = 2.0f * acosf(fabsf(twistRotation.w));
|
||||
glm::vec3 twistedX = twistRotation * Vectors::UNIT_X;
|
||||
twistAngle *= copysignf(1.0f, glm::dot(glm::cross(Vectors::UNIT_X, twistedX), Vectors::UNIT_Y));
|
||||
twistAngle = handleTwistBoundaryConditions(twistAngle);
|
||||
|
||||
if (twistAngle < _minTwist || twistAngle > _maxTwist) {
|
||||
// expand twist limits
|
||||
_twistAdjusted = true;
|
||||
_oldMinTwist = _minTwist;
|
||||
_oldMaxTwist = _maxTwist;
|
||||
if (twistAngle < _minTwist) {
|
||||
_minTwist = twistAngle;
|
||||
} else if (twistAngle > _maxTwist) {
|
||||
_maxTwist = twistAngle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SwingTwistConstraint::clearHistory() {
|
||||
_lastTwistBoundary = LAST_CLAMP_NO_BOUNDARY;
|
||||
}
|
||||
|
|
|
@ -53,24 +53,45 @@ public:
|
|||
void setLowerSpine(bool lowerSpine) { _lowerSpine = lowerSpine; }
|
||||
virtual bool isLowerSpine() const override { return _lowerSpine; }
|
||||
|
||||
/// \param rotation rotation to allow
|
||||
/// \brief clear previous adjustment and adjust constraint limits to allow rotation
|
||||
virtual void dynamicallyAdjustLimits(const glm::quat& rotation) override;
|
||||
|
||||
// for testing purposes
|
||||
const std::vector<float>& getMinDots() { return _swingLimitFunction.getMinDots(); }
|
||||
|
||||
// SwingLimitFunction is an implementation of the constraint check described in the paper:
|
||||
// "The Parameterization of Joint Rotation with the Unit Quaternion" by Quang Liu and Edmond C. Prakash
|
||||
//
|
||||
// The "dynamic adjustment" feature allows us to change the limits on the fly for one particular theta angle.
|
||||
//
|
||||
class SwingLimitFunction {
|
||||
public:
|
||||
SwingLimitFunction();
|
||||
|
||||
/// \brief use a uniform conical swing limit
|
||||
void setCone(float maxAngle);
|
||||
|
||||
/// \brief use a vector of lookup values for swing limits
|
||||
void setMinDots(const std::vector<float>& minDots);
|
||||
|
||||
/// \param theta radian angle to new minDot
|
||||
/// \param minDot minimum dot limit
|
||||
/// \brief updates swing constraint to permit minDot at theta
|
||||
void dynamicallyAdjustMinDots(float theta, float minDot);
|
||||
|
||||
/// \return minimum dotProduct between reference and swung axes
|
||||
float getMinDot(float theta) const;
|
||||
|
||||
protected:
|
||||
// for testing purposes
|
||||
const std::vector<float>& getMinDots() { return _minDots; }
|
||||
|
||||
private:
|
||||
// the limits are stored in a lookup table with cyclic boundary conditions
|
||||
std::vector<float> _minDots;
|
||||
|
||||
// these values used to restore dynamic adjustment
|
||||
float _minDotA;
|
||||
float _minDotB;
|
||||
int8_t _minDotIndexA;
|
||||
int8_t _minDotIndexB;
|
||||
};
|
||||
|
||||
/// \return reference to SwingLimitFunction instance for unit-testing
|
||||
|
@ -79,15 +100,22 @@ public:
|
|||
/// \brief exposed for unit testing
|
||||
void clearHistory();
|
||||
|
||||
private:
|
||||
float handleTwistBoundaryConditions(float twistAngle) const;
|
||||
|
||||
protected:
|
||||
SwingLimitFunction _swingLimitFunction;
|
||||
float _minTwist;
|
||||
float _maxTwist;
|
||||
|
||||
float _oldMinTwist;
|
||||
float _oldMaxTwist;
|
||||
|
||||
// We want to remember the LAST clamped boundary, so we an use it even when the far boundary is closer.
|
||||
// This reduces "pops" when the input twist angle goes far beyond and wraps around toward the far boundary.
|
||||
mutable int _lastTwistBoundary;
|
||||
bool _lowerSpine { false };
|
||||
bool _twistAdjusted { false };
|
||||
};
|
||||
|
||||
#endif // hifi_SwingTwistConstraint_h
|
||||
|
|
|
@ -1115,7 +1115,7 @@ void AvatarData::detachOne(const QString& modelURL, const QString& jointName) {
|
|||
return;
|
||||
}
|
||||
QVector<AttachmentData> attachmentData = getAttachmentData();
|
||||
for (QVector<AttachmentData>::iterator it = attachmentData.begin(); it != attachmentData.end(); it++) {
|
||||
for (QVector<AttachmentData>::iterator it = attachmentData.begin(); it != attachmentData.end(); ++it) {
|
||||
if (it->modelURL == modelURL && (jointName.isEmpty() || it->jointName == jointName)) {
|
||||
attachmentData.erase(it);
|
||||
setAttachmentData(attachmentData);
|
||||
|
@ -1134,7 +1134,7 @@ void AvatarData::detachAll(const QString& modelURL, const QString& jointName) {
|
|||
if (it->modelURL == modelURL && (jointName.isEmpty() || it->jointName == jointName)) {
|
||||
it = attachmentData.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
++it;
|
||||
}
|
||||
}
|
||||
setAttachmentData(attachmentData);
|
||||
|
|
|
@ -42,6 +42,8 @@ HeadData::HeadData(AvatarData* owningAvatar) :
|
|||
_rightEyeBlink(0.0f),
|
||||
_averageLoudness(0.0f),
|
||||
_browAudioLift(0.0f),
|
||||
_audioAverageLoudness(0.0f),
|
||||
_pupilDilation(0.0f),
|
||||
_owningAvatar(owningAvatar)
|
||||
{
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ class QJsonObject;
|
|||
|
||||
class HeadData {
|
||||
public:
|
||||
HeadData(AvatarData* owningAvatar);
|
||||
explicit HeadData(AvatarData* owningAvatar);
|
||||
virtual ~HeadData() { };
|
||||
|
||||
// degrees
|
||||
|
|
|
@ -157,6 +157,65 @@ void EntityTreeRenderer::update() {
|
|||
deleteReleasedModels();
|
||||
}
|
||||
|
||||
bool EntityTreeRenderer::findBestZoneAndMaybeContainingEntities(const glm::vec3& avatarPosition, QVector<EntityItemID>* entitiesContainingAvatar) {
|
||||
bool didUpdate = false;
|
||||
float radius = 1.0f; // for now, assume 1 meter radius
|
||||
QVector<EntityItemPointer> foundEntities;
|
||||
|
||||
// find the entities near us
|
||||
// don't let someone else change our tree while we search
|
||||
_tree->withReadLock([&] {
|
||||
std::static_pointer_cast<EntityTree>(_tree)->findEntities(avatarPosition, radius, foundEntities);
|
||||
|
||||
// Whenever you're in an intersection between zones, we will always choose the smallest zone.
|
||||
auto oldBestZone = _bestZone;
|
||||
_bestZone = nullptr; // NOTE: Is this what we want?
|
||||
_bestZoneVolume = std::numeric_limits<float>::max();
|
||||
|
||||
// create a list of entities that actually contain the avatar's position
|
||||
foreach(EntityItemPointer entity, foundEntities) {
|
||||
if (entity->contains(avatarPosition)) {
|
||||
if (entitiesContainingAvatar) {
|
||||
*entitiesContainingAvatar << entity->getEntityItemID();
|
||||
}
|
||||
|
||||
// if this entity is a zone, use this time to determine the bestZone
|
||||
if (entity->getType() == EntityTypes::Zone) {
|
||||
if (!entity->getVisible()) {
|
||||
qCDebug(entitiesrenderer) << "not visible";
|
||||
}
|
||||
else {
|
||||
float entityVolumeEstimate = entity->getVolumeEstimate();
|
||||
if (entityVolumeEstimate < _bestZoneVolume) {
|
||||
_bestZoneVolume = entityVolumeEstimate;
|
||||
_bestZone = std::dynamic_pointer_cast<ZoneEntityItem>(entity);
|
||||
}
|
||||
else if (entityVolumeEstimate == _bestZoneVolume) {
|
||||
if (!_bestZone) {
|
||||
_bestZoneVolume = entityVolumeEstimate;
|
||||
_bestZone = std::dynamic_pointer_cast<ZoneEntityItem>(entity);
|
||||
}
|
||||
else {
|
||||
// in the case of the volume being equal, we will use the
|
||||
// EntityItemID to deterministically pick one entity over the other
|
||||
if (entity->getEntityItemID() < _bestZone->getEntityItemID()) {
|
||||
_bestZoneVolume = entityVolumeEstimate;
|
||||
_bestZone = std::dynamic_pointer_cast<ZoneEntityItem>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_bestZone != oldBestZone) {
|
||||
applyZonePropertiesToScene(_bestZone);
|
||||
didUpdate = true;
|
||||
}
|
||||
});
|
||||
return didUpdate;
|
||||
}
|
||||
bool EntityTreeRenderer::checkEnterLeaveEntities() {
|
||||
bool didUpdate = false;
|
||||
|
||||
|
@ -164,54 +223,8 @@ bool EntityTreeRenderer::checkEnterLeaveEntities() {
|
|||
glm::vec3 avatarPosition = _viewState->getAvatarPosition();
|
||||
|
||||
if (avatarPosition != _lastAvatarPosition) {
|
||||
float radius = 1.0f; // for now, assume 1 meter radius
|
||||
QVector<EntityItemPointer> foundEntities;
|
||||
QVector<EntityItemID> entitiesContainingAvatar;
|
||||
|
||||
// find the entities near us
|
||||
// don't let someone else change our tree while we search
|
||||
_tree->withReadLock([&] {
|
||||
std::static_pointer_cast<EntityTree>(_tree)->findEntities(avatarPosition, radius, foundEntities);
|
||||
|
||||
// Whenever you're in an intersection between zones, we will always choose the smallest zone.
|
||||
auto oldBestZone = _bestZone;
|
||||
_bestZone = nullptr; // NOTE: Is this what we want?
|
||||
_bestZoneVolume = std::numeric_limits<float>::max();
|
||||
|
||||
// create a list of entities that actually contain the avatar's position
|
||||
foreach(EntityItemPointer entity, foundEntities) {
|
||||
if (entity->contains(avatarPosition)) {
|
||||
entitiesContainingAvatar << entity->getEntityItemID();
|
||||
|
||||
// if this entity is a zone, use this time to determine the bestZone
|
||||
if (entity->getType() == EntityTypes::Zone) {
|
||||
float entityVolumeEstimate = entity->getVolumeEstimate();
|
||||
if (entityVolumeEstimate < _bestZoneVolume) {
|
||||
_bestZoneVolume = entityVolumeEstimate;
|
||||
_bestZone = std::dynamic_pointer_cast<ZoneEntityItem>(entity);
|
||||
} else if (entityVolumeEstimate == _bestZoneVolume) {
|
||||
if (!_bestZone) {
|
||||
_bestZoneVolume = entityVolumeEstimate;
|
||||
_bestZone = std::dynamic_pointer_cast<ZoneEntityItem>(entity);
|
||||
} else {
|
||||
// in the case of the volume being equal, we will use the
|
||||
// EntityItemID to deterministically pick one entity over the other
|
||||
if (entity->getEntityItemID() < _bestZone->getEntityItemID()) {
|
||||
_bestZoneVolume = entityVolumeEstimate;
|
||||
_bestZone = std::dynamic_pointer_cast<ZoneEntityItem>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_bestZone != oldBestZone) {
|
||||
applyZonePropertiesToScene(_bestZone);
|
||||
didUpdate = true;
|
||||
}
|
||||
});
|
||||
didUpdate = findBestZoneAndMaybeContainingEntities(avatarPosition, &entitiesContainingAvatar);
|
||||
|
||||
// Note: at this point we don't need to worry about the tree being locked, because we only deal with
|
||||
// EntityItemIDs from here. The callEntityScriptMethod() method is robust against attempting to call scripts
|
||||
|
@ -234,6 +247,8 @@ bool EntityTreeRenderer::checkEnterLeaveEntities() {
|
|||
}
|
||||
_currentEntitiesInside = entitiesContainingAvatar;
|
||||
_lastAvatarPosition = avatarPosition;
|
||||
} else {
|
||||
didUpdate = findBestZoneAndMaybeContainingEntities(avatarPosition, nullptr);
|
||||
}
|
||||
}
|
||||
return didUpdate;
|
||||
|
@ -375,7 +390,6 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr<ZoneEntityIt
|
|||
qCDebug(entitiesrenderer) << "Failed to load skybox:" << zone->getSkyboxProperties().getURL();
|
||||
}
|
||||
}
|
||||
|
||||
skyStage->setBackgroundMode(model::SunSkyStage::SKY_BOX);
|
||||
break;
|
||||
}
|
||||
|
@ -872,7 +886,9 @@ void EntityTreeRenderer::updateZone(const EntityItemID& id) {
|
|||
_currentEntitiesInside << id;
|
||||
emit enterEntity(id);
|
||||
_entitiesScriptEngine->callEntityScriptMethod(id, "enterEntity");
|
||||
_bestZone = std::dynamic_pointer_cast<ZoneEntityItem>(zone);
|
||||
if (zone->getVisible()) {
|
||||
_bestZone = std::dynamic_pointer_cast<ZoneEntityItem>(zone);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_bestZone && _bestZone->getID() == id) {
|
||||
|
|
|
@ -124,6 +124,7 @@ protected:
|
|||
|
||||
private:
|
||||
void addEntityToScene(EntityItemPointer entity);
|
||||
bool findBestZoneAndMaybeContainingEntities(const glm::vec3& avatarPosition, QVector<EntityItemID>* entitiesContainingAvatar);
|
||||
|
||||
void applyZonePropertiesToScene(std::shared_ptr<ZoneEntityItem> zone);
|
||||
void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false);
|
||||
|
|
|
@ -48,6 +48,13 @@ RenderableModelEntityItem::~RenderableModelEntityItem() {
|
|||
|
||||
void RenderableModelEntityItem::setModelURL(const QString& url) {
|
||||
auto& currentURL = getParsedModelURL();
|
||||
if (_model && (currentURL != url)) {
|
||||
// The machinery for updateModelBounds will give existing models the opportunity to fix their translation/rotation/scale/registration.
|
||||
// The first two are straightforward, but the latter two have guards to make sure they don't happen after they've already been set.
|
||||
// Here we reset those guards. This doesn't cause the entity values to change -- it just allows the model to match once it comes in.
|
||||
_model->setScaleToFit(false, getDimensions());
|
||||
_model->setSnapModelToRegistrationPoint(false, getRegistrationPoint());
|
||||
}
|
||||
ModelEntityItem::setModelURL(url);
|
||||
|
||||
if (currentURL != getParsedModelURL() || !_model) {
|
||||
|
@ -512,15 +519,17 @@ bool RenderableModelEntityItem::needsToCallUpdate() const {
|
|||
|
||||
void RenderableModelEntityItem::update(const quint64& now) {
|
||||
if (!_dimensionsInitialized && _model && _model->isActive()) {
|
||||
EntityItemProperties properties;
|
||||
auto extents = _model->getMeshExtents();
|
||||
properties.setDimensions(extents.maximum - extents.minimum);
|
||||
|
||||
qCDebug(entitiesrenderer) << "Autoresizing:" << (!getName().isEmpty() ? getName() : getModelURL());
|
||||
QMetaObject::invokeMethod(DependencyManager::get<EntityScriptingInterface>().data(), "editEntity",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(QUuid, getEntityItemID()),
|
||||
Q_ARG(EntityItemProperties, properties));
|
||||
const QSharedPointer<NetworkGeometry> renderNetworkGeometry = _model->getGeometry();
|
||||
if (renderNetworkGeometry && renderNetworkGeometry->isLoaded()) {
|
||||
EntityItemProperties properties;
|
||||
auto extents = _model->getMeshExtents();
|
||||
properties.setDimensions(extents.maximum - extents.minimum);
|
||||
qCDebug(entitiesrenderer) << "Autoresizing:" << (!getName().isEmpty() ? getName() : getModelURL());
|
||||
QMetaObject::invokeMethod(DependencyManager::get<EntityScriptingInterface>().data(), "editEntity",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(QUuid, getEntityItemID()),
|
||||
Q_ARG(EntityItemProperties, properties));
|
||||
}
|
||||
}
|
||||
|
||||
ModelEntityItem::update(now);
|
||||
|
|
|
@ -125,7 +125,8 @@ void main(void) {
|
|||
vec4 quadPos = radius * UNIT_QUAD[twoTriID];
|
||||
|
||||
vec4 anchorPoint;
|
||||
<$transformModelToEyePos(cam, obj, inPosition, anchorPoint)$>
|
||||
vec4 _inPosition = vec4(inPosition, 1.0);
|
||||
<$transformModelToEyePos(cam, obj, _inPosition, anchorPoint)$>
|
||||
|
||||
vec4 eyePos = anchorPoint + quadPos;
|
||||
<$transformEyeToClipPos(cam, eyePos, gl_Position)$>
|
||||
|
|
|
@ -515,7 +515,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
|
|||
// we can confidently ignore this packet
|
||||
EntityTreePointer tree = getTree();
|
||||
if (tree && tree->isDeletedEntity(_id)) {
|
||||
qDebug() << "Recieved packet for previously deleted entity [" << _id << "] ignoring. (inside " << __FUNCTION__ << ")";
|
||||
#ifdef WANT_DEBUG
|
||||
qDebug() << "Recieved packet for previously deleted entity [" << _id << "] ignoring. "
|
||||
"(inside " << __FUNCTION__ << ")";
|
||||
#endif
|
||||
ignoreServerPacket = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,9 @@ public:
|
|||
EntityItem(const EntityItemID& entityItemID);
|
||||
virtual ~EntityItem();
|
||||
|
||||
inline EntityItemPointer getThisPointer() { return std::static_pointer_cast<EntityItem>(shared_from_this()); }
|
||||
inline EntityItemPointer getThisPointer() const {
|
||||
return std::static_pointer_cast<EntityItem>(std::const_pointer_cast<SpatiallyNestable>(shared_from_this()));
|
||||
}
|
||||
|
||||
EntityItemID getEntityItemID() const { return EntityItemID(_id); }
|
||||
|
||||
|
|
|
@ -148,7 +148,11 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties
|
|||
if (entity) {
|
||||
if (propertiesWithSimID.parentRelatedPropertyChanged()) {
|
||||
// due to parenting, the server may not know where something is in world-space, so include the bounding cube.
|
||||
propertiesWithSimID.setQueryAACube(entity->getQueryAACube());
|
||||
bool success;
|
||||
AACube queryAACube = entity->getQueryAACube(success);
|
||||
if (success) {
|
||||
propertiesWithSimID.setQueryAACube(queryAACube);
|
||||
}
|
||||
}
|
||||
|
||||
if (_bidOnSimulationOwnership) {
|
||||
|
@ -178,6 +182,15 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties
|
|||
return id;
|
||||
}
|
||||
|
||||
QUuid EntityScriptingInterface::addModelEntity(const QString& name, const QString& modelUrl, const glm::vec3& position) {
|
||||
EntityItemProperties properties;
|
||||
properties.setType(EntityTypes::Model);
|
||||
properties.setName(name);
|
||||
properties.setModelURL(modelUrl);
|
||||
properties.setPosition(position);
|
||||
return addEntity(properties);
|
||||
}
|
||||
|
||||
EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity) {
|
||||
EntityPropertyFlags noSpecificProperties;
|
||||
return getEntityProperties(identity, noSpecificProperties);
|
||||
|
|
|
@ -84,6 +84,9 @@ public slots:
|
|||
/// adds a model with the specific properties
|
||||
Q_INVOKABLE QUuid addEntity(const EntityItemProperties& properties);
|
||||
|
||||
/// temporary method until addEntity can be used from QJSEngine
|
||||
Q_INVOKABLE QUuid addModelEntity(const QString& name, const QString& modelUrl, const glm::vec3& position);
|
||||
|
||||
/// gets the current model properties for a specific model
|
||||
/// this function will not find return results in script engine contexts which don't have access to models
|
||||
Q_INVOKABLE EntityItemProperties getEntityProperties(QUuid entityID);
|
||||
|
|
|
@ -1011,21 +1011,33 @@ void EntityTree::fixupMissingParents() {
|
|||
EntityItemWeakPointer entityWP = iter.next();
|
||||
EntityItemPointer entity = entityWP.lock();
|
||||
if (entity) {
|
||||
bool queryAACubeSuccess;
|
||||
AACube newCube = entity->getQueryAACube(queryAACubeSuccess);
|
||||
if (queryAACubeSuccess) {
|
||||
// make sure queryAACube encompasses maxAACube
|
||||
bool maxAACubeSuccess;
|
||||
AACube maxAACube = entity->getMaximumAACube(maxAACubeSuccess);
|
||||
if (maxAACubeSuccess && !newCube.contains(maxAACube)) {
|
||||
newCube = maxAACube;
|
||||
}
|
||||
}
|
||||
|
||||
bool doMove = false;
|
||||
if (entity->isParentIDValid()) {
|
||||
// this entity's parent was previously not known, and now is. Update its location in the EntityTree...
|
||||
bool success;
|
||||
AACube newCube = entity->getQueryAACube(success);
|
||||
if (!success) {
|
||||
continue;
|
||||
}
|
||||
moveOperator.addEntityToMoveList(entity, newCube);
|
||||
iter.remove();
|
||||
entity->markAncestorMissing(false);
|
||||
} else if (_avatarIDs.contains(entity->getParentID())) {
|
||||
doMove = true;
|
||||
} else if (getIsServer() && _avatarIDs.contains(entity->getParentID())) {
|
||||
// this is a child of an avatar, which the entity server will never have
|
||||
// a SpatiallyNestable object for. Add it to a list for cleanup when the avatar leaves.
|
||||
if (!_childrenOfAvatars.contains(entity->getParentID())) {
|
||||
_childrenOfAvatars[entity->getParentID()] = QSet<EntityItemID>();
|
||||
}
|
||||
_childrenOfAvatars[entity->getParentID()] += entity->getEntityItemID();
|
||||
doMove = true;
|
||||
}
|
||||
|
||||
if (queryAACubeSuccess && doMove) {
|
||||
moveOperator.addEntityToMoveList(entity, newCube);
|
||||
iter.remove();
|
||||
entity->markAncestorMissing(false);
|
||||
}
|
||||
|
@ -1039,7 +1051,6 @@ void EntityTree::fixupMissingParents() {
|
|||
PerformanceTimer perfTimer("recurseTreeWithOperator");
|
||||
recurseTreeWithOperator(&moveOperator);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void EntityTree::deleteDescendantsOfAvatar(QUuid avatarID) {
|
||||
|
|
|
@ -74,9 +74,14 @@ void Context::downloadFramebuffer(const FramebufferPointer& srcFramebuffer, cons
|
|||
_backend->downloadFramebuffer(srcFramebuffer, region, destImage);
|
||||
}
|
||||
|
||||
const Backend::TransformCamera& Backend::TransformCamera::recomputeDerived() const {
|
||||
const Backend::TransformCamera& Backend::TransformCamera::recomputeDerived(const Transform& xformView) const {
|
||||
_projectionInverse = glm::inverse(_projection);
|
||||
_viewInverse = glm::inverse(_view);
|
||||
|
||||
// Get the viewEyeToWorld matrix form the transformView as passed to the gpu::Batch
|
||||
// this is the "_viewInverse" fed to the shader
|
||||
// Genetrate the "_view" matrix as well from the xform
|
||||
xformView.getMatrix(_viewInverse);
|
||||
_view = glm::inverse(_viewInverse);
|
||||
|
||||
Mat4 viewUntranslated = _view;
|
||||
viewUntranslated[3] = Vec4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
@ -84,16 +89,16 @@ const Backend::TransformCamera& Backend::TransformCamera::recomputeDerived() con
|
|||
return *this;
|
||||
}
|
||||
|
||||
Backend::TransformCamera Backend::TransformCamera::getEyeCamera(int eye, const StereoState& _stereo) const {
|
||||
Backend::TransformCamera Backend::TransformCamera::getEyeCamera(int eye, const StereoState& _stereo, const Transform& xformView) const {
|
||||
TransformCamera result = *this;
|
||||
Transform offsetTransform = xformView;
|
||||
if (!_stereo._skybox) {
|
||||
result._view = _stereo._eyeViews[eye] * result._view;
|
||||
offsetTransform.postTranslate(-Vec3(_stereo._eyeViews[eye][3]));
|
||||
} else {
|
||||
glm::mat4 skyboxView = _stereo._eyeViews[eye];
|
||||
skyboxView[3] = vec4(0, 0, 0, 1);
|
||||
result._view = skyboxView * result._view;
|
||||
// FIXME: If "skybox" the ipd is set to 0 for now, let s try to propose a better solution for this in the future
|
||||
}
|
||||
result._projection = _stereo._eyeProjections[eye];
|
||||
result.recomputeDerived();
|
||||
result.recomputeDerived(offsetTransform);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -79,15 +79,15 @@ public:
|
|||
// UBO class... layout MUST match the layout in TransformCamera.slh
|
||||
class TransformCamera {
|
||||
public:
|
||||
Mat4 _view;
|
||||
mutable Mat4 _view;
|
||||
mutable Mat4 _viewInverse;
|
||||
mutable Mat4 _projectionViewUntranslated;
|
||||
Mat4 _projection;
|
||||
mutable Mat4 _projectionInverse;
|
||||
Vec4 _viewport; // Public value is int but float in the shader to stay in floats for all the transform computations.
|
||||
|
||||
const Backend::TransformCamera& recomputeDerived() const;
|
||||
TransformCamera getEyeCamera(int eye, const StereoState& stereo) const;
|
||||
const Backend::TransformCamera& recomputeDerived(const Transform& xformView) const;
|
||||
TransformCamera getEyeCamera(int eye, const StereoState& stereo, const Transform& xformView) const;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -105,6 +105,7 @@ void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const Stereo
|
|||
}
|
||||
|
||||
if (_invalidView) {
|
||||
// This is when the _view matrix gets assigned
|
||||
_view.getInverseMatrix(_camera._view);
|
||||
}
|
||||
|
||||
|
@ -113,11 +114,11 @@ void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const Stereo
|
|||
if (stereo._enable) {
|
||||
_cameraOffsets.push_back(TransformStageState::Pair(commandIndex, offset));
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
_cameras.push_back(_camera.getEyeCamera(i, stereo));
|
||||
_cameras.push_back(_camera.getEyeCamera(i, stereo, _view));
|
||||
}
|
||||
} else {
|
||||
_cameraOffsets.push_back(TransformStageState::Pair(commandIndex, offset));
|
||||
_cameras.push_back(_camera.recomputeDerived());
|
||||
_cameras.push_back(_camera.recomputeDerived(_view));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -76,24 +76,39 @@ TransformObject getTransformObject() {
|
|||
<$viewport$> = <$cameraTransform$>._viewport;
|
||||
<@endfunc@>
|
||||
|
||||
<@func transformModelToClipPos(cameraTransform, objectTransform, modelPos, clipPos)@>
|
||||
<!// Equivalent to the following but hoppefully a tad more accurate
|
||||
//return camera._projection * camera._view * object._model * pos; !>
|
||||
{ // transformModelToClipPos
|
||||
vec4 _eyepos = (<$objectTransform$>._model * <$modelPos$>) + vec4(-<$modelPos$>.w * <$cameraTransform$>._viewInverse[3].xyz, 0.0);
|
||||
<$clipPos$> = <$cameraTransform$>._projectionViewUntranslated * _eyepos;
|
||||
<@func transformModelToEyeWorldAlignedPos(cameraTransform, objectTransform, modelPos, eyeWorldAlignedPos)@>
|
||||
<!// Bring the model pos in the world aligned space centered on the eye axis !>
|
||||
{ // _transformModelToEyeWorldAlignedPos
|
||||
highp mat4 _mv = <$objectTransform$>._model;
|
||||
_mv[3].xyz -= <$cameraTransform$>._viewInverse[3].xyz;
|
||||
highp vec4 _eyeWApos = (_mv * <$modelPos$>);
|
||||
<$eyeWorldAlignedPos$> = _eyeWApos;
|
||||
}
|
||||
<@endfunc@>
|
||||
|
||||
<@func $transformModelToEyeAndClipPos(cameraTransform, objectTransform, modelPos, eyePos, clipPos)@>
|
||||
<!// Equivalent to the following but hoppefully a tad more accurate
|
||||
//return camera._projection * camera._view * object._model * pos; !>
|
||||
<@func transformModelToClipPos(cameraTransform, objectTransform, modelPos, clipPos)@>
|
||||
{ // transformModelToClipPos
|
||||
vec4 _worldpos = (<$objectTransform$>._model * <$modelPos$>);
|
||||
<$eyePos$> = (<$cameraTransform$>._view * _worldpos);
|
||||
vec4 _eyepos =(<$objectTransform$>._model * <$modelPos$>) + vec4(-<$modelPos$>.w * <$cameraTransform$>._viewInverse[3].xyz, 0.0);
|
||||
<$clipPos$> = <$cameraTransform$>._projectionViewUntranslated * _eyepos;
|
||||
// <$eyePos$> = (<$cameraTransform$>._projectionInverse * <$clipPos$>);
|
||||
vec4 eyeWAPos;
|
||||
<$transformModelToEyeWorldAlignedPos($cameraTransform$, $objectTransform$, $modelPos$, eyeWAPos)$>
|
||||
|
||||
<$clipPos$> = <$cameraTransform$>._projectionViewUntranslated * eyeWAPos;
|
||||
}
|
||||
<@endfunc@>
|
||||
|
||||
<@func transformModelToEyeAndClipPos(cameraTransform, objectTransform, modelPos, eyePos, clipPos)@>
|
||||
{ // transformModelToEyeAndClipPos
|
||||
vec4 eyeWAPos;
|
||||
<$transformModelToEyeWorldAlignedPos($cameraTransform$, $objectTransform$, $modelPos$, eyeWAPos)$>
|
||||
<$clipPos$> = <$cameraTransform$>._projectionViewUntranslated * eyeWAPos;
|
||||
<$eyePos$> = vec4((<$cameraTransform$>._view * vec4(eyeWAPos.xyz, 0.0)).xyz, 1.0);
|
||||
}
|
||||
<@endfunc@>
|
||||
|
||||
<@func transformModelToEyePos(cameraTransform, objectTransform, modelPos, eyePos)@>
|
||||
{ // transformModelToEyePos
|
||||
vec4 eyeWAPos;
|
||||
<$transformModelToEyeWorldAlignedPos($cameraTransform$, $objectTransform$, $modelPos$, eyeWAPos)$>
|
||||
<$eyePos$> = vec4((<$cameraTransform$>._view * vec4(eyeWAPos.xyz, 0.0)).xyz, 1.0);
|
||||
}
|
||||
<@endfunc@>
|
||||
|
||||
|
@ -103,6 +118,7 @@ TransformObject getTransformObject() {
|
|||
}
|
||||
<@endfunc@>
|
||||
|
||||
|
||||
<@func transformModelToEyeDir(cameraTransform, objectTransform, modelDir, eyeDir)@>
|
||||
{ // transformModelToEyeDir
|
||||
vec3 mr0 = vec3(<$objectTransform$>._modelInverse[0].x, <$objectTransform$>._modelInverse[1].x, <$objectTransform$>._modelInverse[2].x);
|
||||
|
@ -129,15 +145,6 @@ TransformObject getTransformObject() {
|
|||
}
|
||||
<@endfunc@>
|
||||
|
||||
<@func $transformModelToEyePos(cameraTransform, objectTransform, modelPos, eyePos)@>
|
||||
<!// Equivalent to the following but hoppefully a tad more accurate
|
||||
//return camera._view * object._model * pos; !>
|
||||
{ // transformModelToEyePos
|
||||
vec4 _worldpos = (<$objectTransform$>._model * vec4(<$modelPos$>.xyz, 1.0));
|
||||
<$eyePos$> = (<$cameraTransform$>._view * _worldpos);
|
||||
}
|
||||
<@endfunc@>
|
||||
|
||||
<@func transformEyeToClipPos(cameraTransform, eyePos, clipPos)@>
|
||||
{ // transformEyeToClipPos
|
||||
<$clipPos$> = <$cameraTransform$>._projection * vec4(<$eyePos$>.xyz, 1.0);
|
||||
|
|
|
@ -135,6 +135,8 @@ bool NetworkGeometry::isLoadedWithTextures() const {
|
|||
}
|
||||
|
||||
if (!_isLoadedWithTextures) {
|
||||
_hasTransparentTextures = true;
|
||||
|
||||
for (auto&& material : _materials) {
|
||||
if ((material->albedoTexture && !material->albedoTexture->isLoaded()) ||
|
||||
(material->normalTexture && !material->normalTexture->isLoaded()) ||
|
||||
|
@ -145,7 +147,16 @@ bool NetworkGeometry::isLoadedWithTextures() const {
|
|||
(material->lightmapTexture && !material->lightmapTexture->isLoaded())) {
|
||||
return false;
|
||||
}
|
||||
if (material->albedoTexture && material->albedoTexture->getGPUTexture()) {
|
||||
// Reset the materialKey transparentTexture key only, as it is albedoTexture-dependent
|
||||
const auto& usage = material->albedoTexture->getGPUTexture()->getUsage();
|
||||
bool isTransparentTexture = usage.isAlpha() && !usage.isAlphaMask();
|
||||
material->_material->setTransparentTexture(isTransparentTexture);
|
||||
// FIXME: Materials with *some* transparent textures seem to give all *other* textures alphas of 0.
|
||||
_hasTransparentTextures = isTransparentTexture && _hasTransparentTextures;
|
||||
}
|
||||
}
|
||||
|
||||
_isLoadedWithTextures = true;
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -75,6 +75,10 @@ public:
|
|||
// true when the requested geometry and its textures are loaded.
|
||||
bool isLoadedWithTextures() const;
|
||||
|
||||
// true if the albedo texture has a non-masking alpha channel.
|
||||
// This can only be known after isLoadedWithTextures().
|
||||
bool hasTransparentTextures() const { return _hasTransparentTextures; }
|
||||
|
||||
// WARNING: only valid when isLoaded returns true.
|
||||
const FBXGeometry& getFBXGeometry() const { return *_geometry; }
|
||||
const std::vector<std::unique_ptr<NetworkMesh>>& getMeshes() const { return _meshes; }
|
||||
|
@ -151,6 +155,7 @@ protected:
|
|||
|
||||
// cache for isLoadedWithTextures()
|
||||
mutable bool _isLoadedWithTextures = false;
|
||||
mutable bool _hasTransparentTextures = false;
|
||||
};
|
||||
|
||||
/// Reads geometry in a worker thread.
|
||||
|
|
|
@ -80,6 +80,10 @@ void Material::setMetallic(float metallic) {
|
|||
_schemaBuffer.edit<Schema>()._metallic = metallic;
|
||||
}
|
||||
|
||||
void Material::setTransparentTexture(bool isTransparent) {
|
||||
_key.setTransparentTexture(isTransparent);
|
||||
_schemaBuffer.edit<Schema>()._key = (uint32)_key._flags.to_ulong();
|
||||
}
|
||||
|
||||
void Material::setTextureMap(MapChannel channel, const TextureMapPointer& textureMap) {
|
||||
if (textureMap) {
|
||||
|
@ -92,6 +96,3 @@ void Material::setTextureMap(MapChannel channel, const TextureMapPointer& textur
|
|||
_textureMaps.erase(channel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ public:
|
|||
METALLIC_VAL_BIT,
|
||||
GLOSSY_VAL_BIT,
|
||||
TRANSPARENT_VAL_BIT,
|
||||
TRANSPARENT_TEX_VAL_BIT,
|
||||
|
||||
EMISSIVE_MAP_BIT,
|
||||
ALBEDO_MAP_BIT,
|
||||
|
@ -101,6 +102,9 @@ public:
|
|||
void setAlbedo(bool value) { _flags.set(ALBEDO_VAL_BIT, value); }
|
||||
bool isAlbedo() const { return _flags[ALBEDO_VAL_BIT]; }
|
||||
|
||||
void setTransparentTexture(bool value) { _flags.set(TRANSPARENT_TEX_VAL_BIT, value); }
|
||||
bool isTransparentTexture() const { return _flags[TRANSPARENT_TEX_VAL_BIT]; }
|
||||
|
||||
void setAlbedoMap(bool value) { _flags.set(ALBEDO_MAP_BIT, value); }
|
||||
bool isAlbedoMap() const { return _flags[ALBEDO_MAP_BIT]; }
|
||||
|
||||
|
@ -164,6 +168,9 @@ public:
|
|||
Builder& withoutAlbedo() { _value.reset(MaterialKey::ALBEDO_VAL_BIT); _mask.set(MaterialKey::ALBEDO_VAL_BIT); return (*this); }
|
||||
Builder& withAlbedo() { _value.set(MaterialKey::ALBEDO_VAL_BIT); _mask.set(MaterialKey::ALBEDO_VAL_BIT); return (*this); }
|
||||
|
||||
Builder& withoutTransparentTexture() { _value.reset(MaterialKey::TRANSPARENT_TEX_VAL_BIT); _mask.set(MaterialKey::TRANSPARENT_TEX_VAL_BIT); return (*this); }
|
||||
Builder& withTransparentTexture() { _value.set(MaterialKey::TRANSPARENT_TEX_VAL_BIT); _mask.set(MaterialKey::TRANSPARENT_TEX_VAL_BIT); return (*this); }
|
||||
|
||||
Builder& withoutAlbedoMap() { _value.reset(MaterialKey::ALBEDO_MAP_BIT); _mask.set(MaterialKey::ALBEDO_MAP_BIT); return (*this); }
|
||||
Builder& withAlbedoMap() { _value.set(MaterialKey::ALBEDO_MAP_BIT); _mask.set(MaterialKey::ALBEDO_MAP_BIT); return (*this); }
|
||||
|
||||
|
@ -248,6 +255,8 @@ public:
|
|||
void setRoughness(float roughness);
|
||||
float getRoughness() const { return _schemaBuffer.get<Schema>()._roughness; }
|
||||
|
||||
void setTransparentTexture(bool isTransparent);
|
||||
|
||||
// Schema to access the attribute values of the material
|
||||
class Schema {
|
||||
public:
|
||||
|
@ -260,8 +269,7 @@ public:
|
|||
glm::vec3 _fresnel{ 0.03f }; // Fresnel value for a default non metallic
|
||||
float _metallic{ 0.0f }; // Not Metallic
|
||||
|
||||
|
||||
glm::vec3 _spare0{ 0.0f };
|
||||
glm::vec3 _spare{ 0.0f };
|
||||
|
||||
uint32_t _key{ 0 }; // a copy of the materialKey
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ struct Material {
|
|||
vec4 _emissiveOpacity;
|
||||
vec4 _albedoRoughness;
|
||||
vec4 _fresnelMetallic;
|
||||
vec4 _spare;
|
||||
vec4 _spareKey;
|
||||
};
|
||||
|
||||
uniform materialBuffer {
|
||||
|
@ -37,25 +37,23 @@ float getMaterialMetallic(Material m) { return m._fresnelMetallic.a; }
|
|||
|
||||
float getMaterialShininess(Material m) { return 1.0 - getMaterialRoughness(m); }
|
||||
|
||||
int getMaterialKey(Material m) { return floatBitsToInt(m._spare.w); }
|
||||
int getMaterialKey(Material m) { return floatBitsToInt(m._spareKey.w); }
|
||||
|
||||
const int EMISSIVE_VAL_BIT = 0x00000001;
|
||||
const int ALBEDO_VAL_BIT = 0x00000002;
|
||||
const int METALLIC_VAL_BIT = 0x00000004;
|
||||
const int GLOSSY_VAL_BIT = 0x00000008;
|
||||
const int TRANSPARENT_VAL_BIT = 0x00000010;
|
||||
const int TRANSPARENT_TEX_VAL_BIT = 0x00000020;
|
||||
|
||||
const int EMISSIVE_MAP_BIT = 0x00000040;
|
||||
const int ALBEDO_MAP_BIT = 0x00000080;
|
||||
const int METALLIC_MAP_BIT = 0x00000100;
|
||||
const int ROUGHNESS_MAP_BIT = 0x00000200;
|
||||
const int TRANSPARENT_MAP_BIT = 0x00000400;
|
||||
const int NORMAL_MAP_BIT = 0x00000800;
|
||||
const int OCCLUSION_MAP_BIT = 0x00001000;
|
||||
|
||||
const int EMISSIVE_MAP_BIT = 0x00000020;
|
||||
const int ALBEDO_MAP_BIT = 0x00000040;
|
||||
const int METALLIC_MAP_BIT = 0x00000080;
|
||||
const int ROUGHNESS_MAP_BIT = 0x00000100;
|
||||
const int TRANSPARENT_MAP_BIT = 0x00000200;
|
||||
const int NORMAL_MAP_BIT = 0x00000400;
|
||||
const int OCCLUSION_MAP_BIT = 0x00000800;
|
||||
|
||||
const int LIGHTMAP_MAP_BIT = 0x00001000;
|
||||
|
||||
|
||||
const int LIGHTMAP_MAP_BIT = 0x00002000;
|
||||
|
||||
<@endif@>
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#include "AssetRequest.h"
|
||||
#include "AssetUpload.h"
|
||||
#include "AssetUtils.h"
|
||||
#include "MappingRequest.h"
|
||||
#include "NetworkAccessManager.h"
|
||||
#include "NetworkLogging.h"
|
||||
#include "NodeList.h"
|
||||
|
@ -30,7 +31,6 @@
|
|||
|
||||
MessageID AssetClient::_currentID = 0;
|
||||
|
||||
|
||||
AssetClient::AssetClient() {
|
||||
|
||||
setCustomDeleter([](Dependency* dependency){
|
||||
|
@ -39,6 +39,8 @@ AssetClient::AssetClient() {
|
|||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
auto& packetReceiver = nodeList->getPacketReceiver();
|
||||
|
||||
packetReceiver.registerListener(PacketType::AssetMappingOperationReply, this, "handleAssetMappingOperationReply");
|
||||
packetReceiver.registerListener(PacketType::AssetGetInfoReply, this, "handleAssetGetInfoReply");
|
||||
packetReceiver.registerListener(PacketType::AssetGetReply, this, "handleAssetGetReply", true);
|
||||
packetReceiver.registerListener(PacketType::AssetUploadReply, this, "handleAssetUploadReply");
|
||||
|
@ -97,6 +99,33 @@ void AssetClient::clearCache() {
|
|||
}
|
||||
}
|
||||
|
||||
void AssetClient::handleAssetMappingOperationReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
MessageID messageID;
|
||||
message->readPrimitive(&messageID);
|
||||
|
||||
AssetServerError error;
|
||||
message->readPrimitive(&error);
|
||||
|
||||
// Check if we have any pending requests for this node
|
||||
auto messageMapIt = _pendingMappingRequests.find(senderNode);
|
||||
if (messageMapIt != _pendingMappingRequests.end()) {
|
||||
|
||||
// Found the node, get the MessageID -> Callback map
|
||||
auto& messageCallbackMap = messageMapIt->second;
|
||||
|
||||
// Check if we have this pending request
|
||||
auto requestIt = messageCallbackMap.find(messageID);
|
||||
if (requestIt != messageCallbackMap.end()) {
|
||||
auto callback = requestIt->second;
|
||||
callback(true, error, message);
|
||||
messageCallbackMap.erase(requestIt);
|
||||
}
|
||||
|
||||
// Although the messageCallbackMap may now be empty, we won't delete the node until we have disconnected from
|
||||
// it to avoid constantly creating/deleting the map on subsequent requests.
|
||||
}
|
||||
}
|
||||
|
||||
bool haveAssetServer() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
|
||||
|
@ -110,52 +139,68 @@ bool haveAssetServer() {
|
|||
return true;
|
||||
}
|
||||
|
||||
AssetRequest* AssetClient::createRequest(const QString& hash, const QString& extension) {
|
||||
if (hash.length() != SHA256_HASH_HEX_LENGTH) {
|
||||
qCWarning(asset_client) << "Invalid hash size";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (haveAssetServer()) {
|
||||
auto request = new AssetRequest(hash, extension);
|
||||
|
||||
// Move to the AssetClient thread in case we are not currently on that thread (which will usually be the case)
|
||||
request->moveToThread(thread());
|
||||
|
||||
return request;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
GetMappingRequest* AssetClient::createGetMappingRequest(const AssetPath& path) {
|
||||
return new GetMappingRequest(path);
|
||||
}
|
||||
|
||||
GetAllMappingsRequest* AssetClient::createGetAllMappingsRequest() {
|
||||
auto request = new GetAllMappingsRequest();
|
||||
|
||||
request->moveToThread(thread());
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
DeleteMappingsRequest* AssetClient::createDeleteMappingsRequest(const AssetPathList& paths) {
|
||||
auto request = new DeleteMappingsRequest(paths);
|
||||
|
||||
request->moveToThread(thread());
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
SetMappingRequest* AssetClient::createSetMappingRequest(const AssetPath& path, const AssetHash& hash) {
|
||||
auto request = new SetMappingRequest(path, hash);
|
||||
|
||||
request->moveToThread(thread());
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
RenameMappingRequest* AssetClient::createRenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath) {
|
||||
auto request = new RenameMappingRequest(oldPath, newPath);
|
||||
|
||||
request->moveToThread(thread());
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
AssetRequest* AssetClient::createRequest(const AssetHash& hash) {
|
||||
auto request = new AssetRequest(hash);
|
||||
|
||||
// Move to the AssetClient thread in case we are not currently on that thread (which will usually be the case)
|
||||
request->moveToThread(thread());
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
AssetUpload* AssetClient::createUpload(const QString& filename) {
|
||||
|
||||
if (haveAssetServer()) {
|
||||
auto upload = new AssetUpload(filename);
|
||||
|
||||
upload->moveToThread(thread());
|
||||
|
||||
return upload;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
auto upload = new AssetUpload(filename);
|
||||
|
||||
upload->moveToThread(thread());
|
||||
|
||||
return upload;
|
||||
}
|
||||
|
||||
AssetUpload* AssetClient::createUpload(const QByteArray& data, const QString& extension) {
|
||||
if (haveAssetServer()) {
|
||||
auto upload = new AssetUpload(data, extension);
|
||||
|
||||
upload->moveToThread(thread());
|
||||
|
||||
return upload;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
AssetUpload* AssetClient::createUpload(const QByteArray& data) {
|
||||
auto upload = new AssetUpload(data);
|
||||
|
||||
upload->moveToThread(thread());
|
||||
|
||||
return upload;
|
||||
}
|
||||
|
||||
bool AssetClient::getAsset(const QString& hash, const QString& extension, DataOffset start, DataOffset end,
|
||||
bool AssetClient::getAsset(const QString& hash, DataOffset start, DataOffset end,
|
||||
ReceivedAssetCallback callback, ProgressCallback progressCallback) {
|
||||
if (hash.length() != SHA256_HASH_HEX_LENGTH) {
|
||||
qCWarning(asset_client) << "Invalid hash size";
|
||||
|
@ -169,8 +214,7 @@ bool AssetClient::getAsset(const QString& hash, const QString& extension, DataOf
|
|||
|
||||
auto messageID = ++_currentID;
|
||||
|
||||
auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH + sizeof(uint8_t) + extension.length()
|
||||
+ sizeof(start) + sizeof(end);
|
||||
auto payloadSize = sizeof(messageID) + 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.";
|
||||
|
@ -179,9 +223,6 @@ bool AssetClient::getAsset(const QString& hash, const QString& extension, DataOf
|
|||
|
||||
packet->write(QByteArray::fromHex(hash.toLatin1()));
|
||||
|
||||
packet->writePrimitive(uint8_t(extension.length()));
|
||||
packet->write(extension.toLatin1());
|
||||
|
||||
packet->writePrimitive(start);
|
||||
packet->writePrimitive(end);
|
||||
|
||||
|
@ -190,34 +231,36 @@ bool AssetClient::getAsset(const QString& hash, const QString& extension, DataOf
|
|||
_pendingRequests[assetServer][messageID] = { callback, progressCallback };
|
||||
|
||||
return true;
|
||||
} else {
|
||||
callback(false, AssetServerError::NoError, QByteArray());
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool AssetClient::getAssetInfo(const QString& hash, const QString& extension, GetInfoCallback callback) {
|
||||
bool AssetClient::getAssetInfo(const QString& hash, GetInfoCallback callback) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
|
||||
|
||||
if (assetServer) {
|
||||
auto messageID = ++_currentID;
|
||||
|
||||
auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH + sizeof(uint8_t) + extension.length();
|
||||
auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH;
|
||||
auto packet = NLPacket::create(PacketType::AssetGetInfo, payloadSize, true);
|
||||
|
||||
packet->writePrimitive(messageID);
|
||||
packet->write(QByteArray::fromHex(hash.toLatin1()));
|
||||
packet->writePrimitive(uint8_t(extension.length()));
|
||||
packet->write(extension.toLatin1());
|
||||
|
||||
nodeList->sendPacket(std::move(packet), *assetServer);
|
||||
|
||||
_pendingInfoRequests[assetServer][messageID] = callback;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
callback(false, AssetServerError::NoError, { "", 0 });
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AssetClient::handleAssetGetInfoReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
|
@ -305,7 +348,137 @@ void AssetClient::handleAssetGetReply(QSharedPointer<ReceivedMessage> message, S
|
|||
}
|
||||
}
|
||||
|
||||
bool AssetClient::uploadAsset(const QByteArray& data, const QString& extension, UploadResultCallback callback) {
|
||||
bool AssetClient::getAssetMapping(const AssetPath& path, MappingOperationCallback callback) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
|
||||
|
||||
if (assetServer) {
|
||||
auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true);
|
||||
|
||||
auto messageID = ++_currentID;
|
||||
packetList->writePrimitive(messageID);
|
||||
|
||||
packetList->writePrimitive(AssetMappingOperationType::Get);
|
||||
|
||||
packetList->writeString(path);
|
||||
|
||||
nodeList->sendPacketList(std::move(packetList), *assetServer);
|
||||
|
||||
_pendingMappingRequests[assetServer][messageID] = callback;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
callback(false, AssetServerError::NoError, QSharedPointer<ReceivedMessage>());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetClient::getAllAssetMappings(MappingOperationCallback callback) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
|
||||
|
||||
if (assetServer) {
|
||||
auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true);
|
||||
|
||||
auto messageID = ++_currentID;
|
||||
packetList->writePrimitive(messageID);
|
||||
|
||||
packetList->writePrimitive(AssetMappingOperationType::GetAll);
|
||||
|
||||
nodeList->sendPacketList(std::move(packetList), *assetServer);
|
||||
|
||||
_pendingMappingRequests[assetServer][messageID] = callback;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
callback(false, AssetServerError::NoError, QSharedPointer<ReceivedMessage>());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetClient::deleteAssetMappings(const AssetPathList& paths, MappingOperationCallback callback) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
|
||||
|
||||
if (assetServer) {
|
||||
auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true);
|
||||
|
||||
auto messageID = ++_currentID;
|
||||
packetList->writePrimitive(messageID);
|
||||
|
||||
packetList->writePrimitive(AssetMappingOperationType::Delete);
|
||||
|
||||
packetList->writePrimitive(int(paths.size()));
|
||||
|
||||
for (auto& path: paths) {
|
||||
packetList->writeString(path);
|
||||
}
|
||||
|
||||
nodeList->sendPacketList(std::move(packetList), *assetServer);
|
||||
|
||||
_pendingMappingRequests[assetServer][messageID] = callback;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
callback(false, AssetServerError::NoError, QSharedPointer<ReceivedMessage>());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetClient::setAssetMapping(const QString& path, const AssetHash& hash, MappingOperationCallback callback) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
|
||||
|
||||
if (assetServer) {
|
||||
auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true);
|
||||
|
||||
auto messageID = ++_currentID;
|
||||
packetList->writePrimitive(messageID);
|
||||
|
||||
packetList->writePrimitive(AssetMappingOperationType::Set);
|
||||
|
||||
packetList->writeString(path);
|
||||
packetList->write(QByteArray::fromHex(hash.toUtf8()));
|
||||
|
||||
nodeList->sendPacketList(std::move(packetList), *assetServer);
|
||||
|
||||
_pendingMappingRequests[assetServer][messageID] = callback;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
callback(false, AssetServerError::NoError, QSharedPointer<ReceivedMessage>());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetClient::renameAssetMapping(const AssetPath& oldPath, const AssetPath& newPath, MappingOperationCallback callback) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
|
||||
|
||||
if (assetServer) {
|
||||
auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true);
|
||||
|
||||
auto messageID = ++_currentID;
|
||||
packetList->writePrimitive(messageID);
|
||||
|
||||
packetList->writePrimitive(AssetMappingOperationType::Rename);
|
||||
|
||||
packetList->writeString(oldPath);
|
||||
packetList->writeString(newPath);
|
||||
|
||||
nodeList->sendPacketList(std::move(packetList), *assetServer);
|
||||
|
||||
_pendingMappingRequests[assetServer][messageID] = callback;
|
||||
|
||||
return true;
|
||||
|
||||
} else {
|
||||
callback(false, AssetServerError::NoError, QSharedPointer<ReceivedMessage>());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetClient::uploadAsset(const QByteArray& data, UploadResultCallback callback) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
|
||||
|
||||
|
@ -315,9 +488,6 @@ bool AssetClient::uploadAsset(const QByteArray& data, const QString& extension,
|
|||
auto messageID = ++_currentID;
|
||||
packetList->writePrimitive(messageID);
|
||||
|
||||
packetList->writePrimitive(static_cast<uint8_t>(extension.length()));
|
||||
packetList->write(extension.toLatin1().constData(), extension.length());
|
||||
|
||||
uint64_t size = data.length();
|
||||
packetList->writePrimitive(size);
|
||||
packetList->write(data.constData(), size);
|
||||
|
@ -327,8 +497,10 @@ bool AssetClient::uploadAsset(const QByteArray& data, const QString& extension,
|
|||
_pendingUploads[assetServer][messageID] = callback;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
callback(false, AssetServerError::NoError, QString());
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void AssetClient::handleAssetUploadReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
|
@ -404,71 +576,14 @@ void AssetClient::handleNodeKilled(SharedNodePointer node) {
|
|||
messageMapIt->second.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AssetScriptingInterface::uploadData(QString data, QString extension, QScriptValue callback) {
|
||||
QByteArray dataByteArray = data.toUtf8();
|
||||
auto upload = DependencyManager::get<AssetClient>()->createUpload(dataByteArray, extension);
|
||||
if (!upload) {
|
||||
qCWarning(asset_client) << "Error uploading file to asset server";
|
||||
return;
|
||||
}
|
||||
|
||||
QObject::connect(upload, &AssetUpload::finished, this, [this, callback, extension](AssetUpload* upload, const QString& hash) mutable {
|
||||
if (callback.isFunction()) {
|
||||
QString url = "atp://" + hash + "." + extension;
|
||||
QScriptValueList args { url };
|
||||
callback.call(_engine->currentContext()->thisObject(), args);
|
||||
}
|
||||
});
|
||||
upload->start();
|
||||
}
|
||||
|
||||
AssetScriptingInterface::AssetScriptingInterface(QScriptEngine* engine) :
|
||||
_engine(engine)
|
||||
{
|
||||
}
|
||||
|
||||
void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) {
|
||||
const QString ATP_SCHEME { "atp://" };
|
||||
|
||||
if (!urlString.startsWith(ATP_SCHEME)) {
|
||||
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] : "";
|
||||
auto extension = parts.length() > 1 ? parts[1] : "";
|
||||
|
||||
if (hash.length() != SHA256_HASH_HEX_LENGTH) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto assetRequest = assetClient->createRequest(hash, extension);
|
||||
|
||||
if (!assetRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingRequests << assetRequest;
|
||||
|
||||
connect(assetRequest, &AssetRequest::finished, this, [this, callback](AssetRequest* request) mutable {
|
||||
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);
|
||||
{
|
||||
auto messageMapIt = _pendingMappingRequests.find(node);
|
||||
if (messageMapIt != _pendingMappingRequests.end()) {
|
||||
for (const auto& value : messageMapIt->second) {
|
||||
value.second(false, AssetServerError::NoError, QSharedPointer<ReceivedMessage>());
|
||||
}
|
||||
messageMapIt->second.clear();
|
||||
}
|
||||
|
||||
request->deleteLater();
|
||||
_pendingRequests.remove(request);
|
||||
});
|
||||
|
||||
assetRequest->start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,14 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
|
||||
#ifndef hifi_AssetClient_h
|
||||
#define hifi_AssetClient_h
|
||||
|
||||
#include <QStandardItemModel>
|
||||
#include <QtQml/QJSEngine>
|
||||
#include <QString>
|
||||
#include <QScriptValue>
|
||||
|
||||
#include <map>
|
||||
|
||||
#include <DependencyManager.h>
|
||||
|
||||
|
@ -25,6 +27,11 @@
|
|||
#include "ReceivedMessage.h"
|
||||
#include "ResourceCache.h"
|
||||
|
||||
class GetMappingRequest;
|
||||
class SetMappingRequest;
|
||||
class GetAllMappingsRequest;
|
||||
class DeleteMappingsRequest;
|
||||
class RenameMappingRequest;
|
||||
class AssetRequest;
|
||||
class AssetUpload;
|
||||
|
||||
|
@ -33,20 +40,25 @@ struct AssetInfo {
|
|||
int64_t size;
|
||||
};
|
||||
|
||||
using MappingOperationCallback = std::function<void(bool responseReceived, AssetServerError serverError, QSharedPointer<ReceivedMessage> message)>;
|
||||
using ReceivedAssetCallback = std::function<void(bool responseReceived, AssetServerError serverError, const QByteArray& data)>;
|
||||
using GetInfoCallback = std::function<void(bool responseReceived, AssetServerError serverError, AssetInfo info)>;
|
||||
using UploadResultCallback = std::function<void(bool responseReceived, AssetServerError serverError, const QString& hash)>;
|
||||
using ProgressCallback = std::function<void(qint64 totalReceived, qint64 total)>;
|
||||
|
||||
|
||||
class AssetClient : public QObject, public Dependency {
|
||||
Q_OBJECT
|
||||
public:
|
||||
AssetClient();
|
||||
|
||||
Q_INVOKABLE AssetRequest* createRequest(const QString& hash, const QString& extension);
|
||||
Q_INVOKABLE GetMappingRequest* createGetMappingRequest(const 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 AssetRequest* createRequest(const AssetHash& hash);
|
||||
Q_INVOKABLE AssetUpload* createUpload(const QString& filename);
|
||||
Q_INVOKABLE AssetUpload* createUpload(const QByteArray& data, const QString& extension);
|
||||
Q_INVOKABLE AssetUpload* createUpload(const QByteArray& data);
|
||||
|
||||
public slots:
|
||||
void init();
|
||||
|
@ -55,6 +67,7 @@ public slots:
|
|||
void clearCache();
|
||||
|
||||
private slots:
|
||||
void handleAssetMappingOperationReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void handleAssetGetInfoReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void handleAssetGetReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void handleAssetUploadReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
|
@ -62,10 +75,16 @@ private slots:
|
|||
void handleNodeKilled(SharedNodePointer node);
|
||||
|
||||
private:
|
||||
bool getAssetInfo(const QString& hash, const QString& extension, GetInfoCallback callback);
|
||||
bool getAsset(const QString& hash, const QString& extension, DataOffset start, DataOffset end,
|
||||
bool getAssetMapping(const AssetHash& hash, MappingOperationCallback callback);
|
||||
bool getAllAssetMappings(MappingOperationCallback callback);
|
||||
bool setAssetMapping(const QString& path, const AssetHash& hash, MappingOperationCallback callback);
|
||||
bool deleteAssetMappings(const AssetPathList& paths, MappingOperationCallback callback);
|
||||
bool renameAssetMapping(const AssetPath& oldPath, const AssetPath& newPath, MappingOperationCallback callback);
|
||||
|
||||
bool getAssetInfo(const QString& hash, GetInfoCallback callback);
|
||||
bool getAsset(const QString& hash, DataOffset start, DataOffset end,
|
||||
ReceivedAssetCallback callback, ProgressCallback progressCallback);
|
||||
bool uploadAsset(const QByteArray& data, const QString& extension, UploadResultCallback callback);
|
||||
bool uploadAsset(const QByteArray& data, UploadResultCallback callback);
|
||||
|
||||
struct GetAssetCallbacks {
|
||||
ReceivedAssetCallback completeCallback;
|
||||
|
@ -73,26 +92,18 @@ private:
|
|||
};
|
||||
|
||||
static MessageID _currentID;
|
||||
std::unordered_map<SharedNodePointer, std::unordered_map<MessageID, MappingOperationCallback>> _pendingMappingRequests;
|
||||
std::unordered_map<SharedNodePointer, std::unordered_map<MessageID, GetAssetCallbacks>> _pendingRequests;
|
||||
std::unordered_map<SharedNodePointer, std::unordered_map<MessageID, GetInfoCallback>> _pendingInfoRequests;
|
||||
std::unordered_map<SharedNodePointer, std::unordered_map<MessageID, UploadResultCallback>> _pendingUploads;
|
||||
|
||||
|
||||
friend class AssetRequest;
|
||||
friend class AssetUpload;
|
||||
friend class GetMappingRequest;
|
||||
friend class GetAllMappingsRequest;
|
||||
friend class SetMappingRequest;
|
||||
friend class DeleteMappingsRequest;
|
||||
friend class RenameMappingRequest;
|
||||
};
|
||||
|
||||
|
||||
class AssetScriptingInterface : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
AssetScriptingInterface(QScriptEngine* engine);
|
||||
|
||||
Q_INVOKABLE void uploadData(QString data, QString extension, QScriptValue callback);
|
||||
Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete);
|
||||
protected:
|
||||
QSet<AssetRequest*> _pendingRequests;
|
||||
QScriptEngine* _engine;
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
|
|
|
@ -20,10 +20,8 @@
|
|||
#include "NodeList.h"
|
||||
#include "ResourceCache.h"
|
||||
|
||||
AssetRequest::AssetRequest(const QString& hash, const QString& extension) :
|
||||
QObject(),
|
||||
_hash(hash),
|
||||
_extension(extension)
|
||||
AssetRequest::AssetRequest(const QString& hash) :
|
||||
_hash(hash)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -37,6 +35,15 @@ void AssetRequest::start() {
|
|||
qCWarning(asset_client) << "AssetRequest already started.";
|
||||
return;
|
||||
}
|
||||
|
||||
// in case we haven't parsed a valid hash, return an error now
|
||||
if (!isValidHash(_hash)) {
|
||||
_error = InvalidHash;
|
||||
_state = Finished;
|
||||
|
||||
emit finished(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to load from cache
|
||||
_data = loadFromCache(getUrl());
|
||||
|
@ -53,9 +60,9 @@ void AssetRequest::start() {
|
|||
_state = WaitingForInfo;
|
||||
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
assetClient->getAssetInfo(_hash, _extension, [this](bool responseReceived, AssetServerError serverError, AssetInfo info) {
|
||||
assetClient->getAssetInfo(_hash, [this](bool responseReceived, AssetServerError serverError, AssetInfo info) {
|
||||
_info = info;
|
||||
|
||||
|
||||
if (!responseReceived) {
|
||||
_error = NetworkError;
|
||||
} else if (serverError != AssetServerError::NoError) {
|
||||
|
@ -85,7 +92,7 @@ void AssetRequest::start() {
|
|||
int start = 0, end = _info.size;
|
||||
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
assetClient->getAsset(_hash, _extension, start, end, [this, start, end](bool responseReceived, AssetServerError serverError,
|
||||
assetClient->getAsset(_hash, start, end, [this, start, end](bool responseReceived, AssetServerError serverError,
|
||||
const QByteArray& data) {
|
||||
if (!responseReceived) {
|
||||
_error = NetworkError;
|
||||
|
|
|
@ -34,19 +34,20 @@ public:
|
|||
NoError,
|
||||
NotFound,
|
||||
InvalidByteRange,
|
||||
InvalidHash,
|
||||
HashVerificationFailed,
|
||||
NetworkError,
|
||||
UnknownError
|
||||
};
|
||||
|
||||
AssetRequest(const QString& hash, const QString& extension);
|
||||
AssetRequest(const QString& hash);
|
||||
|
||||
Q_INVOKABLE void start();
|
||||
|
||||
const QByteArray& getData() const { return _data; }
|
||||
const State& getState() const { return _state; }
|
||||
const Error& getError() const { return _error; }
|
||||
QUrl getUrl() const { return ::getATPUrl(_hash, _extension); }
|
||||
QUrl getUrl() const { return ::getATPUrl(_hash); }
|
||||
|
||||
signals:
|
||||
void finished(AssetRequest* thisRequest);
|
||||
|
@ -58,7 +59,6 @@ private:
|
|||
AssetInfo _info;
|
||||
uint64_t _totalReceived { 0 };
|
||||
QString _hash;
|
||||
QString _extension;
|
||||
QByteArray _data;
|
||||
int _numPendingRequests { 0 };
|
||||
};
|
||||
|
|
|
@ -13,37 +13,99 @@
|
|||
|
||||
#include "AssetClient.h"
|
||||
#include "AssetUtils.h"
|
||||
#include "MappingRequest.h"
|
||||
|
||||
AssetResourceRequest::~AssetResourceRequest() {
|
||||
if (_assetMappingRequest) {
|
||||
_assetMappingRequest->deleteLater();
|
||||
}
|
||||
|
||||
if (_assetRequest) {
|
||||
_assetRequest->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetResourceRequest::urlIsAssetHash() const {
|
||||
static const QString ATP_HASH_REGEX_STRING { "^atp:([A-Fa-f0-9]{64})(\\.[\\w]+)?$" };
|
||||
|
||||
QRegExp hashRegex { ATP_HASH_REGEX_STRING };
|
||||
return hashRegex.exactMatch(_url.toString());
|
||||
}
|
||||
|
||||
void AssetResourceRequest::doSend() {
|
||||
// Make request to atp
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto parts = _url.path().split(".", QString::SkipEmptyParts);
|
||||
auto hash = parts.length() > 0 ? parts[0] : "";
|
||||
auto extension = parts.length() > 1 ? parts[1] : "";
|
||||
|
||||
if (hash.length() != SHA256_HASH_HEX_LENGTH) {
|
||||
_result = InvalidURL;
|
||||
_state = Finished;
|
||||
// We'll either have a hash or an ATP path to a file (that maps to a hash)
|
||||
|
||||
emit finished();
|
||||
return;
|
||||
if (urlIsAssetHash()) {
|
||||
// We've detected that this is a hash - simply use AssetClient to request that asset
|
||||
auto parts = _url.path().split(".", QString::SkipEmptyParts);
|
||||
auto hash = parts.length() > 0 ? parts[0] : "";
|
||||
|
||||
requestHash(hash);
|
||||
} else {
|
||||
// This is an ATP path, we'll need to figure out what the mapping is.
|
||||
// This may incur a roundtrip to the asset-server, or it may return immediately from the cache in AssetClient.
|
||||
|
||||
auto path = _url.path();
|
||||
requestMappingForPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
_assetRequest = assetClient->createRequest(hash, extension);
|
||||
void AssetResourceRequest::requestMappingForPath(const AssetPath& path) {
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
_assetMappingRequest = assetClient->createGetMappingRequest(path);
|
||||
|
||||
if (!_assetRequest) {
|
||||
_result = ServerUnavailable;
|
||||
_state = Finished;
|
||||
// make sure we'll hear about the result of the get mapping request
|
||||
connect(_assetMappingRequest, &GetMappingRequest::finished, this, [this, path](GetMappingRequest* request){
|
||||
Q_ASSERT(_state == InProgress);
|
||||
Q_ASSERT(request == _assetMappingRequest);
|
||||
|
||||
emit finished();
|
||||
return;
|
||||
}
|
||||
switch (request->getError()) {
|
||||
case MappingRequest::NoError:
|
||||
// we have no error, we should have a resulting hash - use that to send of a request for that asset
|
||||
qDebug() << "Got mapping for:" << path << "=>" << request->getHash();
|
||||
|
||||
requestHash(request->getHash());
|
||||
|
||||
break;
|
||||
default: {
|
||||
switch (request->getError()) {
|
||||
case MappingRequest::NotFound:
|
||||
// no result for the mapping request, set error to not found
|
||||
_result = NotFound;
|
||||
break;
|
||||
case MappingRequest::NetworkError:
|
||||
// didn't hear back from the server, mark it unavailable
|
||||
_result = ServerUnavailable;
|
||||
break;
|
||||
default:
|
||||
_result = Error;
|
||||
break;
|
||||
}
|
||||
|
||||
// since we've failed we know we are finished
|
||||
_state = Finished;
|
||||
emit finished();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_assetMappingRequest->deleteLater();
|
||||
_assetMappingRequest = nullptr;
|
||||
});
|
||||
|
||||
_assetMappingRequest->start();
|
||||
}
|
||||
|
||||
void AssetResourceRequest::requestHash(const AssetHash& hash) {
|
||||
|
||||
// Make request to atp
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
_assetRequest = assetClient->createRequest(hash);
|
||||
|
||||
connect(_assetRequest, &AssetRequest::progress, this, &AssetResourceRequest::progress);
|
||||
connect(_assetRequest, &AssetRequest::finished, this, [this](AssetRequest* req) {
|
||||
|
@ -56,6 +118,9 @@ void AssetResourceRequest::doSend() {
|
|||
_data = req->getData();
|
||||
_result = Success;
|
||||
break;
|
||||
case AssetRequest::InvalidHash:
|
||||
_result = InvalidURL;
|
||||
break;
|
||||
case AssetRequest::Error::NotFound:
|
||||
_result = NotFound;
|
||||
break;
|
||||
|
|
|
@ -30,6 +30,12 @@ private slots:
|
|||
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
|
||||
|
||||
private:
|
||||
bool urlIsAssetHash() const;
|
||||
|
||||
void requestMappingForPath(const AssetPath& path);
|
||||
void requestHash(const AssetHash& hash);
|
||||
|
||||
GetMappingRequest* _assetMappingRequest { nullptr };
|
||||
AssetRequest* _assetRequest { nullptr };
|
||||
};
|
||||
|
||||
|
|
|
@ -19,9 +19,8 @@
|
|||
|
||||
const QString AssetUpload::PERMISSION_DENIED_ERROR = "You do not have permission to upload content to this asset-server.";
|
||||
|
||||
AssetUpload::AssetUpload(const QByteArray& data, const QString& extension) :
|
||||
_data(data),
|
||||
_extension(extension)
|
||||
AssetUpload::AssetUpload(const QByteArray& data) :
|
||||
_data(data)
|
||||
{
|
||||
|
||||
}
|
||||
|
@ -35,6 +34,8 @@ AssetUpload::AssetUpload(const QString& filename) :
|
|||
QString AssetUpload::getErrorString() const {
|
||||
// figure out the right error message for error
|
||||
switch (_error) {
|
||||
case AssetUpload::NoError:
|
||||
return QString();
|
||||
case AssetUpload::PermissionDenied:
|
||||
return PERMISSION_DENIED_ERROR;
|
||||
case AssetUpload::TooLarge:
|
||||
|
@ -42,10 +43,11 @@ QString AssetUpload::getErrorString() const {
|
|||
case AssetUpload::FileOpenError:
|
||||
return "The file could not be opened. Please check your permissions and try again.";
|
||||
case AssetUpload::NetworkError:
|
||||
return "The file could not be opened. Please check your network connectivity.";
|
||||
return "There was a problem reaching your Asset Server. Please check your network connectivity.";
|
||||
case AssetUpload::ServerFileError:
|
||||
return "The Asset Server failed to store the asset. Please try again.";
|
||||
default:
|
||||
// not handled, do not show a message box
|
||||
return QString();
|
||||
return QString("Unknown error with code %1").arg(_error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,12 +61,7 @@ void AssetUpload::start() {
|
|||
// try to open the file at the given filename
|
||||
QFile file { _filename };
|
||||
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
|
||||
// file opened, read the data and grab the extension
|
||||
_extension = QFileInfo(_filename).suffix();
|
||||
_extension = _extension.toLower();
|
||||
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
_data = file.readAll();
|
||||
} else {
|
||||
// we couldn't open the file - set the error result
|
||||
|
@ -72,6 +69,8 @@ void AssetUpload::start() {
|
|||
|
||||
// emit that we are done
|
||||
emit finished(this, QString());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +81,7 @@ void AssetUpload::start() {
|
|||
qCDebug(asset_client) << "Attempting to upload" << _filename << "to asset-server.";
|
||||
}
|
||||
|
||||
assetClient->uploadAsset(_data, _extension, [this](bool responseReceived, AssetServerError error, const QString& hash){
|
||||
assetClient->uploadAsset(_data, [this](bool responseReceived, AssetServerError error, const QString& hash){
|
||||
if (!responseReceived) {
|
||||
_error = NetworkError;
|
||||
} else {
|
||||
|
@ -96,6 +95,9 @@ void AssetUpload::start() {
|
|||
case AssetServerError::PermissionDenied:
|
||||
_error = PermissionDenied;
|
||||
break;
|
||||
case AssetServerError::FileOperationFailed:
|
||||
_error = ServerFileError;
|
||||
break;
|
||||
default:
|
||||
_error = FileOpenError;
|
||||
break;
|
||||
|
@ -103,7 +105,7 @@ void AssetUpload::start() {
|
|||
}
|
||||
|
||||
if (_error == NoError && hash == hashData(_data).toHex()) {
|
||||
saveToCache(getATPUrl(hash, _extension), _data);
|
||||
saveToCache(getATPUrl(hash), _data);
|
||||
}
|
||||
|
||||
emit finished(this, hash);
|
||||
|
|
|
@ -32,18 +32,18 @@ public:
|
|||
Timeout,
|
||||
TooLarge,
|
||||
PermissionDenied,
|
||||
FileOpenError
|
||||
FileOpenError,
|
||||
ServerFileError
|
||||
};
|
||||
|
||||
static const QString PERMISSION_DENIED_ERROR;
|
||||
|
||||
AssetUpload(const QString& filename);
|
||||
AssetUpload(const QByteArray& data, const QString& extension);
|
||||
AssetUpload(const QByteArray& data);
|
||||
|
||||
Q_INVOKABLE void start();
|
||||
|
||||
const QString& getFilename() const { return _filename; }
|
||||
const QString& getExtension() const { return _extension; }
|
||||
const Error& getError() const { return _error; }
|
||||
QString getErrorString() const;
|
||||
|
||||
|
@ -54,7 +54,6 @@ signals:
|
|||
private:
|
||||
QString _filename;
|
||||
QByteArray _data;
|
||||
QString _extension;
|
||||
Error _error;
|
||||
};
|
||||
|
||||
|
|
|
@ -19,12 +19,8 @@
|
|||
|
||||
#include "ResourceManager.h"
|
||||
|
||||
QUrl getATPUrl(const QString& hash, const QString& extension) {
|
||||
if (!extension.isEmpty()) {
|
||||
return QUrl(QString("%1:%2.%3").arg(URL_SCHEME_ATP, hash, extension));
|
||||
} else {
|
||||
return QUrl(QString("%1:%2").arg(URL_SCHEME_ATP, hash));
|
||||
}
|
||||
QUrl getATPUrl(const QString& hash) {
|
||||
return QUrl(QString("%1:%2").arg(URL_SCHEME_ATP, hash));
|
||||
}
|
||||
|
||||
QByteArray hashData(const QByteArray& data) {
|
||||
|
@ -67,3 +63,13 @@ bool saveToCache(const QUrl& url, const QByteArray& file) {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isValidPath(const AssetPath& path) {
|
||||
QRegExp pathRegex { ASSET_PATH_REGEX_STRING };
|
||||
return pathRegex.exactMatch(path);
|
||||
}
|
||||
|
||||
bool isValidHash(const AssetHash& hash) {
|
||||
QRegExp hashRegex { ASSET_HASH_REGEX_STRING };
|
||||
return hashRegex.exactMatch(hash);
|
||||
}
|
||||
|
|
|
@ -14,29 +14,52 @@
|
|||
|
||||
#include <cstdint>
|
||||
|
||||
#include <map>
|
||||
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QUrl>
|
||||
|
||||
using MessageID = uint32_t;
|
||||
using DataOffset = int64_t;
|
||||
|
||||
using AssetPath = QString;
|
||||
using AssetHash = QString;
|
||||
using AssetMapping = std::map<AssetPath, AssetHash>;
|
||||
using AssetPathList = QStringList;
|
||||
|
||||
const size_t SHA256_HASH_LENGTH = 32;
|
||||
const size_t SHA256_HASH_HEX_LENGTH = 64;
|
||||
const uint64_t MAX_UPLOAD_SIZE = 1000 * 1000 * 1000; // 1GB
|
||||
|
||||
const QString ASSET_PATH_REGEX_STRING = "^\\/(?!\\/)(?:[^\\/]|\\/(?!\\/))*$";
|
||||
const QString ASSET_HASH_REGEX_STRING = QString("^[a-fA-F0-9]{%1}$").arg(SHA256_HASH_HEX_LENGTH);
|
||||
|
||||
enum AssetServerError : uint8_t {
|
||||
NoError = 0,
|
||||
AssetNotFound,
|
||||
InvalidByteRange,
|
||||
AssetTooLarge,
|
||||
PermissionDenied
|
||||
PermissionDenied,
|
||||
MappingOperationFailed,
|
||||
FileOperationFailed
|
||||
};
|
||||
|
||||
QUrl getATPUrl(const QString& hash, const QString& extension = QString());
|
||||
enum AssetMappingOperationType : uint8_t {
|
||||
Get = 0,
|
||||
GetAll,
|
||||
Set,
|
||||
Delete,
|
||||
Rename
|
||||
};
|
||||
|
||||
QUrl getATPUrl(const QString& hash);
|
||||
|
||||
QByteArray hashData(const QByteArray& data);
|
||||
|
||||
QByteArray loadFromCache(const QUrl& url);
|
||||
bool saveToCache(const QUrl& url, const QByteArray& file);
|
||||
|
||||
bool isValidPath(const AssetPath& path);
|
||||
bool isValidHash(const QString& hashString);
|
||||
|
||||
#endif
|
||||
|
|
240
libraries/networking/src/MappingRequest.cpp
Normal file
240
libraries/networking/src/MappingRequest.cpp
Normal file
|
@ -0,0 +1,240 @@
|
|||
//
|
||||
// MappingRequest.cpp
|
||||
// libraries/networking/src
|
||||
//
|
||||
// Created by Stephen Birarda on 2016-03-08.
|
||||
// Copyright 2016 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 "MappingRequest.h"
|
||||
|
||||
#include <QtCore/QThread>
|
||||
|
||||
#include <DependencyManager.h>
|
||||
|
||||
#include "AssetClient.h"
|
||||
|
||||
void MappingRequest::start() {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "start", Qt::AutoConnection);
|
||||
return;
|
||||
}
|
||||
doStart();
|
||||
};
|
||||
|
||||
QString MappingRequest::getErrorString() const {
|
||||
switch (_error) {
|
||||
case MappingRequest::NoError:
|
||||
return QString();
|
||||
case MappingRequest::NotFound:
|
||||
return "Asset not found";
|
||||
case MappingRequest::NetworkError:
|
||||
return "Unable to communicate with Asset Server";
|
||||
case MappingRequest::PermissionDenied:
|
||||
return "Permission denied";
|
||||
case MappingRequest::InvalidPath:
|
||||
return "Path is invalid";
|
||||
case MappingRequest::InvalidHash:
|
||||
return "Hash is invalid";
|
||||
case MappingRequest::UnknownError:
|
||||
return "Asset Server internal error";
|
||||
default:
|
||||
return QString("Unknown error with code %1").arg(_error);
|
||||
}
|
||||
}
|
||||
|
||||
GetMappingRequest::GetMappingRequest(const AssetPath& path) : _path(path.trimmed()) {
|
||||
};
|
||||
|
||||
void GetMappingRequest::doStart() {
|
||||
|
||||
// short circuit the request if the path is invalid
|
||||
if (!isValidPath(_path)) {
|
||||
_error = MappingRequest::InvalidPath;
|
||||
emit finished(this);
|
||||
return;
|
||||
}
|
||||
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
|
||||
assetClient->getAssetMapping(_path, [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer<ReceivedMessage> message) {
|
||||
if (!responseReceived) {
|
||||
_error = NetworkError;
|
||||
} else {
|
||||
switch (error) {
|
||||
case AssetServerError::NoError:
|
||||
_error = NoError;
|
||||
break;
|
||||
case AssetServerError::AssetNotFound:
|
||||
_error = NotFound;
|
||||
break;
|
||||
default:
|
||||
_error = UnknownError;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_error) {
|
||||
_hash = message->read(SHA256_HASH_LENGTH).toHex();
|
||||
}
|
||||
emit finished(this);
|
||||
});
|
||||
};
|
||||
|
||||
GetAllMappingsRequest::GetAllMappingsRequest() {
|
||||
};
|
||||
|
||||
void GetAllMappingsRequest::doStart() {
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
assetClient->getAllAssetMappings([this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer<ReceivedMessage> message) {
|
||||
if (!responseReceived) {
|
||||
_error = NetworkError;
|
||||
} else {
|
||||
switch (error) {
|
||||
case AssetServerError::NoError:
|
||||
_error = NoError;
|
||||
break;
|
||||
default:
|
||||
_error = UnknownError;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!_error) {
|
||||
int numberOfMappings;
|
||||
message->readPrimitive(&numberOfMappings);
|
||||
for (auto i = 0; i < numberOfMappings; ++i) {
|
||||
auto path = message->readString();
|
||||
auto hash = message->read(SHA256_HASH_LENGTH).toHex();
|
||||
_mappings[path] = hash;
|
||||
}
|
||||
}
|
||||
emit finished(this);
|
||||
});
|
||||
};
|
||||
|
||||
SetMappingRequest::SetMappingRequest(const AssetPath& path, const AssetHash& hash) :
|
||||
_path(path.trimmed()),
|
||||
_hash(hash)
|
||||
{
|
||||
|
||||
};
|
||||
|
||||
void SetMappingRequest::doStart() {
|
||||
|
||||
// short circuit the request if the hash or path are invalid
|
||||
auto validPath = isValidPath(_path);
|
||||
auto validHash = isValidHash(_hash);
|
||||
if (!validPath || !validHash) {
|
||||
_error = !validPath ? MappingRequest::InvalidPath : MappingRequest::InvalidHash;
|
||||
emit finished(this);
|
||||
return;
|
||||
}
|
||||
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
|
||||
assetClient->setAssetMapping(_path, _hash, [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer<ReceivedMessage> message) {
|
||||
if (!responseReceived) {
|
||||
_error = NetworkError;
|
||||
} else {
|
||||
switch (error) {
|
||||
case AssetServerError::NoError:
|
||||
_error = NoError;
|
||||
break;
|
||||
case AssetServerError::PermissionDenied:
|
||||
_error = PermissionDenied;
|
||||
break;
|
||||
default:
|
||||
_error = UnknownError;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit finished(this);
|
||||
});
|
||||
};
|
||||
|
||||
DeleteMappingsRequest::DeleteMappingsRequest(const AssetPathList& paths) : _paths(paths) {
|
||||
for (auto& path : _paths) {
|
||||
path = path.trimmed();
|
||||
}
|
||||
};
|
||||
|
||||
void DeleteMappingsRequest::doStart() {
|
||||
|
||||
// short circuit the request if any of the paths are invalid
|
||||
for (auto& path : _paths) {
|
||||
if (!isValidPath(path)) {
|
||||
_error = MappingRequest::InvalidPath;
|
||||
emit finished(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
|
||||
assetClient->deleteAssetMappings(_paths, [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer<ReceivedMessage> message) {
|
||||
if (!responseReceived) {
|
||||
_error = NetworkError;
|
||||
} else {
|
||||
switch (error) {
|
||||
case AssetServerError::NoError:
|
||||
_error = NoError;
|
||||
break;
|
||||
case AssetServerError::PermissionDenied:
|
||||
_error = PermissionDenied;
|
||||
break;
|
||||
default:
|
||||
_error = UnknownError;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit finished(this);
|
||||
});
|
||||
};
|
||||
|
||||
RenameMappingRequest::RenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath) :
|
||||
_oldPath(oldPath.trimmed()),
|
||||
_newPath(newPath.trimmed())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void RenameMappingRequest::doStart() {
|
||||
|
||||
// short circuit the request if either of the paths are invalid
|
||||
if (!isValidPath(_oldPath) || !isValidPath(_newPath)) {
|
||||
_error = InvalidPath;
|
||||
emit finished(this);
|
||||
return;
|
||||
}
|
||||
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
|
||||
assetClient->renameAssetMapping(_oldPath, _newPath, [this, assetClient](bool responseReceived,
|
||||
AssetServerError error,
|
||||
QSharedPointer<ReceivedMessage> message) {
|
||||
if (!responseReceived) {
|
||||
_error = NetworkError;
|
||||
} else {
|
||||
switch (error) {
|
||||
case AssetServerError::NoError:
|
||||
_error = NoError;
|
||||
break;
|
||||
case AssetServerError::PermissionDenied:
|
||||
_error = PermissionDenied;
|
||||
break;
|
||||
default:
|
||||
_error = UnknownError;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit finished(this);
|
||||
});
|
||||
}
|
127
libraries/networking/src/MappingRequest.h
Normal file
127
libraries/networking/src/MappingRequest.h
Normal file
|
@ -0,0 +1,127 @@
|
|||
//
|
||||
// MappingRequest.h
|
||||
// libraries/networking/src
|
||||
//
|
||||
// Created by Stephen Birarda on 2016-03-08.
|
||||
// Copyright 2016 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
|
||||
|
||||
#ifndef hifi_MappingRequest_h
|
||||
#define hifi_MappingRequest_h
|
||||
|
||||
#include <QtCore/QObject>
|
||||
|
||||
#include "AssetUtils.h"
|
||||
|
||||
class MappingRequest : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Error {
|
||||
NoError,
|
||||
NotFound,
|
||||
NetworkError,
|
||||
PermissionDenied,
|
||||
InvalidPath,
|
||||
InvalidHash,
|
||||
UnknownError
|
||||
};
|
||||
|
||||
Q_INVOKABLE void start();
|
||||
Error getError() const { return _error; }
|
||||
Q_INVOKABLE QString getErrorString() const;
|
||||
|
||||
protected:
|
||||
Error _error { NoError };
|
||||
|
||||
private:
|
||||
virtual void doStart() = 0;
|
||||
};
|
||||
|
||||
|
||||
class GetMappingRequest : public MappingRequest {
|
||||
Q_OBJECT
|
||||
public:
|
||||
GetMappingRequest(const AssetPath& path);
|
||||
|
||||
AssetHash getHash() const { return _hash; }
|
||||
|
||||
signals:
|
||||
void finished(GetMappingRequest* thisRequest);
|
||||
|
||||
private:
|
||||
virtual void doStart() override;
|
||||
|
||||
AssetPath _path;
|
||||
AssetHash _hash;
|
||||
};
|
||||
|
||||
class SetMappingRequest : public MappingRequest {
|
||||
Q_OBJECT
|
||||
public:
|
||||
SetMappingRequest(const AssetPath& path, const AssetHash& hash);
|
||||
|
||||
AssetPath getPath() const { return _path; }
|
||||
AssetHash getHash() const { return _hash; }
|
||||
|
||||
signals:
|
||||
void finished(SetMappingRequest* thisRequest);
|
||||
|
||||
private:
|
||||
virtual void doStart() override;
|
||||
|
||||
AssetPath _path;
|
||||
AssetHash _hash;
|
||||
};
|
||||
|
||||
class DeleteMappingsRequest : public MappingRequest {
|
||||
Q_OBJECT
|
||||
public:
|
||||
DeleteMappingsRequest(const AssetPathList& path);
|
||||
|
||||
signals:
|
||||
void finished(DeleteMappingsRequest* thisRequest);
|
||||
|
||||
private:
|
||||
virtual void doStart() override;
|
||||
|
||||
AssetPathList _paths;
|
||||
};
|
||||
|
||||
class RenameMappingRequest : public MappingRequest {
|
||||
Q_OBJECT
|
||||
public:
|
||||
RenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath);
|
||||
|
||||
signals:
|
||||
void finished(RenameMappingRequest* thisRequest);
|
||||
|
||||
private:
|
||||
virtual void doStart() override;
|
||||
|
||||
AssetPath _oldPath;
|
||||
AssetPath _newPath;
|
||||
};
|
||||
|
||||
class GetAllMappingsRequest : public MappingRequest {
|
||||
Q_OBJECT
|
||||
public:
|
||||
GetAllMappingsRequest();
|
||||
|
||||
AssetMapping getMappings() const { return _mappings; }
|
||||
|
||||
signals:
|
||||
void finished(GetAllMappingsRequest* thisRequest);
|
||||
|
||||
private:
|
||||
virtual void doStart() override;
|
||||
|
||||
std::map<AssetPath, AssetHash> _mappings;
|
||||
};
|
||||
|
||||
|
||||
#endif // hifi_MappingRequest_h
|
|
@ -107,6 +107,15 @@ QByteArray ReceivedMessage::readAll() {
|
|||
return read(getBytesLeftToRead());
|
||||
}
|
||||
|
||||
QString ReceivedMessage::readString() {
|
||||
uint32_t size;
|
||||
readPrimitive(&size);
|
||||
//Q_ASSERT(size <= _size - _position);
|
||||
auto string = QString::fromUtf8(_data.constData() + _position, size);
|
||||
_position += size;
|
||||
return string;
|
||||
}
|
||||
|
||||
QByteArray ReceivedMessage::readWithoutCopy(qint64 size) {
|
||||
QByteArray data { QByteArray::fromRawData(_data.constData() + _position, size) };
|
||||
_position += size;
|
||||
|
|
|
@ -63,6 +63,8 @@ public:
|
|||
QByteArray read(qint64 size);
|
||||
QByteArray readAll();
|
||||
|
||||
QString readString();
|
||||
|
||||
QByteArray readHead(qint64 size);
|
||||
|
||||
// This will return a QByteArray referencing the underlying data _without_ refcounting that data.
|
||||
|
@ -86,7 +88,6 @@ private:
|
|||
QByteArray _data;
|
||||
QByteArray _headData;
|
||||
|
||||
std::atomic<qint64> _size { true };
|
||||
std::atomic<qint64> _position { 0 };
|
||||
std::atomic<qint64> _numPackets { 0 };
|
||||
|
||||
|
|
|
@ -56,6 +56,15 @@ void ResourceCache::refresh(const QUrl& url) {
|
|||
}
|
||||
}
|
||||
|
||||
void ResourceCache::setRequestLimit(int limit) {
|
||||
_requestLimit = limit;
|
||||
|
||||
// Now go fill any new request spots
|
||||
while (attemptHighestPriorityRequest()) {
|
||||
// just keep looping until we reach the new limit or no more pending requests
|
||||
}
|
||||
}
|
||||
|
||||
void ResourceCache::getResourceAsynchronously(const QUrl& url) {
|
||||
qCDebug(networking) << "ResourceCache::getResourceAsynchronously" << url.toString();
|
||||
_resourcesToBeGottenLock.lockForWrite();
|
||||
|
@ -150,31 +159,37 @@ void ResourceCache::clearUnusedResource() {
|
|||
}
|
||||
}
|
||||
|
||||
void ResourceCache::attemptRequest(Resource* resource) {
|
||||
bool ResourceCache::attemptRequest(Resource* resource) {
|
||||
auto sharedItems = DependencyManager::get<ResourceCacheSharedItems>();
|
||||
|
||||
// Disable request limiting for ATP
|
||||
if (resource->getURL().scheme() != URL_SCHEME_ATP) {
|
||||
if (_requestLimit <= 0) {
|
||||
if (_requestsActive >= _requestLimit) {
|
||||
// wait until a slot becomes available
|
||||
sharedItems->_pendingRequests.append(resource);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
--_requestLimit;
|
||||
}
|
||||
++_requestsActive;
|
||||
}
|
||||
|
||||
sharedItems->_loadingRequests.append(resource);
|
||||
resource->makeRequest();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ResourceCache::requestCompleted(Resource* resource) {
|
||||
auto sharedItems = DependencyManager::get<ResourceCacheSharedItems>();
|
||||
sharedItems->_loadingRequests.removeOne(resource);
|
||||
if (resource->getURL().scheme() != URL_SCHEME_ATP) {
|
||||
++_requestLimit;
|
||||
--_requestsActive;
|
||||
}
|
||||
|
||||
|
||||
attemptHighestPriorityRequest();
|
||||
}
|
||||
|
||||
bool ResourceCache::attemptHighestPriorityRequest() {
|
||||
auto sharedItems = DependencyManager::get<ResourceCacheSharedItems>();
|
||||
// look for the highest priority pending request
|
||||
int highestIndex = -1;
|
||||
float highestPriority = -FLT_MAX;
|
||||
|
@ -191,13 +206,12 @@ void ResourceCache::requestCompleted(Resource* resource) {
|
|||
}
|
||||
i++;
|
||||
}
|
||||
if (highestIndex >= 0) {
|
||||
attemptRequest(sharedItems->_pendingRequests.takeAt(highestIndex));
|
||||
}
|
||||
return (highestIndex >= 0) && attemptRequest(sharedItems->_pendingRequests.takeAt(highestIndex));
|
||||
}
|
||||
|
||||
const int DEFAULT_REQUEST_LIMIT = 10;
|
||||
int ResourceCache::_requestLimit = DEFAULT_REQUEST_LIMIT;
|
||||
int ResourceCache::_requestsActive = 0;
|
||||
|
||||
Resource::Resource(const QUrl& url, bool delayLoad) :
|
||||
_url(url),
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue