Merge branch 'master' into 20851

Conflicts:
	interface/resources/qml/dialogs/MessageDialog.qml
This commit is contained in:
David Rowe 2016-03-17 09:29:56 +13:00
commit 5faa7b3e87
132 changed files with 4324 additions and 1206 deletions

View file

@ -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;
}
}
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -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>();

View file

@ -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];

View file

@ -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();

View 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();

View file

@ -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>

View file

@ -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";

View 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 }
}

View 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(); });

View 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
}
}
}
}
}
}

View file

@ -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;
}
}
}

View file

@ -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

View file

@ -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

View 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
}
}
}

View file

@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -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

View file

@ -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";

View file

@ -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: "&#xe000;"
readonly property string cm: "}"
readonly property string msvg79: "~"
readonly property string deg: "\\"
readonly property string px: "|"
}
}

View file

@ -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();
}

View file

@ -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;

View file

@ -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);

View file

@ -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";

View file

@ -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;
}

View file

@ -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 };
};

View file

@ -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);

View file

@ -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;

View file

@ -308,7 +308,6 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) {
hand = _hand;
}
ok = true;
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
holderID = myAvatar->getSessionUUID();

View file

@ -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");

View file

@ -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);

View file

@ -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;

View file

@ -28,7 +28,7 @@ class Avatar;
class Head : public HeadData {
public:
Head(Avatar* owningAvatar);
explicit Head(Avatar* owningAvatar);
void init();
void reset();

View file

@ -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));

View file

@ -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;

View file

@ -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) {

View file

@ -21,7 +21,7 @@ class MyAvatar;
class MyCharacterController : public CharacterController {
public:
MyCharacterController(MyAvatar* avatar);
explicit MyCharacterController(MyAvatar* avatar);
~MyCharacterController ();
virtual void updateShapeIfNecessary() override;

View file

@ -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);

View 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();
}

View 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

View file

@ -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);
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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

View file

@ -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;

View file

@ -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();

View file

@ -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} {}

View file

@ -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()) {

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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));
}
}
}

View file

@ -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; }

View file

@ -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())
{

View file

@ -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);

View file

@ -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);

View file

@ -83,6 +83,7 @@ public:
Hover
};
Rig() {}
virtual ~Rig() {}
void destroyAnimGraph();

View file

@ -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();
};

View file

@ -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;
}

View file

@ -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

View file

@ -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);

View file

@ -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)
{

View file

@ -32,7 +32,7 @@ class QJsonObject;
class HeadData {
public:
HeadData(AvatarData* owningAvatar);
explicit HeadData(AvatarData* owningAvatar);
virtual ~HeadData() { };
// degrees

View file

@ -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) {

View file

@ -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);

View file

@ -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);

View file

@ -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)$>

View file

@ -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;
}

View file

@ -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); }

View file

@ -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);

View file

@ -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);

View file

@ -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) {

View file

@ -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;
}

View file

@ -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;
};

View file

@ -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));
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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.

View file

@ -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);
}
}

View file

@ -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

View file

@ -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@>

View file

@ -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();
}
}
}

View file

@ -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

View file

@ -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;

View file

@ -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 };
};

View file

@ -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;

View file

@ -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 };
};

View file

@ -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);

View file

@ -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;
};

View file

@ -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);
}

View file

@ -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

View 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);
});
}

View 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

View file

@ -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;

View file

@ -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 };

View file

@ -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