diff --git a/assignment-client/src/AssignmentClient.cpp b/assignment-client/src/AssignmentClient.cpp index 009bd42e88..da1feda7a0 100644 --- a/assignment-client/src/AssignmentClient.cpp +++ b/assignment-client/src/AssignmentClient.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -41,73 +42,65 @@ AssignmentClient::AssignmentClient(int &argc, char **argv) : setOrganizationDomain("highfidelity.io"); setApplicationName("assignment-client"); QSettings::setDefaultFormat(QSettings::IniFormat); - - QStringList argumentList = arguments(); - - // register meta type is required for queued invoke method on Assignment subclasses - + // set the logging target to the the CHILD_TARGET_NAME Logging::setTargetName(ASSIGNMENT_CLIENT_TARGET_NAME); - - const QString ASSIGNMENT_TYPE_OVVERIDE_OPTION = "-t"; - int argumentIndex = argumentList.indexOf(ASSIGNMENT_TYPE_OVVERIDE_OPTION); - + + const QVariantMap argumentVariantMap = HifiConfigVariantMap::mergeCLParametersWithJSONConfig(arguments()); + + const QString ASSIGNMENT_TYPE_OVERRIDE_OPTION = "t"; + const QString ASSIGNMENT_POOL_OPTION = "pool"; + const QString ASSIGNMENT_WALLET_DESTINATION_ID_OPTION = "wallet"; + const QString CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION = "a"; + Assignment::Type requestAssignmentType = Assignment::AllTypes; - - if (argumentIndex != -1) { - requestAssignmentType = (Assignment::Type) argumentList[argumentIndex + 1].toInt(); + + // check for an assignment type passed on the command line or in the config + if (argumentVariantMap.contains(ASSIGNMENT_TYPE_OVERRIDE_OPTION)) { + requestAssignmentType = (Assignment::Type) argumentVariantMap.value(ASSIGNMENT_TYPE_OVERRIDE_OPTION).toInt(); } - - const QString ASSIGNMENT_POOL_OPTION = "--pool"; - - argumentIndex = argumentList.indexOf(ASSIGNMENT_POOL_OPTION); - + QString assignmentPool; - - if (argumentIndex != -1) { - assignmentPool = argumentList[argumentIndex + 1]; + + // check for an assignment pool passed on the command line or in the config + if (argumentVariantMap.contains(ASSIGNMENT_POOL_OPTION)) { + assignmentPool = argumentVariantMap.value(ASSIGNMENT_POOL_OPTION).toString(); } - + // setup our _requestAssignment member variable from the passed arguments _requestAssignment = Assignment(Assignment::RequestCommand, requestAssignmentType, assignmentPool); - - // check if we were passed a wallet UUID on the command line + + // check for a wallet UUID on the command line or in the config // this would represent where the user running AC wants funds sent to - - const QString ASSIGNMENT_WALLET_DESTINATION_ID_OPTION = "--wallet"; - if ((argumentIndex = argumentList.indexOf(ASSIGNMENT_WALLET_DESTINATION_ID_OPTION)) != -1) { - QUuid walletUUID = QString(argumentList[argumentIndex + 1]); + if (argumentVariantMap.contains(ASSIGNMENT_WALLET_DESTINATION_ID_OPTION)) { + QUuid walletUUID = argumentVariantMap.value(ASSIGNMENT_WALLET_DESTINATION_ID_OPTION).toString(); qDebug() << "The destination wallet UUID for credits is" << uuidStringWithoutCurlyBraces(walletUUID); _requestAssignment.setWalletUUID(walletUUID); } - + // create a NodeList as an unassigned client NodeList* nodeList = NodeList::createInstance(NodeType::Unassigned); - + // check for an overriden assignment server hostname - const QString CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION = "-a"; - - argumentIndex = argumentList.indexOf(CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION); - - if (argumentIndex != -1) { - _assignmentServerHostname = argumentList[argumentIndex + 1]; - + if (argumentVariantMap.contains(CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION)) { + _assignmentServerHostname = argumentVariantMap.value(CUSTOM_ASSIGNMENT_SERVER_HOSTNAME_OPTION).toString(); + // set the custom assignment socket on our NodeList HifiSockAddr customAssignmentSocket = HifiSockAddr(_assignmentServerHostname, DEFAULT_DOMAIN_SERVER_PORT); - + nodeList->setAssignmentServerSocket(customAssignmentSocket); } - + // call a timer function every ASSIGNMENT_REQUEST_INTERVAL_MSECS to ask for assignment, if required qDebug() << "Waiting for assignment -" << _requestAssignment; - + QTimer* timer = new QTimer(this); connect(timer, SIGNAL(timeout()), SLOT(sendAssignmentRequest())); timer->start(ASSIGNMENT_REQUEST_INTERVAL_MSECS); - + // connect our readPendingDatagrams method to the readyRead() signal of the socket connect(&nodeList->getNodeSocket(), &QUdpSocket::readyRead, this, &AssignmentClient::readPendingDatagrams); - + // connections to AccountManager for authentication connect(&AccountManager::getInstance(), &AccountManager::authRequired, this, &AssignmentClient::handleAuthenticationRequest); @@ -121,49 +114,49 @@ void AssignmentClient::sendAssignmentRequest() { void AssignmentClient::readPendingDatagrams() { NodeList* nodeList = NodeList::getInstance(); - + QByteArray receivedPacket; HifiSockAddr senderSockAddr; - + while (nodeList->getNodeSocket().hasPendingDatagrams()) { receivedPacket.resize(nodeList->getNodeSocket().pendingDatagramSize()); nodeList->getNodeSocket().readDatagram(receivedPacket.data(), receivedPacket.size(), senderSockAddr.getAddressPointer(), senderSockAddr.getPortPointer()); - + if (nodeList->packetVersionAndHashMatch(receivedPacket)) { if (packetTypeForPacket(receivedPacket) == PacketTypeCreateAssignment) { // construct the deployed assignment from the packet data _currentAssignment = SharedAssignmentPointer(AssignmentFactory::unpackAssignment(receivedPacket)); - + if (_currentAssignment) { qDebug() << "Received an assignment -" << *_currentAssignment; - + // switch our DomainHandler hostname and port to whoever sent us the assignment - + nodeList->getDomainHandler().setSockAddr(senderSockAddr, _assignmentServerHostname); nodeList->getDomainHandler().setAssignmentUUID(_currentAssignment->getUUID()); - + qDebug() << "Destination IP for assignment is" << nodeList->getDomainHandler().getIP().toString(); - + // start the deployed assignment AssignmentThread* workerThread = new AssignmentThread(_currentAssignment, this); - + connect(workerThread, &QThread::started, _currentAssignment.data(), &ThreadedAssignment::run); connect(_currentAssignment.data(), &ThreadedAssignment::finished, workerThread, &QThread::quit); connect(_currentAssignment.data(), &ThreadedAssignment::finished, this, &AssignmentClient::assignmentCompleted); connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater); - + _currentAssignment->moveToThread(workerThread); - + // move the NodeList to the thread used for the _current assignment nodeList->moveToThread(workerThread); - + // let the assignment handle the incoming datagrams for its duration disconnect(&nodeList->getNodeSocket(), 0, this, 0); connect(&nodeList->getNodeSocket(), &QUdpSocket::readyRead, _currentAssignment.data(), &ThreadedAssignment::readPendingDatagrams); - + // Starts an event loop, and emits workerThread->started() workerThread->start(); } else { @@ -180,15 +173,15 @@ void AssignmentClient::readPendingDatagrams() { void AssignmentClient::handleAuthenticationRequest() { const QString DATA_SERVER_USERNAME_ENV = "HIFI_AC_USERNAME"; const QString DATA_SERVER_PASSWORD_ENV = "HIFI_AC_PASSWORD"; - + // this node will be using an authentication server, let's make sure we have a username/password QProcessEnvironment sysEnvironment = QProcessEnvironment::systemEnvironment(); - + QString username = sysEnvironment.value(DATA_SERVER_USERNAME_ENV); QString password = sysEnvironment.value(DATA_SERVER_PASSWORD_ENV); - + AccountManager& accountManager = AccountManager::getInstance(); - + if (!username.isEmpty() && !password.isEmpty()) { // ask the account manager to log us in from the env variables accountManager.requestAccessToken(username, password); @@ -196,7 +189,7 @@ void AssignmentClient::handleAuthenticationRequest() { qDebug() << "Authentication was requested against" << qPrintable(accountManager.getAuthURL().toString()) << "but both or one of" << qPrintable(DATA_SERVER_USERNAME_ENV) << "/" << qPrintable(DATA_SERVER_PASSWORD_ENV) << "are not set. Unable to authenticate."; - + return; } } @@ -204,15 +197,15 @@ void AssignmentClient::handleAuthenticationRequest() { void AssignmentClient::assignmentCompleted() { // reset the logging target to the the CHILD_TARGET_NAME Logging::setTargetName(ASSIGNMENT_CLIENT_TARGET_NAME); - + qDebug("Assignment finished or never started - waiting for new assignment."); - + NodeList* nodeList = NodeList::getInstance(); // have us handle incoming NodeList datagrams again disconnect(&nodeList->getNodeSocket(), 0, _currentAssignment.data(), 0); connect(&nodeList->getNodeSocket(), &QUdpSocket::readyRead, this, &AssignmentClient::readPendingDatagrams); - + // clear our current assignment shared pointer now that we're done with it // if the assignment thread is still around it has its own shared pointer to the assignment _currentAssignment.clear(); diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index b3909660e2..d96fce450a 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -335,7 +335,7 @@ void AudioMixer::prepareMixForListeningNode(Node* node) { AudioMixerClientData* otherNodeClientData = (AudioMixerClientData*) otherNode->getLinkedData(); // enumerate the ARBs attached to the otherNode and add all that should be added to mix - for (unsigned int i = 0; i < otherNodeClientData->getRingBuffers().size(); i++) { + for (int i = 0; i < otherNodeClientData->getRingBuffers().size(); i++) { PositionalAudioRingBuffer* otherNodeBuffer = otherNodeClientData->getRingBuffers()[i]; if ((*otherNode != *node diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 7fb2a7dcab..9494e927a9 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -25,14 +25,14 @@ AudioMixerClientData::AudioMixerClientData() : } AudioMixerClientData::~AudioMixerClientData() { - for (unsigned int i = 0; i < _ringBuffers.size(); i++) { + for (int i = 0; i < _ringBuffers.size(); i++) { // delete this attached PositionalAudioRingBuffer delete _ringBuffers[i]; } } AvatarAudioRingBuffer* AudioMixerClientData::getAvatarAudioRingBuffer() const { - for (unsigned int i = 0; i < _ringBuffers.size(); i++) { + for (int i = 0; i < _ringBuffers.size(); i++) { if (_ringBuffers[i]->getType() == PositionalAudioRingBuffer::Microphone) { return (AvatarAudioRingBuffer*) _ringBuffers[i]; } @@ -79,7 +79,7 @@ int AudioMixerClientData::parseData(const QByteArray& packet) { InjectedAudioRingBuffer* matchingInjectedRingBuffer = NULL; - for (unsigned int i = 0; i < _ringBuffers.size(); i++) { + for (int i = 0; i < _ringBuffers.size(); i++) { if (_ringBuffers[i]->getType() == PositionalAudioRingBuffer::Injector && ((InjectedAudioRingBuffer*) _ringBuffers[i])->getStreamIdentifier() == streamIdentifier) { matchingInjectedRingBuffer = (InjectedAudioRingBuffer*) _ringBuffers[i]; @@ -99,7 +99,7 @@ int AudioMixerClientData::parseData(const QByteArray& packet) { } void AudioMixerClientData::checkBuffersBeforeFrameSend(int jitterBufferLengthSamples) { - for (unsigned int i = 0; i < _ringBuffers.size(); i++) { + for (int i = 0; i < _ringBuffers.size(); i++) { if (_ringBuffers[i]->shouldBeAddedToMix(jitterBufferLengthSamples)) { // this is a ring buffer that is ready to go // set its flag so we know to push its buffer when all is said and done @@ -113,7 +113,7 @@ void AudioMixerClientData::checkBuffersBeforeFrameSend(int jitterBufferLengthSam } void AudioMixerClientData::pushBuffersAfterFrameSend() { - for (unsigned int i = 0; i < _ringBuffers.size(); i++) { + for (int i = 0; i < _ringBuffers.size(); i++) { // this was a used buffer, push the output pointer forwards PositionalAudioRingBuffer* audioBuffer = _ringBuffers[i]; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index b5c2d00d3e..d55a9b52ca 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -47,15 +47,15 @@ DomainServer::DomainServer(int argc, char* argv[]) : setOrganizationDomain("highfidelity.io"); setApplicationName("domain-server"); QSettings::setDefaultFormat(QSettings::IniFormat); - + _argumentVariantMap = HifiConfigVariantMap::mergeCLParametersWithJSONConfig(arguments()); - + _networkAccessManager = new QNetworkAccessManager(this); - + if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth() && optionallySetupAssignmentPayment()) { // we either read a certificate and private key or were not passed one // and completed login or did not need to - + qDebug() << "Setting up LimitedNodeList and assignments."; setupNodeListAndAssignments(); } @@ -65,58 +65,58 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { const QString X509_CERTIFICATE_OPTION = "cert"; const QString X509_PRIVATE_KEY_OPTION = "key"; const QString X509_KEY_PASSPHRASE_ENV = "DOMAIN_SERVER_KEY_PASSPHRASE"; - + QString certPath = _argumentVariantMap.value(X509_CERTIFICATE_OPTION).toString(); QString keyPath = _argumentVariantMap.value(X509_PRIVATE_KEY_OPTION).toString(); - + if (!certPath.isEmpty() && !keyPath.isEmpty()) { // the user wants to use DTLS to encrypt communication with nodes // let's make sure we can load the key and certificate // _x509Credentials = new gnutls_certificate_credentials_t; // gnutls_certificate_allocate_credentials(_x509Credentials); - + QString keyPassphraseString = QProcessEnvironment::systemEnvironment().value(X509_KEY_PASSPHRASE_ENV); - + qDebug() << "Reading certificate file at" << certPath << "for DTLS."; qDebug() << "Reading key file at" << keyPath << "for DTLS."; - + // int gnutlsReturn = gnutls_certificate_set_x509_key_file2(*_x509Credentials, // certPath.toLocal8Bit().constData(), // keyPath.toLocal8Bit().constData(), // GNUTLS_X509_FMT_PEM, // keyPassphraseString.toLocal8Bit().constData(), // 0); -// +// // if (gnutlsReturn < 0) { // qDebug() << "Unable to load certificate or key file." << "Error" << gnutlsReturn << "- domain-server will now quit."; // QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); // return false; // } - + // qDebug() << "Successfully read certificate and private key."; - + // we need to also pass this certificate and private key to the HTTPS manager // this is used for Oauth callbacks when authorizing users against a data server - + QFile certFile(certPath); certFile.open(QIODevice::ReadOnly); - + QFile keyFile(keyPath); keyFile.open(QIODevice::ReadOnly); - + QSslCertificate sslCertificate(&certFile); QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, keyPassphraseString.toUtf8()); - + _httpsManager = new HTTPSManager(DOMAIN_SERVER_HTTPS_PORT, sslCertificate, privateKey, QString(), this, this); - + qDebug() << "TCP server listening for HTTPS connections on" << DOMAIN_SERVER_HTTPS_PORT; - + } else if (!certPath.isEmpty() || !keyPath.isEmpty()) { qDebug() << "Missing certificate or private key. domain-server will now quit."; QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); return false; } - + return true; } @@ -125,12 +125,12 @@ bool DomainServer::optionallySetupOAuth() { const QString OAUTH_CLIENT_ID_OPTION = "oauth-client-id"; const QString OAUTH_CLIENT_SECRET_ENV = "DOMAIN_SERVER_CLIENT_SECRET"; const QString REDIRECT_HOSTNAME_OPTION = "hostname"; - + _oauthProviderURL = QUrl(_argumentVariantMap.value(OAUTH_PROVIDER_URL_OPTION).toString()); _oauthClientID = _argumentVariantMap.value(OAUTH_CLIENT_ID_OPTION).toString(); _oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV); _hostname = _argumentVariantMap.value(REDIRECT_HOSTNAME_OPTION).toString(); - + if (!_oauthClientID.isEmpty()) { if (_oauthProviderURL.isEmpty() || _hostname.isEmpty() @@ -144,47 +144,47 @@ bool DomainServer::optionallySetupOAuth() { qDebug() << "OAuth Client ID is" << _oauthClientID; } } - + return true; } void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { - + const QString CUSTOM_PORT_OPTION = "port"; unsigned short domainServerPort = DEFAULT_DOMAIN_SERVER_PORT; - + if (_argumentVariantMap.contains(CUSTOM_PORT_OPTION)) { domainServerPort = (unsigned short) _argumentVariantMap.value(CUSTOM_PORT_OPTION).toUInt(); } - + unsigned short domainServerDTLSPort = 0; - + if (_isUsingDTLS) { domainServerDTLSPort = DEFAULT_DOMAIN_SERVER_DTLS_PORT; - + const QString CUSTOM_DTLS_PORT_OPTION = "dtls-port"; - + if (_argumentVariantMap.contains(CUSTOM_DTLS_PORT_OPTION)) { domainServerDTLSPort = (unsigned short) _argumentVariantMap.value(CUSTOM_DTLS_PORT_OPTION).toUInt(); } } - + QSet parsedTypes; parseAssignmentConfigs(parsedTypes); - + populateDefaultStaticAssignmentsExcludingTypes(parsedTypes); - + LimitedNodeList* nodeList = LimitedNodeList::createInstance(domainServerPort, domainServerDTLSPort); - + connect(nodeList, &LimitedNodeList::nodeAdded, this, &DomainServer::nodeAdded); connect(nodeList, &LimitedNodeList::nodeKilled, this, &DomainServer::nodeKilled); - + QTimer* silentNodeTimer = new QTimer(this); connect(silentNodeTimer, SIGNAL(timeout()), nodeList, SLOT(removeSilentNodes())); silentNodeTimer->start(NODE_SILENCE_THRESHOLD_MSECS); - + connect(&nodeList->getNodeSocket(), SIGNAL(readyRead()), SLOT(readAvailableDatagrams())); - + // add whatever static assignments that have been parsed to the queue addStaticAssignmentsToQueue(); } @@ -194,21 +194,22 @@ bool DomainServer::optionallySetupAssignmentPayment() { const QString PAY_FOR_ASSIGNMENTS_OPTION = "pay-for-assignments"; const QString HIFI_USERNAME_ENV_KEY = "DOMAIN_SERVER_USERNAME"; const QString HIFI_PASSWORD_ENV_KEY = "DOMAIN_SERVER_PASSWORD"; - - if (_argumentVariantMap.contains(PAY_FOR_ASSIGNMENTS_OPTION)) { + + if (_argumentVariantMap.contains(PAY_FOR_ASSIGNMENTS_OPTION) && + _argumentVariantMap.value(PAY_FOR_ASSIGNMENTS_OPTION).toBool()) { if (!_oauthProviderURL.isEmpty()) { - + AccountManager& accountManager = AccountManager::getInstance(); accountManager.setAuthURL(_oauthProviderURL); - + if (!accountManager.hasValidAccessToken()) { // we don't have a valid access token so we need to get one QString username = QProcessEnvironment::systemEnvironment().value(HIFI_USERNAME_ENV_KEY); QString password = QProcessEnvironment::systemEnvironment().value(HIFI_PASSWORD_ENV_KEY); - + if (!username.isEmpty() && !password.isEmpty()) { accountManager.requestAccessToken(username, password); - + // connect to loginFailed signal from AccountManager so we can quit if that is the case connect(&accountManager, &AccountManager::loginFailed, this, &DomainServer::loginFailed); } else { @@ -217,31 +218,31 @@ bool DomainServer::optionallySetupAssignmentPayment() { return false; } } - + qDebug() << "Assignments will be paid for via" << qPrintable(_oauthProviderURL.toString()); - + // assume that the fact we are authing against HF data server means we will pay for assignments // setup a timer to send transactions to pay assigned nodes every 30 seconds QTimer* creditSetupTimer = new QTimer(this); connect(creditSetupTimer, &QTimer::timeout, this, &DomainServer::setupPendingAssignmentCredits); - + const qint64 CREDIT_CHECK_INTERVAL_MSECS = 5 * 1000; creditSetupTimer->start(CREDIT_CHECK_INTERVAL_MSECS); - + QTimer* nodePaymentTimer = new QTimer(this); connect(nodePaymentTimer, &QTimer::timeout, this, &DomainServer::sendPendingTransactionsToServer); - + const qint64 TRANSACTION_SEND_INTERVAL_MSECS = 30 * 1000; nodePaymentTimer->start(TRANSACTION_SEND_INTERVAL_MSECS); - + } else { qDebug() << "Missing OAuth provider URL, but assigned node payment was enabled. domain-server will now quit."; QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection); - + return false; } } - + return true; } @@ -254,35 +255,35 @@ void DomainServer::parseAssignmentConfigs(QSet& excludedTypes) // check for configs from the command line, these take precedence const QString ASSIGNMENT_CONFIG_REGEX_STRING = "config-([\\d]+)"; QRegExp assignmentConfigRegex(ASSIGNMENT_CONFIG_REGEX_STRING); - + // scan for assignment config keys QStringList variantMapKeys = _argumentVariantMap.keys(); int configIndex = variantMapKeys.indexOf(assignmentConfigRegex); - + while (configIndex != -1) { // figure out which assignment type this matches Assignment::Type assignmentType = (Assignment::Type) assignmentConfigRegex.cap(1).toInt(); - + if (assignmentType < Assignment::AllTypes && !excludedTypes.contains(assignmentType)) { QVariant mapValue = _argumentVariantMap[variantMapKeys[configIndex]]; QJsonArray assignmentArray; - + if (mapValue.type() == QVariant::String) { QJsonDocument deserializedDocument = QJsonDocument::fromJson(mapValue.toString().toUtf8()); assignmentArray = deserializedDocument.array(); } else { assignmentArray = mapValue.toJsonValue().toArray(); } - + if (assignmentType != Assignment::AgentType) { createStaticAssignmentsForType(assignmentType, assignmentArray); } else { createScriptedAssignmentsFromArray(assignmentArray); } - + excludedTypes.insert(assignmentType); } - + configIndex = variantMapKeys.indexOf(assignmentConfigRegex, configIndex + 1); } } @@ -293,34 +294,34 @@ void DomainServer::addStaticAssignmentToAssignmentHash(Assignment* newAssignment _allAssignments.insert(newAssignment->getUUID(), SharedAssignmentPointer(newAssignment)); } -void DomainServer::createScriptedAssignmentsFromArray(const QJsonArray &configArray) { +void DomainServer::createScriptedAssignmentsFromArray(const QJsonArray &configArray) { foreach(const QJsonValue& jsonValue, configArray) { if (jsonValue.isObject()) { QJsonObject jsonObject = jsonValue.toObject(); - + // make sure we were passed a URL, otherwise this is an invalid scripted assignment const QString ASSIGNMENT_URL_KEY = "url"; QString assignmentURL = jsonObject[ASSIGNMENT_URL_KEY].toString(); - + if (!assignmentURL.isEmpty()) { // check the json for a pool const QString ASSIGNMENT_POOL_KEY = "pool"; QString assignmentPool = jsonObject[ASSIGNMENT_POOL_KEY].toString(); - + // check for a number of instances, if not passed then default is 1 const QString ASSIGNMENT_INSTANCES_KEY = "instances"; int numInstances = jsonObject[ASSIGNMENT_INSTANCES_KEY].toInt(); numInstances = (numInstances == 0 ? 1 : numInstances); - + qDebug() << "Adding a static scripted assignment from" << assignmentURL; - + for (int i = 0; i < numInstances; i++) { // add a scripted assignment to the queue for this instance Assignment* scriptAssignment = new Assignment(Assignment::CreateCommand, Assignment::AgentType, assignmentPool); scriptAssignment->setPayload(assignmentURL.toUtf8()); - + // scripts passed on CL or via JSON are static - so they are added back to the queue if the node dies addStaticAssignmentToAssignmentHash(scriptAssignment); } @@ -332,38 +333,38 @@ void DomainServer::createScriptedAssignmentsFromArray(const QJsonArray &configAr void DomainServer::createStaticAssignmentsForType(Assignment::Type type, const QJsonArray& configArray) { // we have a string for config for this type qDebug() << "Parsing config for assignment type" << type; - + int configCounter = 0; - + foreach(const QJsonValue& jsonValue, configArray) { if (jsonValue.isObject()) { QJsonObject jsonObject = jsonValue.toObject(); - + // check the config string for a pool const QString ASSIGNMENT_POOL_KEY = "pool"; QString assignmentPool; - + QJsonValue poolValue = jsonObject[ASSIGNMENT_POOL_KEY]; if (!poolValue.isUndefined()) { assignmentPool = poolValue.toString(); - + jsonObject.remove(ASSIGNMENT_POOL_KEY); } - + ++configCounter; qDebug() << "Type" << type << "config" << configCounter << "=" << jsonObject; - + Assignment* configAssignment = new Assignment(Assignment::CreateCommand, type, assignmentPool); - + // setup the payload as a semi-colon separated list of key = value QStringList payloadStringList; foreach(const QString& payloadKey, jsonObject.keys()) { QString dashes = payloadKey.size() == 1 ? "-" : "--"; payloadStringList << QString("%1%2 %3").arg(dashes).arg(payloadKey).arg(jsonObject[payloadKey].toString()); } - + configAssignment->setPayload(payloadStringList.join(' ').toUtf8()); - + addStaticAssignmentToAssignmentHash(configAssignment); } } @@ -393,27 +394,27 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock NodeType_t nodeType; HifiSockAddr publicSockAddr, localSockAddr; - + int numPreInterestBytes = parseNodeDataFromByteArray(nodeType, publicSockAddr, localSockAddr, packet, senderSockAddr); - + QUuid packetUUID = uuidFromPacketHeader(packet); // check if this connect request matches an assignment in the queue bool isAssignment = _pendingAssignedNodes.contains(packetUUID); SharedAssignmentPointer matchingQueuedAssignment = SharedAssignmentPointer(); PendingAssignedNodeData* pendingAssigneeData = NULL; - + if (isAssignment) { pendingAssigneeData = _pendingAssignedNodes.value(packetUUID); - + if (pendingAssigneeData) { matchingQueuedAssignment = matchingQueuedAssignmentForCheckIn(pendingAssigneeData->getAssignmentUUID(), nodeType); - + if (matchingQueuedAssignment) { qDebug() << "Assignment deployed with" << uuidStringWithoutCurlyBraces(packetUUID) << "matches unfulfilled assignment" << uuidStringWithoutCurlyBraces(matchingQueuedAssignment->getUUID()); - + // remove this unique assignment deployment from the hash of pending assigned nodes // cleanup of the PendingAssignedNodeData happens below after the node has been added to the LimitedNodeList _pendingAssignedNodes.remove(packetUUID); @@ -424,9 +425,9 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock return; } } - + } - + if (!isAssignment && !_oauthProviderURL.isEmpty() && _argumentVariantMap.contains(ALLOWED_ROLES_CONFIG_KEY)) { // this is an Agent, and we require authentication so we can compare the user's roles to our list of allowed ones if (_sessionAuthenticationHash.contains(packetUUID)) { @@ -445,37 +446,37 @@ void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSock QDataStream oauthRequestStream(&oauthRequestByteArray, QIODevice::Append); QUrl authorizationURL = packetUUID.isNull() ? oauthAuthorizationURL() : oauthAuthorizationURL(packetUUID); oauthRequestStream << authorizationURL; - + // send this oauth request datagram back to the client LimitedNodeList::getInstance()->writeUnverifiedDatagram(oauthRequestByteArray, senderSockAddr); - + return; } } - + if ((!isAssignment && !STATICALLY_ASSIGNED_NODES.contains(nodeType)) || (isAssignment && matchingQueuedAssignment)) { // this was either not a static assignment or it was and we had a matching one in the queue - + // create a new session UUID for this node QUuid nodeUUID = QUuid::createUuid(); - + SharedNodePointer newNode = LimitedNodeList::getInstance()->addOrUpdateNode(nodeUUID, nodeType, publicSockAddr, localSockAddr); // when the newNode is created the linked data is also created // if this was a static assignment set the UUID, set the sendingSockAddr DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData()); - + if (isAssignment) { nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID()); nodeData->setWalletUUID(pendingAssigneeData->getWalletUUID()); - + // now that we've pulled the wallet UUID and added the node to our list, delete the pending assignee data delete pendingAssigneeData; } - + nodeData->setSendingSockAddr(senderSockAddr); - + // reply back to the user with a PacketTypeDomainList sendDomainListToNode(newNode, senderSockAddr, nodeInterestListFromPacket(packet, numPreInterestBytes)); } @@ -492,26 +493,26 @@ QUrl DomainServer::oauthAuthorizationURL(const QUuid& stateUUID) { // for now these are all interface clients that have a GUI // so just send them back the full authorization URL QUrl authorizationURL = _oauthProviderURL; - + const QString OAUTH_AUTHORIZATION_PATH = "/oauth/authorize"; authorizationURL.setPath(OAUTH_AUTHORIZATION_PATH); - + QUrlQuery authorizationQuery; - + authorizationQuery.addQueryItem(OAUTH_CLIENT_ID_QUERY_KEY, _oauthClientID); - + const QString OAUTH_RESPONSE_TYPE_QUERY_KEY = "response_type"; const QString OAUTH_REPSONSE_TYPE_QUERY_VALUE = "code"; authorizationQuery.addQueryItem(OAUTH_RESPONSE_TYPE_QUERY_KEY, OAUTH_REPSONSE_TYPE_QUERY_VALUE); - + const QString OAUTH_STATE_QUERY_KEY = "state"; // create a new UUID that will be the state parameter for oauth authorization AND the new session UUID for that node authorizationQuery.addQueryItem(OAUTH_STATE_QUERY_KEY, uuidStringWithoutCurlyBraces(stateUUID)); - + authorizationQuery.addQueryItem(OAUTH_REDIRECT_URI_QUERY_KEY, oauthRedirectURL().toString()); - + authorizationURL.setQuery(authorizationQuery); - + return authorizationURL; } @@ -520,14 +521,14 @@ int DomainServer::parseNodeDataFromByteArray(NodeType_t& nodeType, HifiSockAddr& const HifiSockAddr& senderSockAddr) { QDataStream packetStream(packet); packetStream.skipRawData(numBytesForPacketHeader(packet)); - + packetStream >> nodeType; packetStream >> publicSockAddr >> localSockAddr; - + if (publicSockAddr.getAddress().isNull()) { // this node wants to use us its STUN server // so set the node public address to whatever we perceive the public address to be - + // if the sender is on our box then leave its public address to 0 so that // other users attempt to reach it on the same address they have for the domain-server if (senderSockAddr.getAddress().isLoopback()) { @@ -536,95 +537,95 @@ int DomainServer::parseNodeDataFromByteArray(NodeType_t& nodeType, HifiSockAddr& publicSockAddr.setAddress(senderSockAddr.getAddress()); } } - + return packetStream.device()->pos(); } NodeSet DomainServer::nodeInterestListFromPacket(const QByteArray& packet, int numPreceedingBytes) { QDataStream packetStream(packet); packetStream.skipRawData(numPreceedingBytes); - + quint8 numInterestTypes = 0; packetStream >> numInterestTypes; - + quint8 nodeType; NodeSet nodeInterestSet; - + for (int i = 0; i < numInterestTypes; i++) { packetStream >> nodeType; nodeInterestSet.insert((NodeType_t) nodeType); } - + return nodeInterestSet; } void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr &senderSockAddr, const NodeSet& nodeInterestList) { - + QByteArray broadcastPacket = byteArrayWithPopulatedHeader(PacketTypeDomainList); - + // always send the node their own UUID back QDataStream broadcastDataStream(&broadcastPacket, QIODevice::Append); broadcastDataStream << node->getUUID(); - + int numBroadcastPacketLeadBytes = broadcastDataStream.device()->pos(); - + DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); - + LimitedNodeList* nodeList = LimitedNodeList::getInstance(); - + if (nodeInterestList.size() > 0) { - + // DTLSServerSession* dtlsSession = _isUsingDTLS ? _dtlsSessions[senderSockAddr] : NULL; int dataMTU = MAX_PACKET_SIZE; - + if (nodeData->isAuthenticated()) { // if this authenticated node has any interest types, send back those nodes as well foreach (const SharedNodePointer& otherNode, nodeList->getNodeHash()) { - + // reset our nodeByteArray and nodeDataStream QByteArray nodeByteArray; QDataStream nodeDataStream(&nodeByteArray, QIODevice::Append); - + if (otherNode->getUUID() != node->getUUID() && nodeInterestList.contains(otherNode->getType())) { - + // don't send avatar nodes to other avatars, that will come from avatar mixer nodeDataStream << *otherNode.data(); - + // pack the secret that these two nodes will use to communicate with each other QUuid secretUUID = nodeData->getSessionSecretHash().value(otherNode->getUUID()); if (secretUUID.isNull()) { // generate a new secret UUID these two nodes can use secretUUID = QUuid::createUuid(); - + // set that on the current Node's sessionSecretHash nodeData->getSessionSecretHash().insert(otherNode->getUUID(), secretUUID); - + // set it on the other Node's sessionSecretHash reinterpret_cast(otherNode->getLinkedData()) ->getSessionSecretHash().insert(node->getUUID(), secretUUID); - + } - + nodeDataStream << secretUUID; - + if (broadcastPacket.size() + nodeByteArray.size() > dataMTU) { // we need to break here and start a new packet // so send the current one - + nodeList->writeDatagram(broadcastPacket, node, senderSockAddr); - + // reset the broadcastPacket structure broadcastPacket.resize(numBroadcastPacketLeadBytes); broadcastDataStream.device()->seek(numBroadcastPacketLeadBytes); } - + // append the nodeByteArray to the current state of broadcastDataStream broadcastPacket.append(nodeByteArray); } } } - + // always write the last broadcastPacket nodeList->writeDatagram(broadcastPacket, node, senderSockAddr); } @@ -635,7 +636,7 @@ void DomainServer::readAvailableDatagrams() { HifiSockAddr senderSockAddr; QByteArray receivedPacket; - + static QByteArray assignmentPacket = byteArrayWithPopulatedHeader(PacketTypeCreateAssignment); static int numAssignmentPacketHeaderBytes = assignmentPacket.size(); @@ -648,44 +649,44 @@ void DomainServer::readAvailableDatagrams() { // construct the requested assignment from the packet data Assignment requestAssignment(receivedPacket); - + // Suppress these for Assignment::AgentType to once per 5 seconds static QElapsedTimer noisyMessageTimer; static bool wasNoisyTimerStarted = false; - + if (!wasNoisyTimerStarted) { noisyMessageTimer.start(); wasNoisyTimerStarted = true; } - - const quint64 NOISY_MESSAGE_INTERVAL_MSECS = 5 * 1000; - + + const qint64 NOISY_MESSAGE_INTERVAL_MSECS = 5 * 1000; + if (requestAssignment.getType() != Assignment::AgentType || noisyMessageTimer.elapsed() > NOISY_MESSAGE_INTERVAL_MSECS) { qDebug() << "Received a request for assignment type" << requestAssignment.getType() << "from" << senderSockAddr; noisyMessageTimer.restart(); } - + SharedAssignmentPointer assignmentToDeploy = deployableAssignmentForRequest(requestAssignment); - + if (assignmentToDeploy) { qDebug() << "Deploying assignment -" << *assignmentToDeploy.data() << "- to" << senderSockAddr; - + // give this assignment out, either the type matches or the requestor said they will take any assignmentPacket.resize(numAssignmentPacketHeaderBytes); - + // setup a copy of this assignment that will have a unique UUID, for packaging purposes Assignment uniqueAssignment(*assignmentToDeploy.data()); uniqueAssignment.setUUID(QUuid::createUuid()); - + QDataStream assignmentStream(&assignmentPacket, QIODevice::Append); - + assignmentStream << uniqueAssignment; - + nodeList->getNodeSocket().writeDatagram(assignmentPacket, senderSockAddr.getAddress(), senderSockAddr.getPort()); - + // add the information for that deployed assignment to the hash of pending assigned nodes PendingAssignedNodeData* pendingNodeData = new PendingAssignedNodeData(assignmentToDeploy->getUUID(), requestAssignment.getWalletUUID()); @@ -705,7 +706,7 @@ void DomainServer::readAvailableDatagrams() { // we're using DTLS, so tell the sender to get back to us using DTLS static QByteArray dtlsRequiredPacket = byteArrayWithPopulatedHeader(PacketTypeDomainServerRequireDTLS); static int numBytesDTLSHeader = numBytesForPacketHeaderGivenPacketType(PacketTypeDomainServerRequireDTLS); - + if (dtlsRequiredPacket.size() == numBytesDTLSHeader) { // pack the port that we accept DTLS traffic on unsigned short dtlsPort = nodeList->getDTLSSocket().localPort(); @@ -721,12 +722,12 @@ void DomainServer::setupPendingAssignmentCredits() { // enumerate the NodeList to find the assigned nodes foreach (const SharedNodePointer& node, LimitedNodeList::getInstance()->getNodeHash()) { DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); - + if (!nodeData->getAssignmentUUID().isNull() && !nodeData->getWalletUUID().isNull()) { // check if we have a non-finalized transaction for this node to add this amount to TransactionHash::iterator i = _pendingAssignmentCredits.find(nodeData->getWalletUUID()); WalletTransaction* existingTransaction = NULL; - + while (i != _pendingAssignmentCredits.end() && i.key() == nodeData->getWalletUUID()) { if (!i.value()->isFinalized()) { existingTransaction = i.value(); @@ -735,16 +736,16 @@ void DomainServer::setupPendingAssignmentCredits() { ++i; } } - + qint64 elapsedMsecsSinceLastPayment = nodeData->getPaymentIntervalTimer().elapsed(); nodeData->getPaymentIntervalTimer().restart(); - + const float CREDITS_PER_HOUR = 0.10f; const float CREDITS_PER_MSEC = CREDITS_PER_HOUR / (60 * 60 * 1000); const int SATOSHIS_PER_MSEC = CREDITS_PER_MSEC * SATOSHIS_PER_CREDIT; - + float pendingCredits = elapsedMsecsSinceLastPayment * SATOSHIS_PER_MSEC; - + if (existingTransaction) { existingTransaction->incrementAmount(pendingCredits); } else { @@ -757,30 +758,30 @@ void DomainServer::setupPendingAssignmentCredits() { } void DomainServer::sendPendingTransactionsToServer() { - + AccountManager& accountManager = AccountManager::getInstance(); - + if (accountManager.hasValidAccessToken()) { - + // enumerate the pending transactions and send them to the server to complete payment TransactionHash::iterator i = _pendingAssignmentCredits.begin(); - + JSONCallbackParameters transactionCallbackParams; - + transactionCallbackParams.jsonCallbackReceiver = this; transactionCallbackParams.jsonCallbackMethod = "transactionJSONCallback"; - + while (i != _pendingAssignmentCredits.end()) { accountManager.authenticatedRequest("api/v1/transactions", QNetworkAccessManager::PostOperation, transactionCallbackParams, i.value()->postJson().toJson()); - + // set this transaction to finalized so we don't add additional credits to it i.value()->setIsFinalized(true); - + ++i; } } - + } void DomainServer::transactionJSONCallback(const QJsonObject& data) { @@ -789,18 +790,18 @@ void DomainServer::transactionJSONCallback(const QJsonObject& data) { // create a dummy wallet transaction to unpack the JSON to WalletTransaction dummyTransaction; dummyTransaction.loadFromJson(data); - + TransactionHash::iterator i = _pendingAssignmentCredits.find(dummyTransaction.getDestinationUUID()); - + while (i != _pendingAssignmentCredits.end() && i.key() == dummyTransaction.getDestinationUUID()) { if (i.value()->getUUID() == dummyTransaction.getUUID()) { // we have a match - we can remove this from the hash of pending credits // and delete it for clean up - + WalletTransaction* matchingTransaction = i.value(); _pendingAssignmentCredits.erase(i); delete matchingTransaction; - + break; } else { ++i; @@ -811,28 +812,28 @@ void DomainServer::transactionJSONCallback(const QJsonObject& data) { void DomainServer::processDatagram(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr) { LimitedNodeList* nodeList = LimitedNodeList::getInstance(); - + if (nodeList->packetVersionAndHashMatch(receivedPacket)) { PacketType requestType = packetTypeForPacket(receivedPacket); - + if (requestType == PacketTypeDomainConnectRequest) { handleConnectRequest(receivedPacket, senderSockAddr); } else if (requestType == PacketTypeDomainListRequest) { QUuid nodeUUID = uuidFromPacketHeader(receivedPacket); - + if (!nodeUUID.isNull() && nodeList->nodeWithUUID(nodeUUID)) { NodeType_t throwawayNodeType; HifiSockAddr nodePublicAddress, nodeLocalAddress; - + int numNodeInfoBytes = parseNodeDataFromByteArray(throwawayNodeType, nodePublicAddress, nodeLocalAddress, receivedPacket, senderSockAddr); - + SharedNodePointer checkInNode = nodeList->updateSocketsForNode(nodeUUID, nodePublicAddress, nodeLocalAddress); - + // update last receive to now quint64 timeNow = usecTimestampNow(); checkInNode->setLastHeardMicrostamp(timeNow); - + sendDomainListToNode(checkInNode, senderSockAddr, nodeInterestListFromPacket(receivedPacket, numNodeInfoBytes)); } } else if (requestType == PacketTypeNodeJsonStats) { @@ -871,36 +872,36 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) { // add the node UUID nodeJson[JSON_KEY_UUID] = uuidStringWithoutCurlyBraces(node->getUUID()); - + // add the node type nodeJson[JSON_KEY_TYPE] = nodeTypeName; // add the node socket information nodeJson[JSON_KEY_PUBLIC_SOCKET] = jsonForSocket(node->getPublicSocket()); nodeJson[JSON_KEY_LOCAL_SOCKET] = jsonForSocket(node->getLocalSocket()); - + // add the node uptime in our list nodeJson[JSON_KEY_WAKE_TIMESTAMP] = QString::number(node->getWakeTimestamp()); - + // if the node has pool information, add it DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); SharedAssignmentPointer matchingAssignment = _allAssignments.value(nodeData->getAssignmentUUID()); if (matchingAssignment) { nodeJson[JSON_KEY_POOL] = matchingAssignment->getPool(); - + if (!nodeData->getWalletUUID().isNull()) { TransactionHash::iterator i = _pendingAssignmentCredits.find(nodeData->getWalletUUID()); float pendingCreditAmount = 0; - + while (i != _pendingAssignmentCredits.end() && i.key() == nodeData->getWalletUUID()) { pendingCreditAmount += i.value()->getAmount() / SATOSHIS_PER_CREDIT; ++i; } - + nodeJson[JSON_KEY_PENDING_CREDITS] = pendingCreditAmount; } } - + return nodeJson; } @@ -916,157 +917,157 @@ QString pathForAssignmentScript(const QUuid& assignmentUUID) { bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url) { const QString JSON_MIME_TYPE = "application/json"; - + const QString URI_ASSIGNMENT = "/assignment"; const QString URI_NODES = "/nodes"; - + const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; - + if (connection->requestOperation() == QNetworkAccessManager::GetOperation) { if (url.path() == "/assignments.json") { // user is asking for json list of assignments - + // setup the JSON QJsonObject assignmentJSON; QJsonObject assignedNodesJSON; - + // enumerate the NodeList to find the assigned nodes foreach (const SharedNodePointer& node, LimitedNodeList::getInstance()->getNodeHash()) { DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); - + if (!nodeData->getAssignmentUUID().isNull()) { // add the node using the UUID as the key QString uuidString = uuidStringWithoutCurlyBraces(nodeData->getAssignmentUUID()); assignedNodesJSON[uuidString] = jsonObjectForNode(node); } } - + assignmentJSON["fulfilled"] = assignedNodesJSON; - + QJsonObject queuedAssignmentsJSON; - + // add the queued but unfilled assignments to the json foreach(const SharedAssignmentPointer& assignment, _unfulfilledAssignments) { QJsonObject queuedAssignmentJSON; - + QString uuidString = uuidStringWithoutCurlyBraces(assignment->getUUID()); queuedAssignmentJSON[JSON_KEY_TYPE] = QString(assignment->getTypeName()); - + // if the assignment has a pool, add it if (!assignment->getPool().isEmpty()) { queuedAssignmentJSON[JSON_KEY_POOL] = assignment->getPool(); } - + // add this queued assignment to the JSON queuedAssignmentsJSON[uuidString] = queuedAssignmentJSON; } - + assignmentJSON["queued"] = queuedAssignmentsJSON; - + // print out the created JSON QJsonDocument assignmentDocument(assignmentJSON); connection->respond(HTTPConnection::StatusCode200, assignmentDocument.toJson(), qPrintable(JSON_MIME_TYPE)); - + // we've processed this request return true; } else if (url.path() == "/transactions.json") { // enumerate our pending transactions and display them in an array QJsonObject rootObject; QJsonArray transactionArray; - + TransactionHash::iterator i = _pendingAssignmentCredits.begin(); while (i != _pendingAssignmentCredits.end()) { transactionArray.push_back(i.value()->toJson()); ++i; } - + rootObject["pending_transactions"] = transactionArray; - + // print out the created JSON QJsonDocument transactionsDocument(rootObject); connection->respond(HTTPConnection::StatusCode200, transactionsDocument.toJson(), qPrintable(JSON_MIME_TYPE)); - + return true; } else if (url.path() == QString("%1.json").arg(URI_NODES)) { // setup the JSON QJsonObject rootJSON; QJsonArray nodesJSONArray; - + // enumerate the NodeList to find the assigned nodes LimitedNodeList* nodeList = LimitedNodeList::getInstance(); - + foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { // add the node using the UUID as the key nodesJSONArray.append(jsonObjectForNode(node)); } - + rootJSON["nodes"] = nodesJSONArray; - + // print out the created JSON QJsonDocument nodesDocument(rootJSON); - + // send the response connection->respond(HTTPConnection::StatusCode200, nodesDocument.toJson(), qPrintable(JSON_MIME_TYPE)); - + return true; } else { // check if this is for json stats for a node const QString NODE_JSON_REGEX_STRING = QString("\\%1\\/(%2).json\\/?$").arg(URI_NODES).arg(UUID_REGEX_STRING); QRegExp nodeShowRegex(NODE_JSON_REGEX_STRING); - + if (nodeShowRegex.indexIn(url.path()) != -1) { QUuid matchingUUID = QUuid(nodeShowRegex.cap(1)); - + // see if we have a node that matches this ID SharedNodePointer matchingNode = LimitedNodeList::getInstance()->nodeWithUUID(matchingUUID); if (matchingNode) { // create a QJsonDocument with the stats QJsonObject QJsonObject statsObject = reinterpret_cast(matchingNode->getLinkedData())->getStatsJSONObject(); - + // add the node type to the JSON data for output purposes statsObject["node_type"] = NodeType::getNodeTypeName(matchingNode->getType()).toLower().replace(' ', '-'); - + QJsonDocument statsDocument(statsObject); - + // send the response connection->respond(HTTPConnection::StatusCode200, statsDocument.toJson(), qPrintable(JSON_MIME_TYPE)); - + // tell the caller we processed the request return true; } - + return false; } - + // check if this is a request for a scripted assignment (with a temp unique UUID) const QString ASSIGNMENT_REGEX_STRING = QString("\\%1\\/(%2)\\/?$").arg(URI_ASSIGNMENT).arg(UUID_REGEX_STRING); QRegExp assignmentRegex(ASSIGNMENT_REGEX_STRING); - + if (assignmentRegex.indexIn(url.path()) != -1) { QUuid matchingUUID = QUuid(assignmentRegex.cap(1)); - + SharedAssignmentPointer matchingAssignment = _allAssignments.value(matchingUUID); if (!matchingAssignment) { // check if we have a pending assignment that matches this temp UUID, and it is a scripted assignment PendingAssignedNodeData* pendingData = _pendingAssignedNodes.value(matchingUUID); if (pendingData) { matchingAssignment = _allAssignments.value(pendingData->getAssignmentUUID()); - + if (matchingAssignment && matchingAssignment->getType() == Assignment::AgentType) { // we have a matching assignment and it is for the right type, have the HTTP manager handle it // via correct URL for the script so the client can download - + QUrl scriptURL = url; scriptURL.setPath(URI_ASSIGNMENT + "/" + uuidStringWithoutCurlyBraces(pendingData->getAssignmentUUID())); - + // have the HTTPManager serve the appropriate script file return _httpManager.handleHTTPRequest(connection, scriptURL); } } } - + // request not handled return false; } @@ -1075,92 +1076,92 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url if (url.path() == URI_ASSIGNMENT) { // this is a script upload - ask the HTTPConnection to parse the form data QList formData = connection->parseFormData(); - + // check optional headers for # of instances and pool const QString ASSIGNMENT_INSTANCES_HEADER = "ASSIGNMENT-INSTANCES"; const QString ASSIGNMENT_POOL_HEADER = "ASSIGNMENT-POOL"; - + QByteArray assignmentInstancesValue = connection->requestHeaders().value(ASSIGNMENT_INSTANCES_HEADER.toLocal8Bit()); - + int numInstances = 1; - + if (!assignmentInstancesValue.isEmpty()) { // the user has requested a specific number of instances // so set that on the created assignment - + numInstances = assignmentInstancesValue.toInt(); } - + QString assignmentPool = emptyPool; QByteArray assignmentPoolValue = connection->requestHeaders().value(ASSIGNMENT_POOL_HEADER.toLocal8Bit()); - + if (!assignmentPoolValue.isEmpty()) { // specific pool requested, set that on the created assignment assignmentPool = QString(assignmentPoolValue); } - - + + for (int i = 0; i < numInstances; i++) { - + // create an assignment for this saved script Assignment* scriptAssignment = new Assignment(Assignment::CreateCommand, Assignment::AgentType, assignmentPool); - + QString newPath = pathForAssignmentScript(scriptAssignment->getUUID()); - + // create a file with the GUID of the assignment in the script host location QFile scriptFile(newPath); scriptFile.open(QIODevice::WriteOnly); scriptFile.write(formData[0].second); - + qDebug() << qPrintable(QString("Saved a script for assignment at %1%2") .arg(newPath).arg(assignmentPool == emptyPool ? "" : " - pool is " + assignmentPool)); - + // add the script assigment to the assignment queue SharedAssignmentPointer sharedScriptedAssignment(scriptAssignment); _unfulfilledAssignments.enqueue(sharedScriptedAssignment); _allAssignments.insert(sharedScriptedAssignment->getUUID(), sharedScriptedAssignment); } - + // respond with a 200 code for successful upload connection->respond(HTTPConnection::StatusCode200); - + return true; } } else if (connection->requestOperation() == QNetworkAccessManager::DeleteOperation) { const QString ALL_NODE_DELETE_REGEX_STRING = QString("\\%1\\/?$").arg(URI_NODES); const QString NODE_DELETE_REGEX_STRING = QString("\\%1\\/(%2)\\/$").arg(URI_NODES).arg(UUID_REGEX_STRING); - + QRegExp allNodesDeleteRegex(ALL_NODE_DELETE_REGEX_STRING); QRegExp nodeDeleteRegex(NODE_DELETE_REGEX_STRING); - + if (nodeDeleteRegex.indexIn(url.path()) != -1) { // this is a request to DELETE one node by UUID - + // pull the captured string, if it exists QUuid deleteUUID = QUuid(nodeDeleteRegex.cap(1)); - + SharedNodePointer nodeToKill = LimitedNodeList::getInstance()->nodeWithUUID(deleteUUID); - + if (nodeToKill) { // start with a 200 response connection->respond(HTTPConnection::StatusCode200); - + // we have a valid UUID and node - kill the node that has this assignment QMetaObject::invokeMethod(LimitedNodeList::getInstance(), "killNodeWithUUID", Q_ARG(const QUuid&, deleteUUID)); - + // successfully processed request return true; } - + return true; } else if (allNodesDeleteRegex.indexIn(url.path()) != -1) { qDebug() << "Received request to kill all nodes."; LimitedNodeList::getInstance()->eraseAllNodes(); - + return true; } } - + // didn't process the request, let the HTTPManager try and handle return false; } @@ -1168,44 +1169,44 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &url) { const QString URI_OAUTH = "/oauth"; if (url.path() == URI_OAUTH) { - + QUrlQuery codeURLQuery(url); - + const QString CODE_QUERY_KEY = "code"; QString authorizationCode = codeURLQuery.queryItemValue(CODE_QUERY_KEY); - + const QString STATE_QUERY_KEY = "state"; QUuid stateUUID = QUuid(codeURLQuery.queryItemValue(STATE_QUERY_KEY)); - - + + if (!authorizationCode.isEmpty() && !stateUUID.isNull()) { // fire off a request with this code and state to get an access token for the user - + const QString OAUTH_TOKEN_REQUEST_PATH = "/oauth/token"; QUrl tokenRequestUrl = _oauthProviderURL; tokenRequestUrl.setPath(OAUTH_TOKEN_REQUEST_PATH); - + const QString OAUTH_GRANT_TYPE_POST_STRING = "grant_type=authorization_code"; QString tokenPostBody = OAUTH_GRANT_TYPE_POST_STRING; tokenPostBody += QString("&code=%1&redirect_uri=%2&client_id=%3&client_secret=%4") .arg(authorizationCode, oauthRedirectURL().toString(), _oauthClientID, _oauthClientSecret); - + QNetworkRequest tokenRequest(tokenRequestUrl); tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - + QNetworkReply* tokenReply = _networkAccessManager->post(tokenRequest, tokenPostBody.toLocal8Bit()); - + qDebug() << "Requesting a token for user with session UUID" << uuidStringWithoutCurlyBraces(stateUUID); - + // insert this to our pending token replies so we can associate the returned access token with the right UUID _networkReplyUUIDMap.insert(tokenReply, stateUUID); - + connect(tokenReply, &QNetworkReply::finished, this, &DomainServer::handleTokenRequestFinished); } - + // respond with a 200 code indicating that login is complete connection->respond(HTTPConnection::StatusCode200); - + return true; } else { return false; @@ -1217,25 +1218,25 @@ const QString OAUTH_JSON_ACCESS_TOKEN_KEY = "access_token"; void DomainServer::handleTokenRequestFinished() { QNetworkReply* networkReply = reinterpret_cast(sender()); QUuid matchingSessionUUID = _networkReplyUUIDMap.take(networkReply); - + if (!matchingSessionUUID.isNull() && networkReply->error() == QNetworkReply::NoError) { // pull the access token from the returned JSON and store it with the matching session UUID QJsonDocument returnedJSON = QJsonDocument::fromJson(networkReply->readAll()); QString accessToken = returnedJSON.object()[OAUTH_JSON_ACCESS_TOKEN_KEY].toString(); - + qDebug() << "Received access token for user with UUID" << uuidStringWithoutCurlyBraces(matchingSessionUUID); - + // fire off a request to get this user's identity so we can see if we will let them in QUrl profileURL = _oauthProviderURL; profileURL.setPath("/api/v1/users/profile"); profileURL.setQuery(QString("%1=%2").arg(OAUTH_JSON_ACCESS_TOKEN_KEY, accessToken)); - + QNetworkReply* profileReply = _networkAccessManager->get(QNetworkRequest(profileURL)); - + qDebug() << "Requesting access token for user with session UUID" << uuidStringWithoutCurlyBraces(matchingSessionUUID); - + connect(profileReply, &QNetworkReply::finished, this, &DomainServer::handleProfileRequestFinished); - + _networkReplyUUIDMap.insert(profileReply, matchingSessionUUID); } } @@ -1243,18 +1244,18 @@ void DomainServer::handleTokenRequestFinished() { void DomainServer::handleProfileRequestFinished() { QNetworkReply* networkReply = reinterpret_cast(sender()); QUuid matchingSessionUUID = _networkReplyUUIDMap.take(networkReply); - + if (!matchingSessionUUID.isNull() && networkReply->error() == QNetworkReply::NoError) { QJsonDocument profileJSON = QJsonDocument::fromJson(networkReply->readAll()); - + if (profileJSON.object()["status"].toString() == "success") { // pull the user roles from the response QJsonArray userRolesArray = profileJSON.object()["data"].toObject()["user"].toObject()["roles"].toArray(); - + QJsonArray allowedRolesArray = _argumentVariantMap.value(ALLOWED_ROLES_CONFIG_KEY).toJsonValue().toArray(); - + bool shouldAllowUserToConnect = false; - + foreach(const QJsonValue& roleValue, userRolesArray) { if (allowedRolesArray.contains(roleValue)) { // the user has a role that lets them in @@ -1263,10 +1264,10 @@ void DomainServer::handleProfileRequestFinished() { break; } } - + qDebug() << "Confirmed authentication state for user" << uuidStringWithoutCurlyBraces(matchingSessionUUID) << "-" << shouldAllowUserToConnect; - + // insert this UUID and a flag that indicates if they are allowed to connect _sessionAuthenticationHash.insert(matchingSessionUUID, shouldAllowUserToConnect); } @@ -1276,15 +1277,15 @@ void DomainServer::handleProfileRequestFinished() { void DomainServer::refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer& assignment) { QUuid oldUUID = assignment->getUUID(); assignment->resetUUID(); - + qDebug() << "Reset UUID for assignment -" << *assignment.data() << "- and added to queue. Old UUID was" << uuidStringWithoutCurlyBraces(oldUUID); - + if (assignment->getType() == Assignment::AgentType && assignment->getPayload().isEmpty()) { // if this was an Agent without a script URL, we need to rename the old file so it can be retrieved at the new UUID QFile::rename(pathForAssignmentScript(oldUUID), pathForAssignmentScript(assignment->getUUID())); } - + // add the static assignment back under the right UUID, and to the queue _allAssignments.insert(assignment->getUUID(), assignment); _unfulfilledAssignments.enqueue(assignment); @@ -1296,19 +1297,19 @@ void DomainServer::nodeAdded(SharedNodePointer node) { } void DomainServer::nodeKilled(SharedNodePointer node) { - + DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); - + if (nodeData) { // if this node's UUID matches a static assignment we need to throw it back in the assignment queue if (!nodeData->getAssignmentUUID().isNull()) { SharedAssignmentPointer matchedAssignment = _allAssignments.take(nodeData->getAssignmentUUID()); - + if (matchedAssignment && matchedAssignment->isStatic()) { refreshStaticAssignmentAndAddToQueue(matchedAssignment); } } - + // cleanup the connection secrets that we set up for this node (on the other nodes) foreach (const QUuid& otherNodeSessionUUID, nodeData->getSessionSecretHash().keys()) { SharedNodePointer otherNode = LimitedNodeList::getInstance()->nodeWithUUID(otherNodeSessionUUID); @@ -1321,19 +1322,19 @@ void DomainServer::nodeKilled(SharedNodePointer node) { SharedAssignmentPointer DomainServer::matchingQueuedAssignmentForCheckIn(const QUuid& assignmentUUID, NodeType_t nodeType) { QQueue::iterator i = _unfulfilledAssignments.begin(); - + while (i != _unfulfilledAssignments.end()) { if (i->data()->getType() == Assignment::typeForNodeType(nodeType) && i->data()->getUUID() == assignmentUUID) { // we have an unfulfilled assignment to return - + // return the matching assignment return _unfulfilledAssignments.takeAt(i - _unfulfilledAssignments.begin()); } else { ++i; } } - + return SharedAssignmentPointer(); } @@ -1348,17 +1349,17 @@ SharedAssignmentPointer DomainServer::deployableAssignmentForRequest(const Assig bool assignmentTypesMatch = assignment->getType() == requestAssignment.getType(); bool nietherHasPool = assignment->getPool().isEmpty() && requestAssignment.getPool().isEmpty(); bool assignmentPoolsMatch = assignment->getPool() == requestAssignment.getPool(); - + if ((requestIsAllTypes || assignmentTypesMatch) && (nietherHasPool || assignmentPoolsMatch)) { - + // remove the assignment from the queue SharedAssignmentPointer deployableAssignment = _unfulfilledAssignments.takeAt(sharedAssignment - _unfulfilledAssignments.begin()); - + // until we get a connection for this assignment // put assignment back in queue but stick it at the back so the others have a chance to go out _unfulfilledAssignments.enqueue(deployableAssignment); - + // stop looping, we've handed out an assignment return deployableAssignment; } else { @@ -1366,7 +1367,7 @@ SharedAssignmentPointer DomainServer::deployableAssignmentForRequest(const Assig ++sharedAssignment; } } - + return SharedAssignmentPointer(); } @@ -1375,7 +1376,7 @@ void DomainServer::removeMatchingAssignmentFromQueue(const SharedAssignmentPoint while (potentialMatchingAssignment != _unfulfilledAssignments.end()) { if (potentialMatchingAssignment->data()->getUUID() == removableAssignment->getUUID()) { _unfulfilledAssignments.erase(potentialMatchingAssignment); - + // we matched and removed an assignment, bail out break; } else { @@ -1393,19 +1394,19 @@ void DomainServer::addStaticAssignmentsToQueue() { while (staticAssignment != staticHashCopy.end()) { // add any of the un-matched static assignments to the queue bool foundMatchingAssignment = false; - + // enumerate the nodes and check if there is one with an attached assignment with matching UUID foreach (const SharedNodePointer& node, LimitedNodeList::getInstance()->getNodeHash()) { if (node->getUUID() == staticAssignment->data()->getUUID()) { foundMatchingAssignment = true; } } - + if (!foundMatchingAssignment) { // this assignment has not been fulfilled - reset the UUID and add it to the assignment queue refreshStaticAssignmentAndAddToQueue(*staticAssignment); } - + ++staticAssignment; } } diff --git a/examples/editModels.js b/examples/editModels.js index 93a34b9a3a..3cd727570c 100644 --- a/examples/editModels.js +++ b/examples/editModels.js @@ -5,6 +5,19 @@ // Created by Clément Brisset on 4/24/14. // Copyright 2014 High Fidelity, Inc. // +// This script allows you to edit models either with the razor hydras or with your mouse +// +// If using the hydras : +// grab grab models with the triggers, you can then move the models around or scale them with both hands. +// You can switch mode using the bumpers so that you can move models roud more easily. +// +// If using the mouse : +// - left click lets you move the model in the plane facing you. +// If pressing shift, it will move on the horizontale plane it's in. +// - right click lets you rotate the model. z and x give you access to more axix of rotation while shift allows for finer control. +// - left + right click lets you scale the model. +// - you can press r while holding the model to reset its rotation +// // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // @@ -653,16 +666,18 @@ function initToolBar() { } function moveOverlays() { + var newViewPort = Controller.getViewportDimensions(); + if (typeof(toolBar) === 'undefined') { initToolBar(); - } else if (windowDimensions.x == Controller.getViewportDimensions().x && - windowDimensions.y == Controller.getViewportDimensions().y) { + } else if (windowDimensions.x == newViewPort.x && + windowDimensions.y == newViewPort.y) { return; } - windowDimensions = Controller.getViewportDimensions(); + windowDimensions = newViewPort; var toolsX = windowDimensions.x - 8 - toolBar.width; var toolsY = (windowDimensions.y - toolBar.height) / 2; @@ -680,8 +695,6 @@ var intersection; var SCALE_FACTOR = 200.0; -var TRANSLATION_FACTOR = 100.0; -var ROTATION_FACTOR = 100.0; function rayPlaneIntersection(pickRay, point, normal) { var d = -Vec3.dot(point, normal); @@ -690,7 +703,53 @@ function rayPlaneIntersection(pickRay, point, normal) { return Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction, t)); } +function Tooltip() { + this.x = 285; + this.y = 115; + this.width = 110; + this.height = 115 ; + this.margin = 5; + this.decimals = 3; + + this.textOverlay = Overlays.addOverlay("text", { + x: this.x, + y: this.y, + width: this.width, + height: this.height, + margin: this.margin, + text: "", + color: { red: 128, green: 128, blue: 128 }, + alpha: 0.2, + visible: false + }); + this.show = function(doShow) { + Overlays.editOverlay(this.textOverlay, { visible: doShow }); + } + this.updateText = function(properties) { + var angles = Quat.safeEulerAngles(properties.modelRotation); + var text = "Model Properties:\n" + text += "x: " + properties.position.x.toFixed(this.decimals) + "\n" + text += "y: " + properties.position.y.toFixed(this.decimals) + "\n" + text += "z: " + properties.position.z.toFixed(this.decimals) + "\n" + text += "pitch: " + angles.x.toFixed(this.decimals) + "\n" + text += "yaw: " + angles.y.toFixed(this.decimals) + "\n" + text += "roll: " + angles.z.toFixed(this.decimals) + "\n" + text += "Scale: " + 2 * properties.radius.toFixed(this.decimals) + "\n" + + Overlays.editOverlay(this.textOverlay, { text: text }); + } + + this.cleanup = function() { + Overlays.deleteOverlay(this.textOverlay); + } +} +var tooltip = new Tooltip(); + function mousePressEvent(event) { + if (event.isAlt) { + return; + } + mouseLastPosition = { x: event.x, y: event.y }; modelSelected = false; var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); @@ -782,6 +841,8 @@ function mousePressEvent(event) { selectedModelProperties.glowLevel = 0.0; print("Clicked on " + selectedModelID.id + " " + modelSelected); + tooltip.updateText(selectedModelProperties); + tooltip.show(true); } } @@ -790,6 +851,10 @@ var oldModifier = 0; var modifier = 0; var wasShifted = false; function mouseMoveEvent(event) { + if (event.isAlt) { + return; + } + var pickRay = Camera.computePickRay(event.x, event.y); if (!modelSelected) { @@ -878,22 +943,54 @@ function mouseMoveEvent(event) { break; case 3: // Let's rotate - var rotation = Quat.fromVec3Degrees({ x: event.y - mouseLastPosition.y, y: event.x - mouseLastPosition.x, z: 0 }); - if (event.isShifted) { - rotation = Quat.fromVec3Degrees({ x: event.y - mouseLastPosition.y, y: 0, z: mouseLastPosition.x - event.x }); + if (somethingChanged) { + selectedModelProperties.oldRotation.x = selectedModelProperties.modelRotation.x; + selectedModelProperties.oldRotation.y = selectedModelProperties.modelRotation.y; + selectedModelProperties.oldRotation.z = selectedModelProperties.modelRotation.z; + selectedModelProperties.oldRotation.w = selectedModelProperties.modelRotation.w; + mouseLastPosition.x = event.x; + mouseLastPosition.y = event.y; + somethingChanged = false; } - var newRotation = Quat.multiply(orientation, rotation); - newRotation = Quat.multiply(newRotation, Quat.inverse(orientation)); - selectedModelProperties.modelRotation = Quat.multiply(newRotation, selectedModelProperties.oldRotation); + var pixelPerDegrees = windowDimensions.y / (1 * 360); // the entire height of the window allow you to make 2 full rotations + + var STEP = 15; + var delta = Math.floor((event.x - mouseLastPosition.x) / pixelPerDegrees); + + if (!event.isShifted) { + delta = Math.floor(delta / STEP) * STEP; + } + + var rotation = Quat.fromVec3Degrees({ + x: (!zIsPressed && xIsPressed) ? delta : 0, // z is pressed + y: (!zIsPressed && !xIsPressed) ? delta : 0, // x is pressed + z: (zIsPressed && !xIsPressed) ? delta : 0 // neither is pressed + }); + rotation = Quat.multiply(selectedModelProperties.oldRotation, rotation); + + selectedModelProperties.modelRotation.x = rotation.x; + selectedModelProperties.modelRotation.y = rotation.y; + selectedModelProperties.modelRotation.z = rotation.z; + selectedModelProperties.modelRotation.w = rotation.w; break; } Models.editModel(selectedModelID, selectedModelProperties); + tooltip.updateText(selectedModelProperties); } + function mouseReleaseEvent(event) { + if (event.isAlt) { + return; + } + + if (modelSelected) { + tooltip.show(false); + } + modelSelected = false; glowedModelID.id = -1; @@ -931,6 +1028,7 @@ function scriptEnding() { rightController.cleanup(); toolBar.cleanup(); cleanupModelMenus(); + tooltip.cleanup(); } Script.scriptEnding.connect(scriptEnding); @@ -963,3 +1061,35 @@ Menu.menuItemEvent.connect(function(menuItem){ }); +// handling of inspect.js concurrence +var zIsPressed = false; +var xIsPressed = false; +var somethingChanged = false; +Controller.keyPressEvent.connect(function(event) { + if ((event.text == "z" || event.text == "Z") && !zIsPressed) { + zIsPressed = true; + somethingChanged = true; + } + if ((event.text == "x" || event.text == "X") && !xIsPressed) { + xIsPressed = true; + somethingChanged = true; + } + + // resets model orientation when holding with mouse + if (event.text == "r" && modelSelected) { + selectedModelProperties.modelRotation = Quat.fromVec3Degrees({ x: 0, y: 0, z: 0 }); + Models.editModel(selectedModelID, selectedModelProperties); + tooltip.updateText(selectedModelProperties); + somethingChanged = true; + } +}); +Controller.keyReleaseEvent.connect(function(event) { + if (event.text == "z" || event.text == "Z") { + zIsPressed = false; + somethingChanged = true; + } + if (event.text == "x" || event.text == "X") { + xIsPressed = false; + somethingChanged = true; + } +}); \ No newline at end of file diff --git a/examples/hydraMove.js b/examples/hydraMove.js index f211a450a3..675a885b6d 100644 --- a/examples/hydraMove.js +++ b/examples/hydraMove.js @@ -19,8 +19,8 @@ var damping = 0.9; var position = { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z }; var joysticksCaptured = false; -var THRUST_CONTROLLER = 1; -var VIEW_CONTROLLER = 0; +var THRUST_CONTROLLER = 0; +var VIEW_CONTROLLER = 1; var INITIAL_THRUST_MULTPLIER = 1.0; var THRUST_INCREASE_RATE = 1.05; var MAX_THRUST_MULTIPLIER = 75.0; diff --git a/examples/inspect.js b/examples/inspect.js index 28db1e7735..b292d5f609 100644 --- a/examples/inspect.js +++ b/examples/inspect.js @@ -34,6 +34,7 @@ var noMode = 0; var orbitMode = 1; var radialMode = 2; var panningMode = 3; +var detachedMode = 4; var mode = noMode; @@ -48,6 +49,9 @@ var radius = 0.0; var azimuth = 0.0; var altitude = 0.0; +var avatarPosition; +var avatarOrientation; + function handleRadialMode(dx, dy) { azimuth += dx / AZIMUTH_RATE; @@ -108,7 +112,7 @@ function restoreCameraState() { } function handleModes() { - var newMode = noMode; + var newMode = (mode == noMode) ? noMode : detachedMode; if (alt) { if (control) { if (shift) { @@ -121,6 +125,22 @@ function handleModes() { } } + // if entering detachMode + if (newMode == detachedMode && mode != detachedMode) { + avatarPosition = MyAvatar.position; + avatarOrientation = MyAvatar.orientation; + } + // if leaving detachMode + if (mode == detachedMode && newMode == detachedMode && + (avatarPosition.x != MyAvatar.position.x || + avatarPosition.y != MyAvatar.position.y || + avatarPosition.z != MyAvatar.position.z || + avatarOrientation.x != MyAvatar.orientation.x || + avatarOrientation.y != MyAvatar.orientation.y || + avatarOrientation.z != MyAvatar.orientation.z || + avatarOrientation.w != MyAvatar.orientation.w)) { + newMode = noMode; + } // if leaving noMode if (mode == noMode && newMode != noMode) { saveCameraState(); @@ -177,30 +197,45 @@ function keyReleaseEvent(event) { function mousePressEvent(event) { if (alt && !isActive) { - isActive = true; mouseLastX = event.x; mouseLastY = event.y; // Compute trajectories related values var pickRay = Camera.computePickRay(mouseLastX, mouseLastY); - var intersection = Voxels.findRayIntersection(pickRay); + var voxelIntersection = Voxels.findRayIntersection(pickRay); + var modelIntersection = Models.findRayIntersection(pickRay); position = Camera.getPosition(); - avatarTarget = MyAvatar.getTargetAvatarPosition(); - voxelTarget = intersection.intersection; - if (Vec3.length(Vec3.subtract(avatarTarget, position)) < Vec3.length(Vec3.subtract(voxelTarget, position))) { - if (avatarTarget.x != 0 || avatarTarget.y != 0 || avatarTarget.z != 0) { - center = avatarTarget; - } else { - center = voxelTarget; - } - } else { - if (voxelTarget.x != 0 || voxelTarget.y != 0 || voxelTarget.z != 0) { - center = voxelTarget; - } else { - center = avatarTarget; - } + var avatarTarget = MyAvatar.getTargetAvatarPosition(); + var voxelTarget = voxelIntersection.intersection; + + + var distance = -1; + var string; + + if (modelIntersection.intersects && modelIntersection.accurate) { + distance = modelIntersection.distance; + center = modelIntersection.modelProperties.position; + string = "Inspecting model"; + } + + if ((distance == -1 || Vec3.length(Vec3.subtract(avatarTarget, position)) < distance) && + (avatarTarget.x != 0 || avatarTarget.y != 0 || avatarTarget.z != 0)) { + distance = Vec3.length(Vec3.subtract(avatarTarget, position)); + center = avatarTarget; + string = "Inspecting avatar"; + } + + if ((distance == -1 || Vec3.length(Vec3.subtract(voxelTarget, position)) < distance) && + (voxelTarget.x != 0 || voxelTarget.y != 0 || voxelTarget.z != 0)) { + distance = Vec3.length(Vec3.subtract(voxelTarget, position)); + center = voxelTarget; + string = "Inspecting voxel"; + } + + if (distance == -1) { + return; } vector = Vec3.subtract(position, center); @@ -209,6 +244,8 @@ function mousePressEvent(event) { altitude = Math.asin(vector.y / Vec3.length(vector)); Camera.keepLookingAt(center); + print(string); + isActive = true; } } @@ -235,6 +272,10 @@ function mouseMoveEvent(event) { } } +function update() { + handleModes(); +} + function scriptEnding() { if (mode != noMode) { restoreCameraState(); @@ -248,4 +289,5 @@ Controller.mousePressEvent.connect(mousePressEvent); Controller.mouseReleaseEvent.connect(mouseReleaseEvent); Controller.mouseMoveEvent.connect(mouseMoveEvent); +Script.update.connect(update); Script.scriptEnding.connect(scriptEnding); \ No newline at end of file diff --git a/examples/myBalance.js b/examples/myBalance.js new file mode 100644 index 0000000000..bd48e8fd21 --- /dev/null +++ b/examples/myBalance.js @@ -0,0 +1,119 @@ +// +// myBalance.js +// examples +// +// Created by Stojce Slavkovski on June 5, 2014 +// Copyright 2014 High Fidelity, Inc. +// +// Show wallet ₵ balance +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var Controller = Controller || {}; +var Overlays = Overlays || {}; +var Script = Script || {}; +var Account = Account || {}; + +(function () { + "use strict"; + var iconUrl = 'http://highfidelity-public.s3-us-west-1.amazonaws.com/images/tools/', + overlayWidth = 150, + overlayHeight = 50, + overlayTopOffset = 15, + overlayRightOffset = 140, + textRightOffset = 105, + maxIntegers = 5, + downColor = { + red: 0, + green: 0, + blue: 255 + }, + upColor = { + red: 0, + green: 255, + blue: 0 + }, + normalColor = { + red: 204, + green: 204, + blue: 204 + }, + balance = -1, + walletBox = Overlays.addOverlay("image", { + x: 0, + y: overlayTopOffset, + width: 122, + height: 32, + imageURL: iconUrl + "wallet.svg", + alpha: 1 + }), + textOverlay = Overlays.addOverlay("text", { + x: 0, + y: overlayTopOffset, + topMargin: 10, + font: { + size: 16 + }, + color: normalColor + }); + + function scriptEnding() { + Overlays.deleteOverlay(walletBox); + Overlays.deleteOverlay(textOverlay); + } + + function update(deltaTime) { + var xPos = Controller.getViewportDimensions().x; + Overlays.editOverlay(walletBox, { + x: xPos - overlayRightOffset, + visible: Account.isLoggedIn() + }); + + Overlays.editOverlay(textOverlay, { + x: xPos - textRightOffset, + visible: Account.isLoggedIn() + }); + } + + function formatedBalance() { + var integers = balance.toFixed(0).length, + decimals = Math.abs(maxIntegers - integers) + 2; + + var x = balance.toFixed(decimals).split('.'), + x1 = x[0], + x2 = x.length > 1 ? '.' + x[1] : ''; + var rgx = /(\d+)(\d{3})/; + while (rgx.test(x1)) { + x1 = x1.replace(rgx, '$1' + ',' + '$2'); + } + return x1 + x2; + } + + function updateBalance(newBalance) { + if (balance === newBalance) { + return; + } + + var change = newBalance - balance, + textColor = change < 0 ? downColor : upColor; + + balance = newBalance; + Overlays.editOverlay(textOverlay, { + text: formatedBalance(), + color: textColor + }); + + Script.setTimeout(function () { + Overlays.editOverlay(textOverlay, { + color: normalColor + }); + }, 1000); + } + + updateBalance(Account.getBalance()); + Account.balanceChanged.connect(updateBalance); + Script.scriptEnding.connect(scriptEnding); + Script.update.connect(update); +}()); \ No newline at end of file diff --git a/interface/resources/images/sixense-reticle.png b/interface/resources/images/sixense-reticle.png new file mode 100644 index 0000000000..b168b25106 Binary files /dev/null and b/interface/resources/images/sixense-reticle.png differ diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 24fcb3dc70..5507089798 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -73,6 +73,7 @@ #include "devices/TV3DManager.h" #include "renderer/ProgramObject.h" +#include "scripting/AccountScriptingInterface.h" #include "scripting/AudioDeviceScriptingInterface.h" #include "scripting/ClipboardScriptingInterface.h" #include "scripting/MenuScriptingInterface.h" @@ -150,6 +151,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _mouseX(0), _mouseY(0), _lastMouseMove(usecTimestampNow()), + _lastMouseMoveType(QEvent::MouseMove), _mouseHidden(false), _seenMouseMove(false), _touchAvgX(0.0f), @@ -653,7 +655,14 @@ void Application::paintGL() { { PerformanceTimer perfTimer("paintGL/renderOverlay"); - _applicationOverlay.renderOverlay(); + //If alpha is 1, we can render directly to the screen. + if (_applicationOverlay.getAlpha() == 1.0f) { + _applicationOverlay.renderOverlay(); + } else { + //Render to to texture so we can fade it + _applicationOverlay.renderOverlay(true); + _applicationOverlay.displayOverlayTexture(); + } } } @@ -1096,6 +1105,9 @@ void Application::mouseMoveEvent(QMouseEvent* event) { showMouse = false; } + // Used by application overlay to determine how to draw cursor(s) + _lastMouseMoveType = event->type(); + _controllerScriptingInterface.emitMouseMoveEvent(event); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it @@ -1375,6 +1387,9 @@ void Application::setEnable3DTVMode(bool enable3DTVMode) { resizeGL(_glWidget->width(),_glWidget->height()); } +void Application::setEnableVRMode(bool enableVRMode) { + resizeGL(_glWidget->width(), _glWidget->height()); +} void Application::setRenderVoxels(bool voxelRender) { _voxelEditSender.setShouldSend(voxelRender); @@ -3516,12 +3531,13 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript } else { // start the script on a new thread... scriptEngine = new ScriptEngine(scriptUrl, &_controllerScriptingInterface); - _scriptEnginesHash.insert(scriptURLString, scriptEngine); if (!scriptEngine->hasScript()) { qDebug() << "Application::loadScript(), script failed to load..."; return NULL; } + + _scriptEnginesHash.insert(scriptURLString, scriptEngine); _runningScriptsWidget->setRunningScripts(getRunningScripts()); } @@ -3564,6 +3580,7 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript scriptEngine->registerGlobalObject("AudioDevice", AudioDeviceScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("AnimationCache", &_animationCache); scriptEngine->registerGlobalObject("AudioReflector", &_audioReflector); + scriptEngine->registerGlobalObject("Account", AccountScriptingInterface::getInstance()); QThread* workerThread = new QThread(this); diff --git a/interface/src/Application.h b/interface/src/Application.h index 2889dcb301..31e1f2291b 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -206,6 +206,7 @@ public: const glm::vec3& getMouseRayDirection() const { return _mouseRayDirection; } int getMouseX() const { return _mouseX; } int getMouseY() const { return _mouseY; } + unsigned int getLastMouseMoveType() const { return _lastMouseMoveType; } Faceplus* getFaceplus() { return &_faceplus; } Faceshift* getFaceshift() { return &_faceshift; } Visage* getVisage() { return &_visage; } @@ -345,6 +346,7 @@ private slots: void setFullscreen(bool fullscreen); void setEnable3DTVMode(bool enable3DTVMode); + void setEnableVRMode(bool enableVRMode); void cameraMenuChanged(); glm::vec2 getScaledScreenPoint(glm::vec2 projectedPoint); @@ -505,6 +507,7 @@ private: int _mouseDragStartedX; int _mouseDragStartedY; quint64 _lastMouseMove; + unsigned int _lastMouseMoveType; bool _mouseHidden; bool _seenMouseMove; @@ -521,6 +524,7 @@ private: QSet _keysPressed; + GeometryCache _geometryCache; AnimationCache _animationCache; TextureCache _textureCache; diff --git a/interface/src/Audio.cpp b/interface/src/Audio.cpp index 2cbe3a062f..271bcd5279 100644 --- a/interface/src/Audio.cpp +++ b/interface/src/Audio.cpp @@ -1419,7 +1419,7 @@ bool Audio::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) // proportional to the accelerator ratio. #ifdef Q_OS_WIN -const float Audio::CALLBACK_ACCELERATOR_RATIO = 0.4f; +const float Audio::CALLBACK_ACCELERATOR_RATIO = 0.1f; #endif #ifdef Q_OS_MAC diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 6cf99418e3..5c8c2e97aa 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -45,6 +45,7 @@ #include "ui/ModelsBrowser.h" #include "ui/LoginDialog.h" #include "ui/NodeBounds.h" +#include "devices/OculusManager.h" Menu* Menu::_instance = NULL; @@ -83,6 +84,7 @@ Menu::Menu() : _audioJitterBufferSamples(0), _bandwidthDialog(NULL), _fieldOfView(DEFAULT_FIELD_OF_VIEW_DEGREES), + _realWorldFieldOfView(DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), _faceshiftEyeDeflection(DEFAULT_FACESHIFT_EYE_DEFLECTION), _frustumDrawMode(FRUSTUM_DRAW_MODE_ALL), _viewFrustumOffset(DEFAULT_FRUSTUM_OFFSET), @@ -91,6 +93,9 @@ Menu::Menu() : _lodToolsDialog(NULL), _maxVoxels(DEFAULT_MAX_VOXELS_PER_SYSTEM), _voxelSizeScale(DEFAULT_OCTREE_SIZE_SCALE), + _oculusUIAngularSize(DEFAULT_OCULUS_UI_ANGULAR_SIZE), + _sixenseReticleMoveSpeed(DEFAULT_SIXENSE_RETICLE_MOVE_SPEED), + _invertSixenseButtons(DEFAULT_INVERT_SIXENSE_MOUSE_BUTTONS), _automaticAvatarLOD(true), _avatarLODDecreaseFPS(DEFAULT_ADJUST_AVATAR_LOD_DOWN_FPS), _avatarLODIncreaseFPS(ADJUST_LOD_UP_FPS), @@ -127,7 +132,7 @@ Menu::Menu() : toggleLoginMenuItem(); // connect to the appropriate slots of the AccountManager so that we can change the Login/Logout menu item - connect(&accountManager, &AccountManager::accessTokenChanged, this, &Menu::toggleLoginMenuItem); + connect(&accountManager, &AccountManager::profileChanged, this, &Menu::toggleLoginMenuItem); connect(&accountManager, &AccountManager::logoutComplete, this, &Menu::toggleLoginMenuItem); addDisabledActionAndSeparator(fileMenu, "Scripts"); @@ -165,6 +170,8 @@ Menu::Menu() : Qt::Key_At, this, SLOT(goTo())); + connect(&LocationManager::getInstance(), &LocationManager::multipleDestinationsFound, + this, &Menu::multipleDestinationsDecision); addDisabledActionAndSeparator(fileMenu, "Upload Avatar Model"); addActionToQMenuAndActionHash(fileMenu, MenuOption::UploadHead, 0, Application::getInstance(), SLOT(uploadHead())); @@ -254,6 +261,11 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::FullscreenMirror, Qt::Key_H, false, appInstance, SLOT(cameraMenuChanged())); + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::EnableVRMode, 0, + false, + appInstance, + SLOT(setEnableVRMode(bool))); + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Enable3DTVMode, 0, false, appInstance, @@ -321,7 +333,7 @@ Menu::Menu() : shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, "None", 0, true)); shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, MenuOption::SimpleShadows, 0, false)); shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, MenuOption::CascadedShadows, 0, false)); - + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Metavoxels, 0, true); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::BuckyBalls, 0, false); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Particles, 0, true); @@ -382,8 +394,6 @@ Menu::Menu() : QMenu* sixenseOptionsMenu = developerMenu->addMenu("Sixense Options"); addCheckableActionToQMenuAndActionHash(sixenseOptionsMenu, MenuOption::SixenseMouseInput, 0, true); - addCheckableActionToQMenuAndActionHash(sixenseOptionsMenu, MenuOption::SixenseLeftHanded, 0, false); - addCheckableActionToQMenuAndActionHash(sixenseOptionsMenu, MenuOption::SixenseInvertInputButtons, 0, false); QMenu* handOptionsMenu = developerMenu->addMenu("Hand Options"); @@ -403,7 +413,7 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::DisableNackPackets, 0, false); addDisabledActionAndSeparator(developerMenu, "Testing"); - + QMenu* timingMenu = developerMenu->addMenu("Timing and Statistics Tools"); QMenu* perfTimerMenu = timingMenu->addMenu("Performance Timer"); addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::DisplayTimingDetails, 0, true); @@ -458,7 +468,7 @@ Menu::Menu() : false, appInstance->getAudio(), SLOT(toggleToneInjection())); - addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioScope, + addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioScope, Qt::CTRL | Qt::Key_P, false, appInstance->getAudio(), SLOT(toggleScope())); @@ -1068,9 +1078,7 @@ bool Menu::goToURL(QString location) { } void Menu::goToUser(const QString& user) { - LocationManager* manager = &LocationManager::getInstance(); - manager->goTo(user); - connect(manager, &LocationManager::multipleDestinationsFound, this, &Menu::multipleDestinationsDecision); + LocationManager::getInstance().goTo(user); } /// Open a url, shortcutting any "hifi" scheme URLs to the local application. @@ -1092,13 +1100,10 @@ void Menu::multipleDestinationsDecision(const QJsonObject& userData, const QJson int userResponse = msgBox.exec(); if (userResponse == QMessageBox::Ok) { - Application::getInstance()->getAvatar()->goToLocationFromResponse(userData); + Application::getInstance()->getAvatar()->goToLocationFromAddress(userData["address"].toObject()); } else if (userResponse == QMessageBox::Open) { - Application::getInstance()->getAvatar()->goToLocationFromResponse(userData); + Application::getInstance()->getAvatar()->goToLocationFromAddress(placeData["address"].toObject()); } - - LocationManager* manager = reinterpret_cast(sender()); - disconnect(manager, &LocationManager::multipleDestinationsFound, this, &Menu::multipleDestinationsDecision); } void Menu::muteEnvironment() { @@ -1771,4 +1776,3 @@ QString Menu::getSnapshotsLocation() const { } return _snapshotsLocation; } - diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 8415fb68c4..4d2174a448 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -90,6 +90,12 @@ public: void setFieldOfView(float fieldOfView) { _fieldOfView = fieldOfView; } float getRealWorldFieldOfView() const { return _realWorldFieldOfView; } void setRealWorldFieldOfView(float realWorldFieldOfView) { _realWorldFieldOfView = realWorldFieldOfView; } + float getOculusUIAngularSize() const { return _oculusUIAngularSize; } + void setOculusUIAngularSize(float oculusUIAngularSize) { _oculusUIAngularSize = oculusUIAngularSize; } + float getSixenseReticleMoveSpeed() const { return _sixenseReticleMoveSpeed; } + void setSixenseReticleMoveSpeed(float sixenseReticleMoveSpeed) { _sixenseReticleMoveSpeed = sixenseReticleMoveSpeed; } + bool getInvertSixenseButtons() const { return _invertSixenseButtons; } + void setInvertSixenseButtons(bool invertSixenseButtons) { _invertSixenseButtons = invertSixenseButtons; } float getFaceshiftEyeDeflection() const { return _faceshiftEyeDeflection; } void setFaceshiftEyeDeflection(float faceshiftEyeDeflection) { _faceshiftEyeDeflection = faceshiftEyeDeflection; } @@ -255,6 +261,9 @@ private: LodToolsDialog* _lodToolsDialog; int _maxVoxels; float _voxelSizeScale; + float _oculusUIAngularSize; + float _sixenseReticleMoveSpeed; + bool _invertSixenseButtons; bool _automaticAvatarLOD; float _avatarLODDecreaseFPS; float _avatarLODIncreaseFPS; @@ -336,6 +345,7 @@ namespace MenuOption { const QString EchoLocalAudio = "Echo Local Audio"; const QString EchoServerAudio = "Echo Server Audio"; const QString Enable3DTVMode = "Enable 3DTV Mode"; + const QString EnableVRMode = "Enable VR Mode"; const QString ExpandMiscAvatarTiming = "Expand Misc MyAvatar Timing"; const QString ExpandAvatarUpdateTiming = "Expand MyAvatar update Timing"; const QString ExpandAvatarSimulateTiming = "Expand MyAvatar simulate Timing"; @@ -399,8 +409,6 @@ namespace MenuOption { const QString SettingsExport = "Export Settings"; const QString SettingsImport = "Import Settings"; const QString SimpleShadows = "Simple"; - const QString SixenseInvertInputButtons = "Invert Sixense Mouse Input Buttons"; - const QString SixenseLeftHanded = "Left Handed Sixense Mouse Input"; const QString SixenseMouseInput = "Enable Sixense Mouse Input"; const QString ShowBordersVoxelNodes = "Show Voxel Nodes"; const QString ShowBordersModelNodes = "Show Model Nodes"; diff --git a/interface/src/Util.cpp b/interface/src/Util.cpp index 07ca65b286..5ce1435bd6 100644 --- a/interface/src/Util.cpp +++ b/interface/src/Util.cpp @@ -21,6 +21,8 @@ #include +#include + #include "InterfaceConfig.h" #include "ui/TextRenderer.h" #include "VoxelConstants.h" @@ -409,8 +411,44 @@ void runTimingTests() { float NSEC_TO_USEC = 1.0f / 1000.0f; elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; - qDebug("QElapsedTimer::nsecElapsed() usecs: %f", elapsedUsecs / (float) numTests); + qDebug("QElapsedTimer::nsecElapsed() usecs: %f", elapsedUsecs); + // Test sleep functions for accuracy + startTime.start(); + QThread::msleep(1); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("QThread::msleep(1) ms: %f", elapsedUsecs / 1000.0f); + + startTime.start(); + QThread::sleep(1); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("QThread::sleep(1) ms: %f", elapsedUsecs / 1000.0f); + + startTime.start(); + usleep(1); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("usleep(1) ms: %f", elapsedUsecs / 1000.0f); + + startTime.start(); + usleep(10); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("usleep(10) ms: %f", elapsedUsecs / 1000.0f); + + startTime.start(); + usleep(100); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("usleep(100) ms: %f", elapsedUsecs / 1000.0f); + + startTime.start(); + usleep(1000); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("usleep(1000) ms: %f", elapsedUsecs / 1000.0f); + + startTime.start(); + usleep(15000); + elapsedUsecs = (float)startTime.nsecsElapsed() * NSEC_TO_USEC; + qDebug("usleep(15000) ms: %f", elapsedUsecs / 1000.0f); + // Random number generation startTime.start(); for (int i = 0; i < numTests; i++) { diff --git a/interface/src/XmppClient.cpp b/interface/src/XmppClient.cpp index 666906681c..ef9db55620 100644 --- a/interface/src/XmppClient.cpp +++ b/interface/src/XmppClient.cpp @@ -23,7 +23,7 @@ XmppClient::XmppClient() : _xmppMUCManager() { AccountManager& accountManager = AccountManager::getInstance(); - connect(&accountManager, SIGNAL(accessTokenChanged()), this, SLOT(connectToServer())); + connect(&accountManager, SIGNAL(profileChanged()), this, SLOT(connectToServer())); connect(&accountManager, SIGNAL(logoutComplete()), this, SLOT(disconnectFromServer())); } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index a14152aa04..54ed641d72 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1626,44 +1626,46 @@ void MyAvatar::resetSize() { } void MyAvatar::goToLocationFromResponse(const QJsonObject& jsonObject) { - if (jsonObject["status"].toString() == "success") { - - // send a node kill request, indicating to other clients that they should play the "disappeared" effect - sendKillAvatar(); - QJsonObject locationObject = jsonObject["data"].toObject()["address"].toObject(); - QString positionString = locationObject["position"].toString(); - QString orientationString = locationObject["orientation"].toString(); - QString domainHostnameString = locationObject["domain"].toString(); - - qDebug() << "Changing domain to" << domainHostnameString << - ", position to" << positionString << - ", and orientation to" << orientationString; - - QStringList coordinateItems = positionString.split(','); - QStringList orientationItems = orientationString.split(','); - - NodeList::getInstance()->getDomainHandler().setHostname(domainHostnameString); - - // orient the user to face the target - glm::quat newOrientation = glm::quat(glm::radians(glm::vec3(orientationItems[0].toFloat(), - orientationItems[1].toFloat(), - orientationItems[2].toFloat()))) - * glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); - setOrientation(newOrientation); - - // move the user a couple units away - const float DISTANCE_TO_USER = 2.0f; - glm::vec3 newPosition = glm::vec3(coordinateItems[0].toFloat(), coordinateItems[1].toFloat(), - coordinateItems[2].toFloat()) - newOrientation * IDENTITY_FRONT * DISTANCE_TO_USER; - setPosition(newPosition); - emit transformChanged(); + goToLocationFromAddress(locationObject); } else { QMessageBox::warning(Application::getInstance()->getWindow(), "", "That user or location could not be found."); } } +void MyAvatar::goToLocationFromAddress(const QJsonObject& locationObject) { + // send a node kill request, indicating to other clients that they should play the "disappeared" effect + sendKillAvatar(); + + QString positionString = locationObject["position"].toString(); + QString orientationString = locationObject["orientation"].toString(); + QString domainHostnameString = locationObject["domain"].toString(); + + qDebug() << "Changing domain to" << domainHostnameString << + ", position to" << positionString << + ", and orientation to" << orientationString; + + QStringList coordinateItems = positionString.split(','); + QStringList orientationItems = orientationString.split(','); + + NodeList::getInstance()->getDomainHandler().setHostname(domainHostnameString); + + // orient the user to face the target + glm::quat newOrientation = glm::quat(glm::radians(glm::vec3(orientationItems[0].toFloat(), + orientationItems[1].toFloat(), + orientationItems[2].toFloat()))) + * glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); + setOrientation(newOrientation); + + // move the user a couple units away + const float DISTANCE_TO_USER = 2.0f; + glm::vec3 newPosition = glm::vec3(coordinateItems[0].toFloat(), coordinateItems[1].toFloat(), + coordinateItems[2].toFloat()) - newOrientation * IDENTITY_FRONT * DISTANCE_TO_USER; + setPosition(newPosition); + emit transformChanged(); +} + void MyAvatar::updateMotionBehaviorsFromMenu() { Menu* menu = Menu::getInstance(); if (menu->isOptionChecked(MenuOption::ObeyEnvironmentalGravity)) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index d99102c356..2fbc488feb 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -129,6 +129,7 @@ public slots: void resetSize(); void goToLocationFromResponse(const QJsonObject& jsonObject); + void goToLocationFromAddress(const QJsonObject& jsonObject); // Set/Get update the thrust that will move the avatar around void addThrust(glm::vec3 newThrust) { _thrust += newThrust; }; diff --git a/interface/src/devices/OculusManager.cpp b/interface/src/devices/OculusManager.cpp index fe8cc37168..b2ee4e8c18 100644 --- a/interface/src/devices/OculusManager.cpp +++ b/interface/src/devices/OculusManager.cpp @@ -72,6 +72,14 @@ void OculusManager::connect() { #endif } +bool OculusManager::isConnected() { +#ifdef HAVE_LIBOVR + return _isConnected && Menu::getInstance()->isOptionChecked(MenuOption::EnableVRMode); +#else + return false; +#endif +} + void OculusManager::configureCamera(Camera& camera, int screenWidth, int screenHeight) { #ifdef HAVE_LIBOVR _stereoConfig.SetFullViewport(Viewport(0, 0, screenWidth, screenHeight)); diff --git a/interface/src/devices/OculusManager.h b/interface/src/devices/OculusManager.h index 6376df05ca..21b9d67f4d 100644 --- a/interface/src/devices/OculusManager.h +++ b/interface/src/devices/OculusManager.h @@ -20,6 +20,8 @@ #include "renderer/ProgramObject.h" +const float DEFAULT_OCULUS_UI_ANGULAR_SIZE = 72.0f; + class Camera; /// Handles interaction with the Oculus Rift. @@ -27,7 +29,7 @@ class OculusManager { public: static void connect(); - static bool isConnected() { return _isConnected; } + static bool isConnected(); static void configureCamera(Camera& camera, int screenWidth, int screenHeight); diff --git a/interface/src/devices/SixenseManager.cpp b/interface/src/devices/SixenseManager.cpp index abbb32ec5e..07536d0af8 100644 --- a/interface/src/devices/SixenseManager.cpp +++ b/interface/src/devices/SixenseManager.cpp @@ -39,10 +39,14 @@ SixenseManager::SixenseManager() { sixenseInit(); #endif - _triggerPressed = false; - _bumperPressed = false; - _oldX = -1; - _oldY = -1; + _triggerPressed[0] = false; + _bumperPressed[0] = false; + _oldX[0] = -1; + _oldY[0] = -1; + _triggerPressed[1] = false; + _bumperPressed[1] = false; + _oldX[1] = -1; + _oldY[1] = -1; } SixenseManager::~SixenseManager() { @@ -114,10 +118,7 @@ void SixenseManager::update(float deltaTime) { // Emulate the mouse so we can use scripts if (Menu::getInstance()->isOptionChecked(MenuOption::SixenseMouseInput)) { - // Check if we are on the correct palm - if ((Menu::getInstance()->isOptionChecked(MenuOption::SixenseLeftHanded) && numActiveControllers == 1) || numActiveControllers == 2) { - emulateMouse(palm); - } + emulateMouse(palm, numActiveControllers - 1); } // NOTE: Sixense API returns pos data in millimeters but we IMMEDIATELY convert to meters. @@ -184,6 +185,17 @@ void SixenseManager::update(float deltaTime) { #endif // HAVE_SIXENSE } +//Constants for getCursorPixelRangeMultiplier() +const float MIN_PIXEL_RANGE_MULT = 0.4f; +const float MAX_PIXEL_RANGE_MULT = 2.0f; +const float RANGE_MULT = (MAX_PIXEL_RANGE_MULT - MIN_PIXEL_RANGE_MULT) * 0.01; + +//Returns a multiplier to be applied to the cursor range for the controllers +float SixenseManager::getCursorPixelRangeMult() const { + //scales (0,100) to (MINIMUM_PIXEL_RANGE_MULT, MAXIMUM_PIXEL_RANGE_MULT) + return Menu::getInstance()->getSixenseReticleMoveSpeed() * RANGE_MULT + MIN_PIXEL_RANGE_MULT; +} + #ifdef HAVE_SIXENSE // the calibration sequence is: @@ -328,76 +340,118 @@ void SixenseManager::updateCalibration(const sixenseControllerData* controllers) } //Injecting mouse movements and clicks -void SixenseManager::emulateMouse(PalmData *palm) { - MyAvatar* avatar = Application::getInstance()->getAvatar(); +void SixenseManager::emulateMouse(PalmData* palm, int index) { + Application* application = Application::getInstance(); + MyAvatar* avatar = application->getAvatar(); + QGLWidget* widget = application->getGLWidget(); QPoint pos; // Get directon relative to avatar orientation glm::vec3 direction = glm::inverse(avatar->getOrientation()) * palm->getFingerDirection(); - // Get the angles, scaled between 0-1 - float xAngle = (atan2(direction.z, direction.x) + M_PI_2) + 0.5f; - float yAngle = 1.0f - ((atan2(direction.z, direction.y) + M_PI_2) + 0.5f); - - float cursorRange = Application::getInstance()->getGLWidget()->width(); - - pos.setX(cursorRange * xAngle); - pos.setY(cursorRange * yAngle); - - //If position has changed, emit a mouse move to the application - if (pos.x() != _oldX || pos.y() != _oldY) { - QMouseEvent mouseEvent(static_cast(CONTROLLER_MOVE_EVENT), pos, Qt::NoButton, Qt::NoButton, 0); - - Application::getInstance()->mouseMoveEvent(&mouseEvent); - } - _oldX = pos.x(); - _oldY = pos.y(); - Qt::MouseButton bumperButton; Qt::MouseButton triggerButton; - if (Menu::getInstance()->isOptionChecked(MenuOption::SixenseInvertInputButtons)) { + if (Menu::getInstance()->getInvertSixenseButtons()) { bumperButton = Qt::LeftButton; triggerButton = Qt::RightButton; } else { bumperButton = Qt::RightButton; triggerButton = Qt::LeftButton; } + + // Get the angles, scaled between (-0.5,0.5) + float xAngle = (atan2(direction.z, direction.x) + M_PI_2); + float yAngle = 0.5f - ((atan2(direction.z, direction.y) + M_PI_2)); + + // Get the pixel range over which the xAngle and yAngle are scaled + float cursorRange = widget->width() * getCursorPixelRangeMult(); + + pos.setX(widget->width() / 2.0f + cursorRange * xAngle); + pos.setY(widget->height() / 2.0f + cursorRange * yAngle); + + //If we are off screen then we should stop processing, and if a trigger or bumper is pressed, + //we should unpress them. + if (pos.x() < 0 || pos.x() > widget->width() || pos.y() < 0 || pos.y() > widget->height()) { + if (_bumperPressed[index]) { + QMouseEvent mouseEvent(QEvent::MouseButtonRelease, pos, bumperButton, bumperButton, 0); + + application->mouseReleaseEvent(&mouseEvent); + + _bumperPressed[index] = false; + } + if (_triggerPressed[index]) { + QMouseEvent mouseEvent(QEvent::MouseButtonRelease, pos, triggerButton, triggerButton, 0); + + application->mouseReleaseEvent(&mouseEvent); + + _triggerPressed[index] = false; + } + return; + } + + //If position has changed, emit a mouse move to the application + if (pos.x() != _oldX[index] || pos.y() != _oldY[index]) { + QMouseEvent mouseEvent(static_cast(CONTROLLER_MOVE_EVENT), pos, Qt::NoButton, Qt::NoButton, 0); + + //Only send the mouse event if the opposite left button isnt held down. + //This is specifically for edit voxels + if (triggerButton == Qt::LeftButton) { + if (!_triggerPressed[(int)(!index)]) { + application->mouseMoveEvent(&mouseEvent); + } + } else { + if (!_bumperPressed[(int)(!index)]) { + application->mouseMoveEvent(&mouseEvent); + } + } + } + _oldX[index] = pos.x(); + _oldY[index] = pos.y(); + + //We need separate coordinates for clicks, since we need to check if + //a magnification window was clicked on + int clickX = pos.x(); + int clickY = pos.y(); + //Checks for magnification window click + application->getApplicationOverlay().getClickLocation(clickX, clickY); + //Set pos to the new click location, which may be the same if no magnification window is open + pos.setX(clickX); + pos.setY(clickY); + //Check for bumper press ( Right Click ) if (palm->getControllerButtons() & BUTTON_FWD) { - if (!_bumperPressed) { - _bumperPressed = true; + if (!_bumperPressed[index]) { + _bumperPressed[index] = true; QMouseEvent mouseEvent(QEvent::MouseButtonPress, pos, bumperButton, bumperButton, 0); - Application::getInstance()->mousePressEvent(&mouseEvent); + application->mousePressEvent(&mouseEvent); } - } else if (_bumperPressed) { + } else if (_bumperPressed[index]) { QMouseEvent mouseEvent(QEvent::MouseButtonRelease, pos, bumperButton, bumperButton, 0); - Application::getInstance()->mouseReleaseEvent(&mouseEvent); + application->mouseReleaseEvent(&mouseEvent); - _bumperPressed = false; + _bumperPressed[index] = false; } //Check for trigger press ( Left Click ) if (palm->getTrigger() == 1.0f) { - if (!_triggerPressed) { - _triggerPressed = true; + if (!_triggerPressed[index]) { + _triggerPressed[index] = true; QMouseEvent mouseEvent(QEvent::MouseButtonPress, pos, triggerButton, triggerButton, 0); - Application::getInstance()->mousePressEvent(&mouseEvent); + application->mousePressEvent(&mouseEvent); } - } else if (_triggerPressed) { + } else if (_triggerPressed[index]) { QMouseEvent mouseEvent(QEvent::MouseButtonRelease, pos, triggerButton, triggerButton, 0); - Application::getInstance()->mouseReleaseEvent(&mouseEvent); + application->mouseReleaseEvent(&mouseEvent); - _triggerPressed = false; + _triggerPressed[index] = false; } - - } #endif // HAVE_SIXENSE diff --git a/interface/src/devices/SixenseManager.h b/interface/src/devices/SixenseManager.h index 93888ce944..8803c2c006 100644 --- a/interface/src/devices/SixenseManager.h +++ b/interface/src/devices/SixenseManager.h @@ -30,6 +30,9 @@ const unsigned int BUTTON_FWD = 1U << 7; // Event type that represents moving the controller const unsigned int CONTROLLER_MOVE_EVENT = 1500U; +const float DEFAULT_SIXENSE_RETICLE_MOVE_SPEED = 37.5f; +const bool DEFAULT_INVERT_SIXENSE_MOUSE_BUTTONS = false; + /// Handles interaction with the Sixense SDK (e.g., Razer Hydra). class SixenseManager : public QObject { Q_OBJECT @@ -39,6 +42,7 @@ public: ~SixenseManager(); void update(float deltaTime); + float getCursorPixelRangeMult() const; public slots: @@ -47,7 +51,7 @@ public slots: private: #ifdef HAVE_SIXENSE void updateCalibration(const sixenseControllerData* controllers); - void emulateMouse(PalmData *palm); + void emulateMouse(PalmData* palm, int index); int _calibrationState; @@ -70,11 +74,11 @@ private: quint64 _lastMovement; glm::vec3 _amountMoved; - // for mouse emulation - bool _triggerPressed; - bool _bumperPressed; - int _oldX; - int _oldY; + // for mouse emulation with the two controllers + bool _triggerPressed[2]; + bool _bumperPressed[2]; + int _oldX[2]; + int _oldY[2]; }; #endif // hifi_SixenseManager_h diff --git a/interface/src/location/LocationManager.cpp b/interface/src/location/LocationManager.cpp index f80c331df4..1d783cc8e7 100644 --- a/interface/src/location/LocationManager.cpp +++ b/interface/src/location/LocationManager.cpp @@ -16,10 +16,11 @@ const QString GET_USER_ADDRESS = "/api/v1/users/%1/address"; const QString GET_PLACE_ADDRESS = "/api/v1/places/%1/address"; +const QString GET_ADDRESSES = "/api/v1/addresses/%1"; const QString POST_PLACE_CREATE = "/api/v1/places/"; -LocationManager::LocationManager() : _userData(), _placeData() { +LocationManager::LocationManager() { }; @@ -74,59 +75,38 @@ void LocationManager::goTo(QString destination) { // go to coordinate destination or to Username if (!goToDestination(destination)) { - // reset data on local variables - _userData = QJsonObject(); - _placeData = QJsonObject(); - destination = QString(QUrl::toPercentEncoding(destination)); + JSONCallbackParameters callbackParams; callbackParams.jsonCallbackReceiver = this; - callbackParams.jsonCallbackMethod = "goToUserFromResponse"; - AccountManager::getInstance().authenticatedRequest(GET_USER_ADDRESS.arg(destination), - QNetworkAccessManager::GetOperation, - callbackParams); - - callbackParams.jsonCallbackMethod = "goToLocationFromResponse"; - AccountManager::getInstance().authenticatedRequest(GET_PLACE_ADDRESS.arg(destination), + callbackParams.jsonCallbackMethod = "goToAddressFromResponse"; + AccountManager::getInstance().authenticatedRequest(GET_ADDRESSES.arg(destination), QNetworkAccessManager::GetOperation, callbackParams); } } -void LocationManager::goToUserFromResponse(const QJsonObject& jsonObject) { - _userData = jsonObject; - checkForMultipleDestinations(); -} +void LocationManager::goToAddressFromResponse(const QJsonObject& responseData) { + QJsonValue status = responseData["status"]; + qDebug() << responseData; + if (!status.isUndefined() && status.toString() == "success") { + const QJsonObject& data = responseData["data"].toObject(); + const QJsonValue& userObject = data["user"]; + const QJsonValue& placeObject = data["place"]; -void LocationManager::goToLocationFromResponse(const QJsonObject& jsonObject) { - _placeData = jsonObject; - checkForMultipleDestinations(); -} - -void LocationManager::checkForMultipleDestinations() { - if (!_userData.isEmpty() && !_placeData.isEmpty()) { - if (_userData.contains("status") && _userData["status"].toString() == "success" && - _placeData.contains("status") && _placeData["status"].toString() == "success") { - emit multipleDestinationsFound(_userData, _placeData); - return; + if (!placeObject.isUndefined() && !userObject.isUndefined()) { + emit multipleDestinationsFound(userObject.toObject(), placeObject.toObject()); + } else if (placeObject.isUndefined()) { + Application::getInstance()->getAvatar()->goToLocationFromAddress(userObject.toObject()["address"].toObject()); + } else { + Application::getInstance()->getAvatar()->goToLocationFromAddress(placeObject.toObject()["address"].toObject()); } - - if (_userData.contains("status") && _userData["status"].toString() == "success") { - Application::getInstance()->getAvatar()->goToLocationFromResponse(_userData); - return; - } - - if (_placeData.contains("status") && _placeData["status"].toString() == "success") { - Application::getInstance()->getAvatar()->goToLocationFromResponse(_placeData); - return; - } - + } else { QMessageBox::warning(Application::getInstance()->getWindow(), "", "That user or location could not be found."); } } void LocationManager::goToUser(QString userName) { - JSONCallbackParameters callbackParams; callbackParams.jsonCallbackReceiver = Application::getInstance()->getAvatar(); callbackParams.jsonCallbackMethod = "goToLocationFromResponse"; diff --git a/interface/src/location/LocationManager.h b/interface/src/location/LocationManager.h index ac66b3d08b..b781f3f54e 100644 --- a/interface/src/location/LocationManager.h +++ b/interface/src/location/LocationManager.h @@ -39,11 +39,7 @@ public: bool goToDestination(QString destination); private: - QJsonObject _userData; - QJsonObject _placeData; - void replaceLastOccurrence(const QChar search, const QChar replace, QString& string); - void checkForMultipleDestinations(); signals: void creationCompleted(LocationManager::NamedLocationCreateResponse response); @@ -52,8 +48,7 @@ signals: private slots: void namedLocationDataReceived(const QJsonObject& data); void errorDataReceived(QNetworkReply::NetworkError error, const QString& message); - void goToLocationFromResponse(const QJsonObject& jsonObject); - void goToUserFromResponse(const QJsonObject& jsonObject); + void goToAddressFromResponse(const QJsonObject& jsonObject); }; diff --git a/interface/src/scripting/AccountScriptingInterface.cpp b/interface/src/scripting/AccountScriptingInterface.cpp new file mode 100644 index 0000000000..87ea3220a4 --- /dev/null +++ b/interface/src/scripting/AccountScriptingInterface.cpp @@ -0,0 +1,41 @@ +// +// AccountScriptingInterface.cpp +// interface/src/scripting +// +// Created by Stojce Slavkovski on 6/07/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AccountManager.h" + +#include "AccountScriptingInterface.h" + +AccountScriptingInterface::AccountScriptingInterface() { + AccountManager& accountManager = AccountManager::getInstance(); + connect(&accountManager, &AccountManager::balanceChanged, this, + &AccountScriptingInterface::updateBalance); + +} + +AccountScriptingInterface* AccountScriptingInterface::getInstance() { + static AccountScriptingInterface sharedInstance; + return &sharedInstance; +} + +float AccountScriptingInterface::getBalance() { + AccountManager& accountManager = AccountManager::getInstance(); + return accountManager.getAccountInfo().getBalanceInSatoshis(); +} + +bool AccountScriptingInterface::isLoggedIn() { + AccountManager& accountManager = AccountManager::getInstance(); + return accountManager.isLoggedIn(); +} + +void AccountScriptingInterface::updateBalance() { + AccountManager& accountManager = AccountManager::getInstance(); + emit balanceChanged(accountManager.getAccountInfo().getBalanceInSatoshis()); +} diff --git a/interface/src/scripting/AccountScriptingInterface.h b/interface/src/scripting/AccountScriptingInterface.h new file mode 100644 index 0000000000..e9cf0ede5f --- /dev/null +++ b/interface/src/scripting/AccountScriptingInterface.h @@ -0,0 +1,31 @@ +// +// AccountScriptingInterface.h +// interface/src/scripting +// +// Created by Stojce Slavkovski on 6/07/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AccountScriptingInterface_h +#define hifi_AccountScriptingInterface_h + +#include + +class AccountScriptingInterface : public QObject { + Q_OBJECT + AccountScriptingInterface(); + +signals: + void balanceChanged(float newBalance); + +public slots: + static AccountScriptingInterface* getInstance(); + float getBalance(); + bool isLoggedIn(); + void updateBalance(); +}; + +#endif // hifi_AccountScriptingInterface_h diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 924c2e9d84..9be556cf62 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -16,15 +16,36 @@ #include "Application.h" #include "ApplicationOverlay.h" +#include "devices/OculusManager.h" #include "ui/Stats.h" +// Used to fade the UI +const float FADE_SPEED = 0.08f; +// Used to animate the magnification windows +const float MAG_SPEED = 0.08f; + +const quint64 MSECS_TO_USECS = 1000ULL; + +// Fast helper functions +inline float max(float a, float b) { + return (a > b) ? a : b; +} + +inline float min(float a, float b) { + return (a < b) ? a : b; +} + ApplicationOverlay::ApplicationOverlay() : _framebufferObject(NULL), - _oculusAngle(65.0f * RADIANS_PER_DEGREE), - _distance(0.5f), - _uiType(HEMISPHERE) { + _textureFov(DEFAULT_OCULUS_UI_ANGULAR_SIZE * RADIANS_PER_DEGREE), + _crosshairTexture(0), + _alpha(1.0f), + _active(true) { + memset(_reticleActive, 0, sizeof(_reticleActive)); + memset(_magActive, 0, sizeof(_reticleActive)); + memset(_magSizeMult, 0, sizeof(_magSizeMult)); } ApplicationOverlay::~ApplicationOverlay() { @@ -34,100 +55,33 @@ ApplicationOverlay::~ApplicationOverlay() { } const float WHITE_TEXT[] = { 0.93f, 0.93f, 0.93f }; - -void renderControllerPointer() { - Application* application = Application::getInstance(); - QGLWidget* glWidget = application->getGLWidget(); - MyAvatar* myAvatar = application->getAvatar(); - - const HandData* handData = Application::getInstance()->getAvatar()->getHandData(); - int numberOfPalms = handData->getNumPalms(); - - - int palmIndex; - if (Menu::getInstance()->isOptionChecked(MenuOption::SixenseLeftHanded)) { - palmIndex = 2; - } else { - palmIndex = 3; - } - const PalmData* palmData = NULL; - - if (palmIndex >= handData->getPalms().size()) { - return; - } - - if (handData->getPalms()[palmIndex].isActive()) { - palmData = &handData->getPalms()[palmIndex]; - } else { - return; - } - - // Get directon relative to avatar orientation - glm::vec3 direction = glm::inverse(myAvatar->getOrientation()) * palmData->getFingerDirection(); - - // Get the angles, scaled between 0-1 - float xAngle = (atan2(direction.z, direction.x) + M_PI_2) + 0.5f; - float yAngle = 1.0f - ((atan2(direction.z, direction.y) + M_PI_2) + 0.5f); - - float cursorRange = glWidget->width(); - - int mouseX = cursorRange * xAngle; - int mouseY = cursorRange * yAngle; - - if (mouseX < 0) { - mouseX = 0; - } else if (mouseX > glWidget->width()) { - mouseX = glWidget->width(); - } - if (mouseY < 0) { - mouseY = 0; - } else if (mouseY > glWidget->width()) { - mouseY = glWidget->width(); - } - - const float pointerWidth = 40; - const float pointerHeight = 40; - const float crossPad = 16; - - mouseX -= pointerWidth / 2.0f; - mouseY += pointerHeight / 2.0f; - - glBegin(GL_QUADS); - - glColor3f(0, 0, 1); - - //Horizontal crosshair - glVertex2i(mouseX, mouseY - crossPad); - glVertex2i(mouseX + pointerWidth, mouseY - crossPad); - glVertex2i(mouseX + pointerWidth, mouseY - pointerHeight + crossPad); - glVertex2i(mouseX, mouseY - pointerHeight + crossPad); - - //Vertical crosshair - glVertex2i(mouseX + crossPad, mouseY); - glVertex2i(mouseX + pointerWidth - crossPad, mouseY); - glVertex2i(mouseX + pointerWidth - crossPad, mouseY - pointerHeight); - glVertex2i(mouseX + crossPad, mouseY - pointerHeight); - - glEnd(); -} +const float RETICLE_COLOR[] = { 0.0f, 198.0f / 255.0f, 244.0f / 255.0f }; // Renders the overlays either to a texture or to the screen void ApplicationOverlay::renderOverlay(bool renderToTexture) { + PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "ApplicationOverlay::displayOverlay()"); + _textureFov = Menu::getInstance()->getOculusUIAngularSize() * RADIANS_PER_DEGREE; + Application* application = Application::getInstance(); Overlays& overlays = application->getOverlays(); QGLWidget* glWidget = application->getGLWidget(); MyAvatar* myAvatar = application->getAvatar(); - Audio* audio = application->getAudio(); - const OctreePacketProcessor& octreePacketProcessor = application->getOctreePacketProcessor(); - BandwidthMeter* bandwidthMeter = application->getBandwidthMeter(); - NodeBounds& nodeBoundsDisplay = application->getNodeBoundsDisplay(); - int mouseX = application->getMouseX(); - int mouseY = application->getMouseY(); - bool renderPointer = renderToTexture; + //Handle fadeing and deactivation/activation of UI + if (_active) { + _alpha += FADE_SPEED; + if (_alpha > 1.0f) { + _alpha = 1.0f; + } + } else { + _alpha -= FADE_SPEED; + if (_alpha <= 0.0f) { + _alpha = 0.0f; + } + } if (renderToTexture) { getFramebufferObject()->bind(); @@ -136,7 +90,8 @@ void ApplicationOverlay::renderOverlay(bool renderToTexture) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - // Render 2D overlay: I/O level bar graphs and text + + // Render 2D overlay glMatrixMode(GL_PROJECTION); glPushMatrix(); @@ -145,6 +100,545 @@ void ApplicationOverlay::renderOverlay(bool renderToTexture) { glDisable(GL_DEPTH_TEST); glDisable(GL_LIGHTING); + renderAudioMeter(); + + if (Menu::getInstance()->isOptionChecked(MenuOption::HeadMouse)) { + myAvatar->renderHeadMouse(glWidget->width(), glWidget->height()); + } + + renderStatsAndLogs(); + + // give external parties a change to hook in + emit application->renderingOverlay(); + + overlays.render2D(); + + renderPointers(); + + glPopMatrix(); + + + glMatrixMode(GL_MODELVIEW); + glEnable(GL_DEPTH_TEST); + glEnable(GL_LIGHTING); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); + + if (renderToTexture) { + getFramebufferObject()->release(); + } +} + +// Draws the FBO texture for the screen +void ApplicationOverlay::displayOverlayTexture() { + + if (_alpha == 0.0f) { + return; + } + + Application* application = Application::getInstance(); + QGLWidget* glWidget = application->getGLWidget(); + + glEnable(GL_TEXTURE_2D); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, getFramebufferObject()->texture()); + + glMatrixMode(GL_PROJECTION); + glPushMatrix(); + + glLoadIdentity(); + gluOrtho2D(0, glWidget->width(), glWidget->height(), 0); + glDisable(GL_DEPTH_TEST); + glDisable(GL_LIGHTING); + glEnable(GL_BLEND); + + glBegin(GL_QUADS); + glColor4f(1.0f, 1.0f, 1.0f, _alpha); + glTexCoord2f(0, 0); glVertex2i(0, glWidget->height()); + glTexCoord2f(1, 0); glVertex2i(glWidget->width(), glWidget->height()); + glTexCoord2f(1, 1); glVertex2i(glWidget->width(), 0); + glTexCoord2f(0, 1); glVertex2i(0, 0); + glEnd(); + glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + + glPopMatrix(); + glDisable(GL_TEXTURE_2D); +} + +void ApplicationOverlay::computeOculusPickRay(float x, float y, glm::vec3& direction) const { + glm::quat rot = Application::getInstance()->getAvatar()->getOrientation(); + + //invert y direction + y = 1.0 - y; + + //Get position on hemisphere UI + x = sin((x - 0.5f) * _textureFov); + y = sin((y - 0.5f) * _textureFov); + + float dist = sqrt(x * x + y * y); + float z = -sqrt(1.0f - dist * dist); + + //Rotate the UI pick ray by the avatar orientation + direction = glm::normalize(rot * glm::vec3(x, y, z)); +} + +// Calculates the click location on the screen by taking into account any +// opened magnification windows. +void ApplicationOverlay::getClickLocation(int &x, int &y) const { + int dx; + int dy; + const float xRange = MAGNIFY_WIDTH * MAGNIFY_MULT / 2.0f; + const float yRange = MAGNIFY_WIDTH * MAGNIFY_MULT / 2.0f; + + //Loop through all magnification windows + for (int i = 0; i < NUMBER_OF_MAGNIFIERS; i++) { + if (_magActive[i]) { + dx = x - _magX[i]; + dy = y - _magY[i]; + //Check to see if they clicked inside a mag window + if (abs(dx) <= xRange && abs(dy) <= yRange) { + //Move the click to the actual UI location by inverting the magnification + x = dx / MAGNIFY_MULT + _magX[i]; + y = dy / MAGNIFY_MULT + _magY[i]; + return; + } + } + } +} + +// Draws the FBO texture for Oculus rift. TODO: Draw a curved texture instead of plane. +void ApplicationOverlay::displayOverlayTextureOculus(Camera& whichCamera) { + + if (_alpha == 0.0f) { + return; + } + + Application* application = Application::getInstance(); + + MyAvatar* myAvatar = application->getAvatar(); + const glm::vec3& viewMatrixTranslation = application->getViewMatrixTranslation(); + + glActiveTexture(GL_TEXTURE0); + + glEnable(GL_BLEND); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); + glBindTexture(GL_TEXTURE_2D, getFramebufferObject()->texture()); + glEnable(GL_DEPTH_TEST); + glDisable(GL_LIGHTING); + glEnable(GL_TEXTURE_2D); + + glMatrixMode(GL_MODELVIEW); + + glPushMatrix(); + glLoadIdentity(); + // Transform to world space + glm::quat rotation = whichCamera.getRotation(); + glm::vec3 axis2 = glm::axis(rotation); + glRotatef(-glm::degrees(glm::angle(rotation)), axis2.x, axis2.y, axis2.z); + glTranslatef(viewMatrixTranslation.x, viewMatrixTranslation.y, viewMatrixTranslation.z); + + // Translate to the front of the camera + glm::vec3 pos = whichCamera.getPosition(); + glm::quat rot = myAvatar->getOrientation(); + glm::vec3 axis = glm::axis(rot); + + glTranslatef(pos.x, pos.y, pos.z); + glRotatef(glm::degrees(glm::angle(rot)), axis.x, axis.y, axis.z); + + glDepthMask(GL_TRUE); + + glEnable(GL_ALPHA_TEST); + glAlphaFunc(GL_GREATER, 0.01f); + + //Update and draw the magnifiers + for (int i = 0; i < NUMBER_OF_MAGNIFIERS; i++) { + + if (_magActive[i]) { + _magSizeMult[i] += MAG_SPEED; + if (_magSizeMult[i] > 1.0f) { + _magSizeMult[i] = 1.0f; + } + } else { + _magSizeMult[i] -= MAG_SPEED; + if (_magSizeMult[i] < 0.0f) { + _magSizeMult[i] = 0.0f; + } + } + + if (_magSizeMult[i] > 0.0f) { + //Render magnifier, but dont show border for mouse magnifier + renderMagnifier(_magX[i], _magY[i], _magSizeMult[i], i != MOUSE); + } + } + + glDepthMask(GL_FALSE); + glDisable(GL_ALPHA_TEST); + + glColor4f(1.0f, 1.0f, 1.0f, _alpha); + + renderTexturedHemisphere(); + + renderControllerPointersOculus(); + + glPopMatrix(); + + glDepthMask(GL_TRUE); + glBindTexture(GL_TEXTURE_2D, 0); + glDisable(GL_TEXTURE_2D); + + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); + glEnable(GL_LIGHTING); + + glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + +} + +//Renders optional pointers +void ApplicationOverlay::renderPointers() { + Application* application = Application::getInstance(); + + //lazily load crosshair texture + if (_crosshairTexture == 0) { + _crosshairTexture = Application::getInstance()->getGLWidget()->bindTexture(QImage(Application::resourcesPath() + "images/sixense-reticle.png")); + } + glEnable(GL_TEXTURE_2D); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, _crosshairTexture); + + + if (OculusManager::isConnected() && application->getLastMouseMoveType() == QEvent::MouseMove) { + //If we are in oculus, render reticle later + _reticleActive[MOUSE] = true; + _magActive[MOUSE] = true; + _mouseX[MOUSE] = application->getMouseX(); + _mouseY[MOUSE] = application->getMouseY(); + _magX[MOUSE] = _mouseX[MOUSE]; + _magY[MOUSE] = _mouseY[MOUSE]; + + _reticleActive[LEFT_CONTROLLER] = false; + _reticleActive[RIGHT_CONTROLLER] = false; + + } else if (application->getLastMouseMoveType() == CONTROLLER_MOVE_EVENT && Menu::getInstance()->isOptionChecked(MenuOption::SixenseMouseInput)) { + //only render controller pointer if we aren't already rendering a mouse pointer + _reticleActive[MOUSE] = false; + _magActive[MOUSE] = false; + renderControllerPointers(); + } + glBindTexture(GL_TEXTURE_2D, 0); + glDisable(GL_TEXTURE_2D); + +} + +void ApplicationOverlay::renderControllerPointers() { + Application* application = Application::getInstance(); + QGLWidget* glWidget = application->getGLWidget(); + MyAvatar* myAvatar = application->getAvatar(); + + //Static variables used for storing controller state + static quint64 pressedTime[NUMBER_OF_MAGNIFIERS] = { 0ULL, 0ULL, 0ULL }; + static bool isPressed[NUMBER_OF_MAGNIFIERS] = { false, false, false }; + static bool stateWhenPressed[NUMBER_OF_MAGNIFIERS] = { false, false, false }; + static bool triggerPressed[NUMBER_OF_MAGNIFIERS] = { false, false, false }; + static bool bumperPressed[NUMBER_OF_MAGNIFIERS] = { false, false, false }; + + const HandData* handData = Application::getInstance()->getAvatar()->getHandData(); + + for (unsigned int palmIndex = 2; palmIndex < 4; palmIndex++) { + const int index = palmIndex - 1; + + const PalmData* palmData = NULL; + + if (palmIndex >= handData->getPalms().size()) { + return; + } + + if (handData->getPalms()[palmIndex].isActive()) { + palmData = &handData->getPalms()[palmIndex]; + } else { + continue; + } + + int controllerButtons = palmData->getControllerButtons(); + + //Check for if we should toggle or drag the magnification window + if (controllerButtons & BUTTON_3) { + if (isPressed[index] == false) { + //We are now dragging the window + isPressed[index] = true; + //set the pressed time in us + pressedTime[index] = usecTimestampNow(); + stateWhenPressed[index] = _magActive[index]; + } + } else if (isPressed[index]) { + isPressed[index] = false; + //If the button was only pressed for < 250 ms + //then disable it. + + const int MAX_BUTTON_PRESS_TIME = 250 * MSECS_TO_USECS; + if (usecTimestampNow() - pressedTime[index] < MAX_BUTTON_PRESS_TIME) { + _magActive[index] = !stateWhenPressed[index]; + } + } + + //Check for UI active toggle + if (palmData->getTrigger() == 1.0f) { + if (!triggerPressed[index]) { + if (bumperPressed[index]) { + _active = !_active; + } + triggerPressed[index] = true; + } + } else { + triggerPressed[index] = false; + } + if ((controllerButtons & BUTTON_FWD)) { + if (!bumperPressed[index]) { + if (triggerPressed[index]) { + _active = !_active; + } + bumperPressed[index] = true; + } + } else { + bumperPressed[index] = false; + } + + + // Get directon relative to avatar orientation + glm::vec3 direction = glm::inverse(myAvatar->getOrientation()) * palmData->getFingerDirection(); + + // Get the angles, scaled between (-0.5,0.5) + float xAngle = (atan2(direction.z, direction.x) + M_PI_2) ; + float yAngle = 0.5f - ((atan2(direction.z, direction.y) + M_PI_2)); + + // Get the pixel range over which the xAngle and yAngle are scaled + float cursorRange = glWidget->width() * application->getSixenseManager()->getCursorPixelRangeMult(); + + int mouseX = glWidget->width() / 2.0f + cursorRange * xAngle; + int mouseY = glWidget->height() / 2.0f + cursorRange * yAngle; + + //If the cursor is out of the screen then don't render it + if (mouseX < 0 || mouseX >= glWidget->width() || mouseY < 0 || mouseY >= glWidget->height()) { + _reticleActive[index] = false; + continue; + } + _reticleActive[index] = true; + + //if we have the oculus, we should make the cursor smaller since it will be + //magnified + if (OculusManager::isConnected()) { + + _mouseX[index] = mouseX; + _mouseY[index] = mouseY; + + //When button 2 is pressed we drag the mag window + if (isPressed[index]) { + _magActive[index] = true; + _magX[index] = mouseX; + _magY[index] = mouseY; + } + + // If oculus is enabled, we draw the crosshairs later + continue; + } + + const float reticleSize = 40.0f; + + mouseX -= reticleSize / 2.0f; + mouseY += reticleSize / 2.0f; + + glBegin(GL_QUADS); + + glColor3f(RETICLE_COLOR[0], RETICLE_COLOR[1], RETICLE_COLOR[2]); + + glTexCoord2d(0.0f, 0.0f); glVertex2i(mouseX, mouseY); + glTexCoord2d(1.0f, 0.0f); glVertex2i(mouseX + reticleSize, mouseY); + glTexCoord2d(1.0f, 1.0f); glVertex2i(mouseX + reticleSize, mouseY - reticleSize); + glTexCoord2d(0.0f, 1.0f); glVertex2i(mouseX, mouseY - reticleSize); + + glEnd(); + } +} + +void ApplicationOverlay::renderControllerPointersOculus() { + Application* application = Application::getInstance(); + QGLWidget* glWidget = application->getGLWidget(); + + const int widgetWidth = glWidget->width(); + const int widgetHeight = glWidget->height(); + + const float reticleSize = 50.0f; + + glBindTexture(GL_TEXTURE_2D, _crosshairTexture); + glDisable(GL_DEPTH_TEST); + + for (int i = 0; i < NUMBER_OF_MAGNIFIERS; i++) { + + //Dont render the reticle if its inactive + if (!_reticleActive[i]) { + continue; + } + + float mouseX = (float)_mouseX[i]; + float mouseY = (float)_mouseY[i]; + mouseX -= reticleSize / 2; + mouseY += reticleSize / 2; + + //Get new UV coordinates from our magnification window + float newULeft = mouseX / widgetWidth; + float newURight = (mouseX + reticleSize) / widgetWidth; + float newVBottom = 1.0 - mouseY / widgetHeight; + float newVTop = 1.0 - (mouseY - reticleSize) / widgetHeight; + + // Project our position onto the hemisphere using the UV coordinates + float lX = sin((newULeft - 0.5f) * _textureFov); + float rX = sin((newURight - 0.5f) * _textureFov); + float bY = sin((newVBottom - 0.5f) * _textureFov); + float tY = sin((newVTop - 0.5f) * _textureFov); + + float dist; + //Bottom Left + dist = sqrt(lX * lX + bY * bY); + float blZ = sqrt(1.0f - dist * dist); + //Top Left + dist = sqrt(lX * lX + tY * tY); + float tlZ = sqrt(1.0f - dist * dist); + //Bottom Right + dist = sqrt(rX * rX + bY * bY); + float brZ = sqrt(1.0f - dist * dist); + //Top Right + dist = sqrt(rX * rX + tY * tY); + float trZ = sqrt(1.0f - dist * dist); + + glBegin(GL_QUADS); + + glColor4f(RETICLE_COLOR[0], RETICLE_COLOR[1], RETICLE_COLOR[2], _alpha); + + glTexCoord2f(0.0f, 0.0f); glVertex3f(lX, tY, -tlZ); + glTexCoord2f(1.0f, 0.0f); glVertex3f(rX, tY, -trZ); + glTexCoord2f(1.0f, 1.0f); glVertex3f(rX, bY, -brZ); + glTexCoord2f(0.0f, 1.0f); glVertex3f(lX, bY, -blZ); + + glEnd(); + + } + glEnable(GL_DEPTH_TEST); +} + +//Renders a small magnification of the currently bound texture at the coordinates +void ApplicationOverlay::renderMagnifier(int mouseX, int mouseY, float sizeMult, bool showBorder) const +{ + Application* application = Application::getInstance(); + QGLWidget* glWidget = application->getGLWidget(); + + const int widgetWidth = glWidget->width(); + const int widgetHeight = glWidget->height(); + + const float magnifyWidth = MAGNIFY_WIDTH * sizeMult; + const float magnifyHeight = MAGNIFY_HEIGHT * sizeMult; + + mouseX -= magnifyWidth / 2; + mouseY -= magnifyHeight / 2; + + float newWidth = magnifyWidth * MAGNIFY_MULT; + float newHeight = magnifyHeight * MAGNIFY_MULT; + + // Magnification Texture Coordinates + float magnifyULeft = mouseX / (float)widgetWidth; + float magnifyURight = (mouseX + magnifyWidth) / (float)widgetWidth; + float magnifyVBottom = 1.0f - mouseY / (float)widgetHeight; + float magnifyVTop = 1.0f - (mouseY + magnifyHeight) / (float)widgetHeight; + + // Coordinates of magnification overlay + float newMouseX = (mouseX + magnifyWidth / 2) - newWidth / 2.0f; + float newMouseY = (mouseY + magnifyHeight / 2) + newHeight / 2.0f; + + // Get position on hemisphere using angle + + //Get new UV coordinates from our magnification window + float newULeft = newMouseX / widgetWidth; + float newURight = (newMouseX + newWidth) / widgetWidth; + float newVBottom = 1.0 - newMouseY / widgetHeight; + float newVTop = 1.0 - (newMouseY - newHeight) / widgetHeight; + + // Project our position onto the hemisphere using the UV coordinates + float lX = sin((newULeft - 0.5f) * _textureFov); + float rX = sin((newURight - 0.5f) * _textureFov); + float bY = sin((newVBottom - 0.5f) * _textureFov); + float tY = sin((newVTop - 0.5f) * _textureFov); + + float blZ, tlZ, brZ, trZ; + + float dist; + float discriminant; + //Bottom Left + dist = sqrt(lX * lX + bY * bY); + discriminant = 1.0f - dist * dist; + if (discriminant > 0) { + blZ = sqrt(discriminant); + } else { + blZ = 0; + } + //Top Left + dist = sqrt(lX * lX + tY * tY); + discriminant = 1.0f - dist * dist; + if (discriminant > 0) { + tlZ = sqrt(discriminant); + } else { + tlZ = 0; + } + //Bottom Right + dist = sqrt(rX * rX + bY * bY); + discriminant = 1.0f - dist * dist; + if (discriminant > 0) { + brZ = sqrt(discriminant); + } else { + brZ = 0; + } + //Top Right + dist = sqrt(rX * rX + tY * tY); + discriminant = 1.0f - dist * dist; + if (discriminant > 0) { + trZ = sqrt(discriminant); + } else { + trZ = 0; + } + + if (showBorder) { + glDisable(GL_TEXTURE_2D); + glLineWidth(1.0f); + //Outer Line + glBegin(GL_LINE_STRIP); + glColor4f(1.0f, 0.0f, 0.0f, _alpha); + + glVertex3f(lX, tY, -tlZ); + glVertex3f(rX, tY, -trZ); + glVertex3f(rX, bY, -brZ); + glVertex3f(lX, bY, -blZ); + glVertex3f(lX, tY, -tlZ); + + glEnd(); + glEnable(GL_TEXTURE_2D); + } + glColor4f(1.0f, 1.0f, 1.0f, _alpha); + + glBegin(GL_QUADS); + + glTexCoord2f(magnifyULeft, magnifyVBottom); glVertex3f(lX, tY, -tlZ); + glTexCoord2f(magnifyURight, magnifyVBottom); glVertex3f(rX, tY, -trZ); + glTexCoord2f(magnifyURight, magnifyVTop); glVertex3f(rX, bY, -brZ); + glTexCoord2f(magnifyULeft, magnifyVTop); glVertex3f(lX, bY, -blZ); + + glEnd(); + +} + +void ApplicationOverlay::renderAudioMeter() { + + Application* application = Application::getInstance(); + + QGLWidget* glWidget = application->getGLWidget(); + Audio* audio = application->getAudio(); + // Display a single screen-size quad to create an alpha blended 'collision' flash if (audio->getCollisionFlashesScreen()) { float collisionSoundMagnitude = audio->getCollisionSoundMagnitude(); @@ -263,11 +757,16 @@ void ApplicationOverlay::renderOverlay(bool renderToTexture) { glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET + audioLevel, audioMeterY + AUDIO_METER_HEIGHT - AUDIO_METER_INSET); glVertex2i(AUDIO_METER_X + AUDIO_METER_INSET, audioMeterY + AUDIO_METER_HEIGHT - AUDIO_METER_INSET); glEnd(); +} +void ApplicationOverlay::renderStatsAndLogs() { - if (Menu::getInstance()->isOptionChecked(MenuOption::HeadMouse)) { - myAvatar->renderHeadMouse(glWidget->width(), glWidget->height()); - } + Application* application = Application::getInstance(); + + QGLWidget* glWidget = application->getGLWidget(); + const OctreePacketProcessor& octreePacketProcessor = application->getOctreePacketProcessor(); + BandwidthMeter* bandwidthMeter = application->getBandwidthMeter(); + NodeBounds& nodeBoundsDisplay = application->getNodeBoundsDisplay(); // Display stats and log text onscreen glLineWidth(1.0f); @@ -298,348 +797,22 @@ void ApplicationOverlay::renderOverlay(bool renderToTexture) { drawText(glWidget->width() - 100, glWidget->height() - timerBottom, 0.30f, 0.0f, 0, frameTimer, WHITE_TEXT); } nodeBoundsDisplay.drawOverlay(); - - // give external parties a change to hook in - emit application->renderingOverlay(); - - overlays.render2D(); - - // Render a crosshair over the pointer when in Oculus - if (renderPointer) { - const float pointerWidth = 10; - const float pointerHeight = 10; - const float crossPad = 4; - - mouseX -= pointerWidth / 2.0f; - mouseY += pointerHeight / 2.0f; - - glBegin(GL_QUADS); - - glColor3f(1, 0, 0); - - //Horizontal crosshair - glVertex2i(mouseX, mouseY - crossPad); - glVertex2i(mouseX + pointerWidth, mouseY - crossPad); - glVertex2i(mouseX + pointerWidth, mouseY - pointerHeight + crossPad); - glVertex2i(mouseX, mouseY - pointerHeight + crossPad); - - //Vertical crosshair - glVertex2i(mouseX + crossPad, mouseY); - glVertex2i(mouseX + pointerWidth - crossPad, mouseY); - glVertex2i(mouseX + pointerWidth - crossPad, mouseY - pointerHeight); - glVertex2i(mouseX + crossPad, mouseY - pointerHeight); - - glEnd(); - } else if (Menu::getInstance()->isOptionChecked(MenuOption::SixenseMouseInput)) { - //only render controller pointer if we aren't already rendering a mouse pointer - renderControllerPointer(); - } - glPopMatrix(); - - glMatrixMode(GL_MODELVIEW); - glEnable(GL_DEPTH_TEST); - glEnable(GL_LIGHTING); - glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); - - if (renderToTexture) { - getFramebufferObject()->release(); - } -} - -// Draws the FBO texture for the screen -void ApplicationOverlay::displayOverlayTexture(Camera& whichCamera) { - - Application* application = Application::getInstance(); - QGLWidget* glWidget = application->getGLWidget(); - - glEnable(GL_TEXTURE_2D); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, getFramebufferObject()->texture()); - - glMatrixMode(GL_PROJECTION); - glPushMatrix(); - - glLoadIdentity(); - gluOrtho2D(0, glWidget->width(), glWidget->height(), 0); - glDisable(GL_DEPTH_TEST); - glDisable(GL_LIGHTING); - - glBegin(GL_QUADS); - glTexCoord2f(0, 0); glVertex2i(0, glWidget->height()); - glTexCoord2f(1, 0); glVertex2i(glWidget->width(), glWidget->height()); - glTexCoord2f(1, 1); glVertex2i(glWidget->width(), 0); - glTexCoord2f(0, 1); glVertex2i(0, 0); - glEnd(); - - glPopMatrix(); - glDisable(GL_TEXTURE_2D); -} - -const float textureFov = PI / 2.5f; - -void ApplicationOverlay::computeOculusPickRay(float x, float y, glm::vec3& direction) const { - glm::quat rot = Application::getInstance()->getAvatar()->getOrientation(); - - //invert y direction - y = 1.0 - y; - - //Get position on hemisphere UI - x = sin((x - 0.5f) * textureFov); - y = sin((y - 0.5f) * textureFov); - - float dist = sqrt(x * x + y * y); - float z = -sqrt(1.0f - dist * dist); - - //Rotate the UI pick ray by the avatar orientation - direction = glm::normalize(rot * glm::vec3(x, y, z)); -} - -// Fast helper functions -inline float max(float a, float b) { - return (a > b) ? a : b; -} - -inline float min(float a, float b) { - return (a < b) ? a : b; -} - -// Draws the FBO texture for Oculus rift. TODO: Draw a curved texture instead of plane. -void ApplicationOverlay::displayOverlayTextureOculus(Camera& whichCamera) { - - Application* application = Application::getInstance(); - - QGLWidget* glWidget = application->getGLWidget(); - MyAvatar* myAvatar = application->getAvatar(); - const glm::vec3& viewMatrixTranslation = application->getViewMatrixTranslation(); - - int mouseX = application->getMouseX(); - int mouseY = application->getMouseY(); - const int widgetWidth = glWidget->width(); - const int widgetHeight = glWidget->height(); - float magnifyWidth = 80.0f; - float magnifyHeight = 60.0f; - const float magnification = 4.0f; - - // Get vertical FoV of the displayed overlay texture - const float halfVerticalAngle = _oculusAngle / 2.0f; - const float overlayAspectRatio = glWidget->width() / (float)glWidget->height(); - const float halfOverlayHeight = _distance * tan(halfVerticalAngle); - const float overlayHeight = halfOverlayHeight * 2.0f; - - // The more vertices, the better the curve - const int numHorizontalVertices = 20; - const int numVerticalVertices = 20; - // U texture coordinate width at each quad - const float quadTexWidth = 1.0f / (numHorizontalVertices - 1); - const float quadTexHeight = 1.0f / (numVerticalVertices - 1); - - // Get horizontal angle and angle increment from vertical angle and aspect ratio - const float horizontalAngle = halfVerticalAngle * 2.0f * overlayAspectRatio; - const float angleIncrement = horizontalAngle / (numHorizontalVertices - 1); - const float halfHorizontalAngle = horizontalAngle / 2; - - const float verticalAngleIncrement = _oculusAngle / (numVerticalVertices - 1); - - glActiveTexture(GL_TEXTURE0); - - glEnable(GL_BLEND); - glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); - glBindTexture(GL_TEXTURE_2D, getFramebufferObject()->texture()); - glEnable(GL_DEPTH_TEST); - glDisable(GL_LIGHTING); - glEnable(GL_TEXTURE_2D); - - glMatrixMode(GL_MODELVIEW); - - glPushMatrix(); - glLoadIdentity(); - // Transform to world space - glm::quat rotation = whichCamera.getRotation(); - glm::vec3 axis2 = glm::axis(rotation); - glRotatef(-glm::degrees(glm::angle(rotation)), axis2.x, axis2.y, axis2.z); - glTranslatef(viewMatrixTranslation.x, viewMatrixTranslation.y, viewMatrixTranslation.z); - - // Translate to the front of the camera - glm::vec3 pos = whichCamera.getPosition(); - glm::quat rot = myAvatar->getOrientation(); - glm::vec3 axis = glm::axis(rot); - - glTranslatef(pos.x, pos.y, pos.z); - glRotatef(glm::degrees(glm::angle(rot)), axis.x, axis.y, axis.z); - - glColor3f(1.0f, 1.0f, 1.0f); - - glDepthMask(GL_TRUE); - - glEnable(GL_ALPHA_TEST); - glAlphaFunc(GL_GREATER, 0.01f); - - //Draw the magnifying glass - - mouseX -= magnifyWidth / 2; - mouseY -= magnifyHeight / 2; - - //clamp the magnification - if (mouseX < 0) { - magnifyWidth += mouseX; - mouseX = 0; - } else if (mouseX + magnifyWidth > widgetWidth) { - magnifyWidth = widgetWidth - mouseX; - } - if (mouseY < 0) { - magnifyHeight += mouseY; - mouseY = 0; - } else if (mouseY + magnifyHeight > widgetHeight) { - magnifyHeight = widgetHeight - mouseY; - } - - const float halfMagnifyHeight = magnifyHeight / 2.0f; - - float newWidth = magnifyWidth * magnification; - float newHeight = magnifyHeight * magnification; - - // Magnification Texture Coordinates - float magnifyULeft = mouseX / (float)widgetWidth; - float magnifyURight = (mouseX + magnifyWidth) / (float)widgetWidth; - float magnifyVBottom = 1.0f - mouseY / (float)widgetHeight; - float magnifyVTop = 1.0f - (mouseY + magnifyHeight) / (float)widgetHeight; - - // Coordinates of magnification overlay - float newMouseX = (mouseX + magnifyWidth / 2) - newWidth / 2.0f; - float newMouseY = (mouseY + magnifyHeight / 2) + newHeight / 2.0f; - - // Get angle on the UI - float leftAngle = (newMouseX / (float)widgetWidth) * horizontalAngle - halfHorizontalAngle; - float rightAngle = ((newMouseX + newWidth) / (float)widgetWidth) * horizontalAngle - halfHorizontalAngle; - - float bottomAngle = (newMouseY / (float)widgetHeight) * _oculusAngle - halfVerticalAngle; - float topAngle = ((newMouseY - newHeight) / (float)widgetHeight) * _oculusAngle - halfVerticalAngle; - - float leftX, rightX, leftZ, rightZ, topZ, bottomZ; - - // Get position on hemisphere using angle - if (_uiType == HEMISPHERE) { - - //Get new UV coordinates from our magnification window - float newULeft = newMouseX / widgetWidth; - float newURight = (newMouseX + newWidth) / widgetWidth; - float newVBottom = 1.0 - newMouseY / widgetHeight; - float newVTop = 1.0 - (newMouseY - newHeight) / widgetHeight; - - // Project our position onto the hemisphere using the UV coordinates - float lX = sin((newULeft - 0.5f) * textureFov); - float rX = sin((newURight - 0.5f) * textureFov); - float bY = sin((newVBottom - 0.5f) * textureFov); - float tY = sin((newVTop - 0.5f) * textureFov); - - float dist; - //Bottom Left - dist = sqrt(lX * lX + bY * bY); - float blZ = sqrt(1.0f - dist * dist); - //Top Left - dist = sqrt(lX * lX + tY * tY); - float tlZ = sqrt(1.0f - dist * dist); - //Bottom Right - dist = sqrt(rX * rX + bY * bY); - float brZ = sqrt(1.0f - dist * dist); - //Top Right - dist = sqrt(rX * rX + tY * tY); - float trZ = sqrt(1.0f - dist * dist); - - glBegin(GL_QUADS); - - glTexCoord2f(magnifyULeft, magnifyVBottom); glVertex3f(lX, tY, -tlZ); - glTexCoord2f(magnifyURight, magnifyVBottom); glVertex3f(rX, tY, -trZ); - glTexCoord2f(magnifyURight, magnifyVTop); glVertex3f(rX, bY, -brZ); - glTexCoord2f(magnifyULeft, magnifyVTop); glVertex3f(lX, bY, -blZ); - - glEnd(); - - } else { - leftX = sin(leftAngle) * _distance; - rightX = sin(rightAngle) * _distance; - leftZ = -cos(leftAngle) * _distance; - rightZ = -cos(rightAngle) * _distance; - if (_uiType == CURVED_SEMICIRCLE) { - topZ = -cos(topAngle * overlayAspectRatio) * _distance; - bottomZ = -cos(bottomAngle * overlayAspectRatio) * _distance; - } else { - // Dont want to use topZ or bottomZ for SEMICIRCLE - topZ = -99999; - bottomZ = -99999; - } - - float bottomY = (1.0 - newMouseY / (float)widgetHeight) * halfOverlayHeight * 2.0f - halfOverlayHeight; - float topY = bottomY + (newHeight / widgetHeight) * halfOverlayHeight * 2; - - //TODO: Remove immediate mode in favor of VBO - glBegin(GL_QUADS); - - glTexCoord2f(magnifyULeft, magnifyVBottom); glVertex3f(leftX, topY, max(topZ, leftZ)); - glTexCoord2f(magnifyURight, magnifyVBottom); glVertex3f(rightX, topY, max(topZ, rightZ)); - glTexCoord2f(magnifyURight, magnifyVTop); glVertex3f(rightX, bottomY, max(bottomZ, rightZ)); - glTexCoord2f(magnifyULeft, magnifyVTop); glVertex3f(leftX, bottomY, max(bottomZ, leftZ)); - - glEnd(); - } - glDepthMask(GL_FALSE); - glDisable(GL_ALPHA_TEST); - - //TODO: Remove immediate mode in favor of VBO - if (_uiType == HEMISPHERE) { - renderTexturedHemisphere(); - } else{ - glBegin(GL_QUADS); - // Place the vertices in a semicircle curve around the camera - for (int i = 0; i < numHorizontalVertices - 1; i++) { - for (int j = 0; j < numVerticalVertices - 1; j++) { - - // Calculate the X and Z coordinates from the angles and radius from camera - leftX = sin(angleIncrement * i - halfHorizontalAngle) * _distance; - rightX = sin(angleIncrement * (i + 1) - halfHorizontalAngle) * _distance; - leftZ = -cos(angleIncrement * i - halfHorizontalAngle) * _distance; - rightZ = -cos(angleIncrement * (i + 1) - halfHorizontalAngle) * _distance; - if (_uiType == 2) { - topZ = -cos((verticalAngleIncrement * (j + 1) - halfVerticalAngle) * overlayAspectRatio) * _distance; - bottomZ = -cos((verticalAngleIncrement * j - halfVerticalAngle) * overlayAspectRatio) * _distance; - } else { - topZ = -99999; - bottomZ = -99999; - } - - glTexCoord2f(quadTexWidth * i, (j + 1) * quadTexHeight); - glVertex3f(leftX, (j + 1) * quadTexHeight * overlayHeight - halfOverlayHeight, max(topZ, leftZ)); - glTexCoord2f(quadTexWidth * (i + 1), (j + 1) * quadTexHeight); - glVertex3f(rightX, (j + 1) * quadTexHeight * overlayHeight - halfOverlayHeight, max(topZ, rightZ)); - glTexCoord2f(quadTexWidth * (i + 1), j * quadTexHeight); - glVertex3f(rightX, j * quadTexHeight * overlayHeight - halfOverlayHeight, max(bottomZ, rightZ)); - glTexCoord2f(quadTexWidth * i, j * quadTexHeight); - glVertex3f(leftX, j * quadTexHeight * overlayHeight - halfOverlayHeight, max(bottomZ, leftZ)); - } - } - - glEnd(); - } - - glPopMatrix(); - - glDepthMask(GL_TRUE); - glBindTexture(GL_TEXTURE_2D, 0); - glDisable(GL_TEXTURE_2D); - - glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_CONSTANT_ALPHA, GL_ONE); - glEnable(GL_LIGHTING); - } +//Renders a hemisphere with texture coordinates. void ApplicationOverlay::renderTexturedHemisphere() { const int slices = 80; const int stacks = 80; + //UV mapping source: http://www.mvps.org/directx/articles/spheremap.htm static VerticesIndices vbo(0, 0); int vertices = slices * (stacks - 1) + 1; int indices = slices * 2 * 3 * (stacks - 2) + slices * 3; - if (vbo.first == 0) { + + static float oldTextureFOV = _textureFov; + //We only generate the VBO when the _textureFov changes + if (vbo.first == 0 || oldTextureFOV != _textureFov) { + oldTextureFOV = _textureFov; TextureVertex* vertexData = new TextureVertex[vertices]; TextureVertex* vertex = vertexData; for (int i = 0; i < stacks - 1; i++) { @@ -652,8 +825,8 @@ void ApplicationOverlay::renderTexturedHemisphere() { vertex->position.x = sinf(theta) * radius; vertex->position.y = cosf(theta) * radius; vertex->position.z = z; - vertex->uv.x = asin(vertex->position.x) / (textureFov) + 0.5f; - vertex->uv.y = asin(vertex->position.y) / (textureFov) + 0.5f; + vertex->uv.x = asin(vertex->position.x) / (_textureFov) + 0.5f; + vertex->uv.y = asin(vertex->position.y) / (_textureFov) + 0.5f; vertex++; } } @@ -664,7 +837,9 @@ void ApplicationOverlay::renderTexturedHemisphere() { vertex->uv.y = 0.5f; vertex++; - glGenBuffers(1, &vbo.first); + if (vbo.first == 0){ + glGenBuffers(1, &vbo.first); + } glBindBuffer(GL_ARRAY_BUFFER, vbo.first); const int BYTES_PER_VERTEX = sizeof(TextureVertex); glBufferData(GL_ARRAY_BUFFER, vertices * BYTES_PER_VERTEX, vertexData, GL_STATIC_DRAW); diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index 1bf0e18816..b9f9596ccf 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -15,28 +15,27 @@ class Overlays; class QOpenGLFramebufferObject; +const float MAGNIFY_WIDTH = 160.0f; +const float MAGNIFY_HEIGHT = 80.0f; +const float MAGNIFY_MULT = 4.0f; + // Handles the drawing of the overlays to the screen class ApplicationOverlay { public: - enum UIType { HEMISPHERE, SEMICIRCLE, CURVED_SEMICIRCLE }; - ApplicationOverlay(); ~ApplicationOverlay(); void renderOverlay(bool renderToTexture = false); - void displayOverlayTexture(Camera& whichCamera); + void displayOverlayTexture(); void displayOverlayTextureOculus(Camera& whichCamera); void computeOculusPickRay(float x, float y, glm::vec3& direction) const; + void getClickLocation(int &x, int &y) const; // Getters QOpenGLFramebufferObject* getFramebufferObject(); - float getOculusAngle() const { return _oculusAngle; } - - // Setters - void setOculusAngle(float oculusAngle) { _oculusAngle = oculusAngle; } - void setUIType(UIType uiType) { _uiType = uiType; } - + float getAlpha() const { return _alpha; } + private: // Interleaved vertex data struct TextureVertex { @@ -46,13 +45,31 @@ private: typedef QPair VerticesIndices; + void renderPointers(); + void renderControllerPointers(); + void renderControllerPointersOculus(); + void renderMagnifier(int mouseX, int mouseY, float sizeMult, bool showBorder) const; + void renderAudioMeter(); + void renderStatsAndLogs(); void renderTexturedHemisphere(); QOpenGLFramebufferObject* _framebufferObject; float _trailingAudioLoudness; - float _oculusAngle; - float _distance; - UIType _uiType; + float _textureFov; + + enum MagnifyDevices { MOUSE, LEFT_CONTROLLER, RIGHT_CONTROLLER, NUMBER_OF_MAGNIFIERS = RIGHT_CONTROLLER + 1 }; + bool _reticleActive[NUMBER_OF_MAGNIFIERS]; + int _mouseX[NUMBER_OF_MAGNIFIERS]; + int _mouseY[NUMBER_OF_MAGNIFIERS]; + bool _magActive[NUMBER_OF_MAGNIFIERS]; + int _magX[NUMBER_OF_MAGNIFIERS]; + int _magY[NUMBER_OF_MAGNIFIERS]; + float _magSizeMult[NUMBER_OF_MAGNIFIERS]; + + float _alpha; + bool _active; + + GLuint _crosshairTexture; }; #endif // hifi_ApplicationOverlay_h \ No newline at end of file diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index d5c6079d4b..5e6c6984eb 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -137,6 +137,13 @@ void PreferencesDialog::loadPreferences() { ui.maxVoxelsSpin->setValue(menuInstance->getMaxVoxels()); ui.maxVoxelsPPSSpin->setValue(menuInstance->getMaxVoxelPacketsPerSecond()); + + ui.oculusUIAngularSizeSpin->setValue(menuInstance->getOculusUIAngularSize()); + + ui.sixenseReticleMoveSpeedSpin->setValue(menuInstance->getSixenseReticleMoveSpeed()); + + ui.invertSixenseButtonsCheckBox->setChecked(menuInstance->getInvertSixenseButtons()); + } void PreferencesDialog::savePreferences() { @@ -189,6 +196,12 @@ void PreferencesDialog::savePreferences() { (float)ui.faceshiftEyeDeflectionSider->maximum()); Menu::getInstance()->setMaxVoxelPacketsPerSecond(ui.maxVoxelsPPSSpin->value()); + Menu::getInstance()->setOculusUIAngularSize(ui.oculusUIAngularSizeSpin->value()); + + Menu::getInstance()->setSixenseReticleMoveSpeed(ui.sixenseReticleMoveSpeedSpin->value()); + + Menu::getInstance()->setInvertSixenseButtons(ui.invertSixenseButtonsCheckBox->isChecked()); + Menu::getInstance()->setAudioJitterBufferSamples(ui.audioJitterSpin->value()); Application::getInstance()->getAudio()->setJitterBufferSamples(ui.audioJitterSpin->value()); diff --git a/interface/src/ui/TextRenderer.cpp b/interface/src/ui/TextRenderer.cpp index 2743e3e572..18279d3914 100644 --- a/interface/src/ui/TextRenderer.cpp +++ b/interface/src/ui/TextRenderer.cpp @@ -26,9 +26,9 @@ Glyph::Glyph(int textureID, const QPoint& location, const QRect& bounds, int wid } TextRenderer::TextRenderer(const char* family, int pointSize, int weight, - bool italic, EffectType effectType, int effectThickness) + bool italic, EffectType effectType, int effectThickness, QColor color) : _font(family, pointSize, weight, italic), _metrics(_font), _effectType(effectType), - _effectThickness(effectThickness), _x(IMAGE_SIZE), _y(IMAGE_SIZE), _rowHeight(0) { + _effectThickness(effectThickness), _x(IMAGE_SIZE), _y(IMAGE_SIZE), _rowHeight(0), _color(color) { _font.setKerning(false); } @@ -157,7 +157,7 @@ const Glyph& TextRenderer::getGlyph(char c) { // render the glyph into an image and copy it into the texture QImage image(bounds.width(), bounds.height(), QImage::Format_ARGB32); if (c == SOLID_BLOCK_CHAR) { - image.fill(QColor(255, 255, 255)); + image.fill(_color); } else { image.fill(0); @@ -180,7 +180,7 @@ const Glyph& TextRenderer::getGlyph(char c) { painter.setRenderHint(QPainter::Antialiasing); painter.drawPath(path); } - painter.setPen(QColor(255, 255, 255)); + painter.setPen(_color); painter.drawText(-bounds.x(), -bounds.y(), ch); } glTexSubImage2D(GL_TEXTURE_2D, 0, _x, _y, bounds.width(), bounds.height(), GL_RGBA, GL_UNSIGNED_BYTE, image.constBits()); diff --git a/interface/src/ui/TextRenderer.h b/interface/src/ui/TextRenderer.h index 813f15a5ac..2daba79c8f 100644 --- a/interface/src/ui/TextRenderer.h +++ b/interface/src/ui/TextRenderer.h @@ -12,6 +12,7 @@ #ifndef hifi_TextRenderer_h #define hifi_TextRenderer_h +#include #include #include #include @@ -41,7 +42,8 @@ public: enum EffectType { NO_EFFECT, SHADOW_EFFECT, OUTLINE_EFFECT }; TextRenderer(const char* family, int pointSize = -1, int weight = -1, bool italic = false, - EffectType effect = NO_EFFECT, int effectThickness = 1); + EffectType effect = NO_EFFECT, int effectThickness = 1, + QColor color = QColor(255, 255, 255)); ~TextRenderer(); const QFontMetrics& metrics() const { return _metrics; } @@ -85,6 +87,9 @@ private: // the list of all texture ids for which we're responsible QVector _allTextureIDs; + + // text color + QColor _color; }; class Glyph { diff --git a/interface/src/ui/overlays/TextOverlay.cpp b/interface/src/ui/overlays/TextOverlay.cpp index e26c772b06..797d0be1a2 100644 --- a/interface/src/ui/overlays/TextOverlay.cpp +++ b/interface/src/ui/overlays/TextOverlay.cpp @@ -19,7 +19,8 @@ TextOverlay::TextOverlay() : _leftMargin(DEFAULT_MARGIN), - _topMargin(DEFAULT_MARGIN) + _topMargin(DEFAULT_MARGIN), + _fontSize(DEFAULT_FONTSIZE) { } @@ -32,7 +33,7 @@ void TextOverlay::render() { } const float MAX_COLOR = 255; - glColor4f(_color.red / MAX_COLOR, _color.green / MAX_COLOR, _color.blue / MAX_COLOR, _alpha); + glColor4f(0 / MAX_COLOR, 0 / MAX_COLOR, 0 / MAX_COLOR, _alpha); glBegin(GL_QUADS); glVertex2f(_bounds.left(), _bounds.top()); @@ -43,8 +44,9 @@ void TextOverlay::render() { //TextRenderer(const char* family, int pointSize = -1, int weight = -1, bool italic = false, // EffectType effect = NO_EFFECT, int effectThickness = 1); - - TextRenderer textRenderer(SANS_FONT_FAMILY, 11, 50); + TextRenderer textRenderer(SANS_FONT_FAMILY, _fontSize, 50, false, TextRenderer::NO_EFFECT, 1, + QColor(_color.red, _color.green, _color.blue)); + const int leftAdjust = -1; // required to make text render relative to left edge of bounds const int topAdjust = -2; // required to make text render relative to top edge of bounds int x = _bounds.left() + _leftMargin + leftAdjust; @@ -67,6 +69,13 @@ void TextOverlay::render() { void TextOverlay::setProperties(const QScriptValue& properties) { Overlay2D::setProperties(properties); + + QScriptValue font = properties.property("font"); + if (font.isObject()) { + if (font.property("size").isValid()) { + setFontSize(font.property("size").toInt32()); + } + } QScriptValue text = properties.property("text"); if (text.isValid()) { diff --git a/interface/src/ui/overlays/TextOverlay.h b/interface/src/ui/overlays/TextOverlay.h index fc04966d07..78a037762e 100644 --- a/interface/src/ui/overlays/TextOverlay.h +++ b/interface/src/ui/overlays/TextOverlay.h @@ -29,6 +29,7 @@ #include "Overlay2D.h" const int DEFAULT_MARGIN = 10; +const int DEFAULT_FONTSIZE = 11; class TextOverlay : public Overlay2D { Q_OBJECT @@ -47,6 +48,7 @@ public: void setText(const QString& text) { _text = text; } void setLeftMargin(int margin) { _leftMargin = margin; } void setTopMargin(int margin) { _topMargin = margin; } + void setFontSize(int fontSize) { _fontSize = fontSize; } virtual void setProperties(const QScriptValue& properties); @@ -55,7 +57,7 @@ private: QString _text; int _leftMargin; int _topMargin; - + int _fontSize; }; diff --git a/interface/ui/preferencesDialog.ui b/interface/ui/preferencesDialog.ui index a1c2073ab6..f00d7c4788 100644 --- a/interface/ui/preferencesDialog.ui +++ b/interface/ui/preferencesDialog.ui @@ -154,9 +154,9 @@ color: #0e7077 0 - -204 - 494 - 1091 + -1002 + 477 + 1386 @@ -1605,6 +1605,331 @@ padding: 10px;margin-top:10px + + + + + 0 + 0 + + + + + 0 + 40 + + + + + Arial + 20 + 50 + false + + + + color: #0e7077 + + + Oculus Rift + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + 0 + + + 10 + + + 0 + + + 10 + + + + + + Arial + + + + color: rgb(51, 51, 51) + + + User Interface Angular Size + + + 15 + + + maxVoxelsSpin + + + + + + + + Arial + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 125 + 36 + + + + + Arial + + + + 30 + + + 160 + + + 1 + + + 72 + + + + + + + + + + 0 + 0 + + + + + 0 + 40 + + + + + Arial + 20 + 50 + false + + + + color: #0e7077 + + + Sixense Controllers + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + 0 + + + 10 + + + 0 + + + 10 + + + + + + Arial + + + + color: rgb(51, 51, 51) + + + Invert Mouse Buttons + + + 15 + + + maxVoxelsSpin + + + + + + + + Arial + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 32 + 0 + + + + + 0 + 0 + + + + + + + + 32 + 32 + + + + + + + + + + 0 + + + 10 + + + 0 + + + 10 + + + + + + Arial + + + + color: rgb(51, 51, 51) + + + Reticle Movement Speed + + + 15 + + + maxVoxelsSpin + + + + + + + + Arial + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 125 + 36 + + + + + Arial + + + + 100 + + + 1 + + + 50 + + + + + diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index 9bc78f2303..7ef3afdf29 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -258,7 +258,7 @@ void Sound::interpretAsWav(const QByteArray& inputAudioByteArray, QByteArray& ou // Now pull out the data quint32 outputAudioByteArraySize = qFromLittleEndian(dataHeader.descriptor.size); outputAudioByteArray.resize(outputAudioByteArraySize); - if (waveStream.readRawData(outputAudioByteArray.data(), outputAudioByteArraySize) != outputAudioByteArraySize) { + if (waveStream.readRawData(outputAudioByteArray.data(), outputAudioByteArraySize) != (int)outputAudioByteArraySize) { qDebug() << "Error reading WAV file"; } diff --git a/libraries/metavoxels/src/Bitstream.cpp b/libraries/metavoxels/src/Bitstream.cpp index d7e15a9dd6..80aa07b026 100644 --- a/libraries/metavoxels/src/Bitstream.cpp +++ b/libraries/metavoxels/src/Bitstream.cpp @@ -44,6 +44,11 @@ static int quatStreamer = Bitstream::registerTypeStreamer(qMetaTypeId static int metaObjectStreamer = Bitstream::registerTypeStreamer(qMetaTypeId(), new SimpleTypeStreamer()); +static int genericValueStreamer = Bitstream::registerTypeStreamer( + qRegisterMetaType(), new GenericValueStreamer()); + +static int qVariantPairListMetaTypeId = qRegisterMetaType(); + IDStreamer::IDStreamer(Bitstream& stream) : _stream(stream), _bits(1) { @@ -92,7 +97,10 @@ int Bitstream::registerMetaObject(const char* className, const QMetaObject* meta } int Bitstream::registerTypeStreamer(int type, TypeStreamer* streamer) { - streamer->setType(type); + streamer->_type = type; + if (!streamer->_self) { + streamer->_self = TypeStreamerPointer(streamer); + } getTypeStreamers().insert(type, streamer); return 0; } @@ -109,13 +117,14 @@ QList Bitstream::getMetaObjectSubClasses(const QMetaObject* return getMetaObjectSubClasses().values(metaObject); } -Bitstream::Bitstream(QDataStream& underlying, MetadataType metadataType, QObject* parent) : +Bitstream::Bitstream(QDataStream& underlying, MetadataType metadataType, GenericsMode genericsMode, QObject* parent) : QObject(parent), _underlying(underlying), _byte(0), _position(0), _metadataType(metadataType), - _metaObjectStreamer(*this), + _genericsMode(genericsMode), + _objectStreamerStreamer(*this), _typeStreamerStreamer(*this), _attributeStreamer(*this), _scriptStringStreamer(*this), @@ -189,7 +198,7 @@ void Bitstream::reset() { } Bitstream::WriteMappings Bitstream::getAndResetWriteMappings() { - WriteMappings mappings = { _metaObjectStreamer.getAndResetTransientOffsets(), + WriteMappings mappings = { _objectStreamerStreamer.getAndResetTransientOffsets(), _typeStreamerStreamer.getAndResetTransientOffsets(), _attributeStreamer.getAndResetTransientOffsets(), _scriptStringStreamer.getAndResetTransientOffsets(), @@ -198,7 +207,7 @@ Bitstream::WriteMappings Bitstream::getAndResetWriteMappings() { } void Bitstream::persistWriteMappings(const WriteMappings& mappings) { - _metaObjectStreamer.persistTransientOffsets(mappings.metaObjectOffsets); + _objectStreamerStreamer.persistTransientOffsets(mappings.objectStreamerOffsets); _typeStreamerStreamer.persistTransientOffsets(mappings.typeStreamerOffsets); _attributeStreamer.persistTransientOffsets(mappings.attributeOffsets); _scriptStringStreamer.persistTransientOffsets(mappings.scriptStringOffsets); @@ -226,7 +235,7 @@ void Bitstream::persistAndResetWriteMappings() { } Bitstream::ReadMappings Bitstream::getAndResetReadMappings() { - ReadMappings mappings = { _metaObjectStreamer.getAndResetTransientValues(), + ReadMappings mappings = { _objectStreamerStreamer.getAndResetTransientValues(), _typeStreamerStreamer.getAndResetTransientValues(), _attributeStreamer.getAndResetTransientValues(), _scriptStringStreamer.getAndResetTransientValues(), @@ -235,7 +244,7 @@ Bitstream::ReadMappings Bitstream::getAndResetReadMappings() { } void Bitstream::persistReadMappings(const ReadMappings& mappings) { - _metaObjectStreamer.persistTransientValues(mappings.metaObjectValues); + _objectStreamerStreamer.persistTransientValues(mappings.objectStreamerValues); _typeStreamerStreamer.persistTransientValues(mappings.typeStreamerValues); _attributeStreamer.persistTransientValues(mappings.attributeValues); _scriptStringStreamer.persistTransientValues(mappings.scriptStringValues); @@ -294,27 +303,27 @@ void Bitstream::writeRawDelta(const QVariant& value, const QVariant& reference) } void Bitstream::readRawDelta(QVariant& value, const QVariant& reference) { - TypeReader typeReader; - _typeStreamerStreamer >> typeReader; - typeReader.readRawDelta(*this, value, reference); + TypeStreamerPointer typeStreamer; + _typeStreamerStreamer >> typeStreamer; + typeStreamer->readRawDelta(*this, value, reference); } void Bitstream::writeRawDelta(const QObject* value, const QObject* reference) { if (!value) { - _metaObjectStreamer << NULL; + _objectStreamerStreamer << NULL; return; } const QMetaObject* metaObject = value->metaObject(); - _metaObjectStreamer << metaObject; - foreach (const PropertyWriter& propertyWriter, getPropertyWriters().value(metaObject)) { - propertyWriter.writeDelta(*this, value, reference); - } + const ObjectStreamer* streamer = (metaObject == &GenericSharedObject::staticMetaObject) ? + static_cast(value)->getStreamer().data() : getObjectStreamers().value(metaObject); + _objectStreamerStreamer << streamer; + streamer->writeRawDelta(*this, value, reference); } void Bitstream::readRawDelta(QObject*& value, const QObject* reference) { - ObjectReader objectReader; - _metaObjectStreamer >> objectReader; - value = objectReader.readDelta(*this, reference); + ObjectStreamerPointer streamer; + _objectStreamerStreamer >> streamer; + value = streamer ? streamer->readRawDelta(*this, reference) : NULL; } void Bitstream::writeRawDelta(const QScriptValue& value, const QScriptValue& reference) { @@ -751,8 +760,7 @@ Bitstream& Bitstream::operator<<(const QVariant& value) { } const TypeStreamer* streamer = getTypeStreamers().value(value.userType()); if (streamer) { - _typeStreamerStreamer << streamer; - streamer->write(*this, value); + streamer->writeVariant(*this, value); } else { qWarning() << "Non-streamable type: " << value.typeName() << "\n"; } @@ -760,12 +768,12 @@ Bitstream& Bitstream::operator<<(const QVariant& value) { } Bitstream& Bitstream::operator>>(QVariant& value) { - TypeReader reader; - _typeStreamerStreamer >> reader; - if (reader.getTypeName().isEmpty()) { + TypeStreamerPointer streamer; + _typeStreamerStreamer >> streamer; + if (!streamer) { value = QVariant(); } else { - value = reader.read(*this); + value = streamer->readVariant(*this); } return *this; } @@ -793,40 +801,62 @@ Bitstream& Bitstream::operator>>(OwnedAttributeValue& attributeValue) { return *this; } +Bitstream& Bitstream::operator<<(const GenericValue& value) { + value.getStreamer()->write(*this, value.getValue()); + return *this; +} + +Bitstream& Bitstream::operator>>(GenericValue& value) { + value = GenericValue(); + return *this; +} + Bitstream& Bitstream::operator<<(const QObject* object) { if (!object) { - _metaObjectStreamer << NULL; + _objectStreamerStreamer << NULL; return *this; } const QMetaObject* metaObject = object->metaObject(); - _metaObjectStreamer << metaObject; - foreach (const PropertyWriter& propertyWriter, getPropertyWriters().value(metaObject)) { - propertyWriter.write(*this, object); - } + const ObjectStreamer* streamer = (metaObject == &GenericSharedObject::staticMetaObject) ? + static_cast(object)->getStreamer().data() : getObjectStreamers().value(metaObject); + _objectStreamerStreamer << streamer; + streamer->write(*this, object); return *this; } Bitstream& Bitstream::operator>>(QObject*& object) { - ObjectReader objectReader; - _metaObjectStreamer >> objectReader; - object = objectReader.read(*this); + ObjectStreamerPointer streamer; + _objectStreamerStreamer >> streamer; + object = streamer ? streamer->read(*this) : NULL; return *this; } Bitstream& Bitstream::operator<<(const QMetaObject* metaObject) { - _metaObjectStreamer << metaObject; + _objectStreamerStreamer << getObjectStreamers().value(metaObject); return *this; } Bitstream& Bitstream::operator>>(const QMetaObject*& metaObject) { - ObjectReader objectReader; - _metaObjectStreamer >> objectReader; - metaObject = objectReader.getMetaObject(); + ObjectStreamerPointer streamer; + _objectStreamerStreamer >> streamer; + metaObject = streamer->getMetaObject(); return *this; } -Bitstream& Bitstream::operator>>(ObjectReader& objectReader) { - _metaObjectStreamer >> objectReader; +Bitstream& Bitstream::operator<<(const ObjectStreamer* streamer) { + _objectStreamerStreamer << streamer; + return *this; +} + +Bitstream& Bitstream::operator>>(const ObjectStreamer*& streamer) { + ObjectStreamerPointer objectStreamer; + _objectStreamerStreamer >> objectStreamer; + streamer = objectStreamer.data(); + return *this; +} + +Bitstream& Bitstream::operator>>(ObjectStreamerPointer& streamer) { + _objectStreamerStreamer >> streamer; return *this; } @@ -836,14 +866,14 @@ Bitstream& Bitstream::operator<<(const TypeStreamer* streamer) { } Bitstream& Bitstream::operator>>(const TypeStreamer*& streamer) { - TypeReader typeReader; - _typeStreamerStreamer >> typeReader; - streamer = typeReader.getStreamer(); + TypeStreamerPointer typeStreamer; + _typeStreamerStreamer >> typeStreamer; + streamer = typeStreamer.data(); return *this; } -Bitstream& Bitstream::operator>>(TypeReader& reader) { - _typeStreamerStreamer >> reader; +Bitstream& Bitstream::operator>>(TypeStreamerPointer& streamer) { + _typeStreamerStreamer >> streamer; return *this; } @@ -1056,57 +1086,55 @@ Bitstream& Bitstream::operator>>(SharedObjectPointer& object) { return *this; } -Bitstream& Bitstream::operator<(const QMetaObject* metaObject) { - if (!metaObject) { +Bitstream& Bitstream::operator<(const ObjectStreamer* streamer) { + if (!streamer) { return *this << QByteArray(); } - *this << QByteArray::fromRawData(metaObject->className(), strlen(metaObject->className())); - if (_metadataType == NO_METADATA) { - return *this; - } - const PropertyWriterVector& propertyWriters = getPropertyWriters().value(metaObject); - *this << propertyWriters.size(); - QCryptographicHash hash(QCryptographicHash::Md5); - foreach (const PropertyWriter& propertyWriter, propertyWriters) { - _typeStreamerStreamer << propertyWriter.getStreamer(); - const QMetaProperty& property = propertyWriter.getProperty(); - if (_metadataType == FULL_METADATA) { - *this << QByteArray::fromRawData(property.name(), strlen(property.name())); - } else { - hash.addData(property.name(), strlen(property.name()) + 1); - } - } - if (_metadataType == HASH_METADATA) { - QByteArray hashResult = hash.result(); - write(hashResult.constData(), hashResult.size() * BITS_IN_BYTE); + const char* name = streamer->getName(); + *this << QByteArray::fromRawData(name, strlen(name)); + if (_metadataType != NO_METADATA) { + streamer->writeMetadata(*this, _metadataType == FULL_METADATA); } return *this; } -Bitstream& Bitstream::operator>(ObjectReader& objectReader) { +Bitstream& Bitstream::operator>(ObjectStreamerPointer& streamer) { QByteArray className; *this >> className; if (className.isEmpty()) { - objectReader = ObjectReader(); + streamer = ObjectStreamerPointer(); return *this; } const QMetaObject* metaObject = _metaObjectSubstitutions.value(className); if (!metaObject) { metaObject = getMetaObjects().value(className); } - if (!metaObject) { - qWarning() << "Unknown class name: " << className << "\n"; + // start out with the streamer for the named class, if any + if (metaObject) { + streamer = getObjectStreamers().value(metaObject)->getSelf(); + } else { + streamer = ObjectStreamerPointer(); } if (_metadataType == NO_METADATA) { - objectReader = ObjectReader(className, metaObject, getPropertyReaders().value(metaObject)); + if (!metaObject) { + qWarning() << "Unknown class name:" << className; + } return *this; } - int storedPropertyCount; - *this >> storedPropertyCount; - PropertyReaderVector properties(storedPropertyCount); - for (int i = 0; i < storedPropertyCount; i++) { - TypeReader typeReader; - *this >> typeReader; + if (_genericsMode == ALL_GENERICS) { + streamer = readGenericObjectStreamer(className); + return *this; + } + if (!metaObject && _genericsMode == FALLBACK_GENERICS) { + streamer = readGenericObjectStreamer(className); + return *this; + } + int propertyCount; + *this >> propertyCount; + QVector properties(propertyCount); + for (int i = 0; i < propertyCount; i++) { + TypeStreamerPointer typeStreamer; + *this >> typeStreamer; QMetaProperty property = QMetaProperty(); if (_metadataType == FULL_METADATA) { QByteArray propertyName; @@ -1115,23 +1143,22 @@ Bitstream& Bitstream::operator>(ObjectReader& objectReader) { property = metaObject->property(metaObject->indexOfProperty(propertyName)); } } - properties[i] = PropertyReader(typeReader, property); + properties[i] = StreamerPropertyPair(typeStreamer, property); } // for hash metadata, check the names/types of the properties as well as the name hash against our own class if (_metadataType == HASH_METADATA) { QCryptographicHash hash(QCryptographicHash::Md5); bool matches = true; if (metaObject) { - const PropertyWriterVector& propertyWriters = getPropertyWriters().value(metaObject); - if (propertyWriters.size() == properties.size()) { - for (int i = 0; i < propertyWriters.size(); i++) { - const PropertyWriter& propertyWriter = propertyWriters.at(i); - if (!properties.at(i).getReader().matchesExactly(propertyWriter.getStreamer())) { + const QVector& localProperties = streamer->getProperties(); + if (localProperties.size() == properties.size()) { + for (int i = 0; i < localProperties.size(); i++) { + const StreamerPropertyPair& localProperty = localProperties.at(i); + if (localProperty.first != properties.at(i).first) { matches = false; break; } - const QMetaProperty& property = propertyWriter.getProperty(); - hash.addData(property.name(), strlen(property.name()) + 1); + hash.addData(localProperty.second.name(), strlen(localProperty.second.name()) + 1); } } else { matches = false; @@ -1141,11 +1168,24 @@ Bitstream& Bitstream::operator>(ObjectReader& objectReader) { QByteArray remoteHashResult(localHashResult.size(), 0); read(remoteHashResult.data(), remoteHashResult.size() * BITS_IN_BYTE); if (metaObject && matches && localHashResult == remoteHashResult) { - objectReader = ObjectReader(className, metaObject, getPropertyReaders().value(metaObject)); return *this; } + } else if (metaObject) { + const QVector& localProperties = streamer->getProperties(); + if (localProperties.size() != properties.size()) { + streamer = ObjectStreamerPointer(new MappedObjectStreamer(metaObject, properties)); + return *this; + } + for (int i = 0; i < localProperties.size(); i++) { + const StreamerPropertyPair& property = properties.at(i); + if (localProperties.at(i).first != property.first || property.second.propertyIndex() != i) { + streamer = ObjectStreamerPointer(new MappedObjectStreamer(metaObject, properties)); + return *this; + } + } + return *this; } - objectReader = ObjectReader(className, metaObject, properties); + streamer = ObjectStreamerPointer(new MappedObjectStreamer(metaObject, properties)); return *this; } @@ -1156,102 +1196,64 @@ Bitstream& Bitstream::operator<(const TypeStreamer* streamer) { } const char* typeName = streamer->getName(); *this << QByteArray::fromRawData(typeName, strlen(typeName)); - if (_metadataType == NO_METADATA) { - return *this; - } - TypeReader::Type type = streamer->getReaderType(); - *this << (int)type; - switch (type) { - case TypeReader::SIMPLE_TYPE: - return *this; - - case TypeReader::ENUM_TYPE: { - QMetaEnum metaEnum = streamer->getMetaEnum(); - if (_metadataType == FULL_METADATA) { - *this << metaEnum.keyCount(); - for (int i = 0; i < metaEnum.keyCount(); i++) { - *this << QByteArray::fromRawData(metaEnum.key(i), strlen(metaEnum.key(i))); - *this << metaEnum.value(i); - } - } else { - *this << streamer->getBits(); - QCryptographicHash hash(QCryptographicHash::Md5); - for (int i = 0; i < metaEnum.keyCount(); i++) { - hash.addData(metaEnum.key(i), strlen(metaEnum.key(i)) + 1); - qint32 value = metaEnum.value(i); - hash.addData((const char*)&value, sizeof(qint32)); - } - QByteArray hashResult = hash.result(); - write(hashResult.constData(), hashResult.size() * BITS_IN_BYTE); - } - return *this; - } - case TypeReader::LIST_TYPE: - case TypeReader::SET_TYPE: - return *this << streamer->getValueStreamer(); - - case TypeReader::MAP_TYPE: - return *this << streamer->getKeyStreamer() << streamer->getValueStreamer(); - - default: - break; // fall through - } - // streamable type - const QVector& metaFields = streamer->getMetaFields(); - *this << metaFields.size(); - if (metaFields.isEmpty()) { - return *this; - } - QCryptographicHash hash(QCryptographicHash::Md5); - foreach (const MetaField& metaField, metaFields) { - _typeStreamerStreamer << metaField.getStreamer(); - if (_metadataType == FULL_METADATA) { - *this << metaField.getName(); - } else { - hash.addData(metaField.getName().constData(), metaField.getName().size() + 1); - } - } - if (_metadataType == HASH_METADATA) { - QByteArray hashResult = hash.result(); - write(hashResult.constData(), hashResult.size() * BITS_IN_BYTE); - } + if (_metadataType != NO_METADATA) { + *this << (int)streamer->getCategory(); + streamer->writeMetadata(*this, _metadataType == FULL_METADATA); + } return *this; } -Bitstream& Bitstream::operator>(TypeReader& reader) { +Bitstream& Bitstream::operator>(TypeStreamerPointer& streamer) { QByteArray typeName; *this >> typeName; if (typeName.isEmpty()) { - reader = TypeReader(); + streamer = TypeStreamerPointer(); return *this; } - const TypeStreamer* streamer = _typeStreamerSubstitutions.value(typeName); - if (!streamer) { - streamer = getTypeStreamers().value(QMetaType::type(typeName.constData())); - if (!streamer) { - streamer = getEnumStreamersByName().value(typeName); + const TypeStreamer* baseStreamer = _typeStreamerSubstitutions.value(typeName); + if (!baseStreamer) { + baseStreamer = getTypeStreamers().value(QMetaType::type(typeName.constData())); + if (!baseStreamer) { + baseStreamer = getEnumStreamersByName().value(typeName); } } - if (!streamer) { - qWarning() << "Unknown type name: " << typeName << "\n"; + // start out with the base, if any + if (baseStreamer) { + streamer = baseStreamer->getSelf(); + } else { + streamer = TypeStreamerPointer(); } if (_metadataType == NO_METADATA) { - reader = TypeReader(typeName, streamer); + if (!baseStreamer) { + qWarning() << "Unknown type name:" << typeName; + } return *this; } - int type; - *this >> type; - switch (type) { - case TypeReader::SIMPLE_TYPE: - reader = TypeReader(typeName, streamer); + int category; + *this >> category; + if (category == TypeStreamer::SIMPLE_CATEGORY) { + if (!streamer) { + qWarning() << "Unknown type name:" << typeName; + } + return *this; + } + if (_genericsMode == ALL_GENERICS) { + streamer = readGenericTypeStreamer(typeName, category); + return *this; + } + if (!baseStreamer) { + if (_genericsMode == FALLBACK_GENERICS) { + streamer = readGenericTypeStreamer(typeName, category); return *this; - - case TypeReader::ENUM_TYPE: { + } + baseStreamer = getInvalidTypeStreamer(); + } + switch (category) { + case TypeStreamer::ENUM_CATEGORY: { if (_metadataType == FULL_METADATA) { int keyCount; *this >> keyCount; - QMetaEnum metaEnum = (streamer && streamer->getReaderType() == TypeReader::ENUM_TYPE) ? - streamer->getMetaEnum() : QMetaEnum(); + QMetaEnum metaEnum = baseStreamer->getMetaEnum(); QHash mappings; bool matches = (keyCount == metaEnum.keyCount()); int highestValue = 0; @@ -1266,17 +1268,16 @@ Bitstream& Bitstream::operator>(TypeReader& reader) { } matches &= (value == localValue); } - if (matches) { - reader = TypeReader(typeName, streamer); - } else { - reader = TypeReader(typeName, streamer, getBitsForHighestValue(highestValue), mappings); + if (!matches) { + streamer = TypeStreamerPointer(new MappedEnumTypeStreamer(baseStreamer, + getBitsForHighestValue(highestValue), mappings)); } } else { int bits; *this >> bits; QCryptographicHash hash(QCryptographicHash::Md5); - if (streamer && streamer->getReaderType() == TypeReader::ENUM_TYPE) { - QMetaEnum metaEnum = streamer->getMetaEnum(); + if (baseStreamer->getCategory() == TypeStreamer::ENUM_CATEGORY) { + QMetaEnum metaEnum = baseStreamer->getMetaEnum(); for (int i = 0; i < metaEnum.keyCount(); i++) { hash.addData(metaEnum.key(i), strlen(metaEnum.key(i)) + 1); qint32 value = metaEnum.value(i); @@ -1286,37 +1287,29 @@ Bitstream& Bitstream::operator>(TypeReader& reader) { QByteArray localHashResult = hash.result(); QByteArray remoteHashResult(localHashResult.size(), 0); read(remoteHashResult.data(), remoteHashResult.size() * BITS_IN_BYTE); - if (localHashResult == remoteHashResult) { - reader = TypeReader(typeName, streamer); - } else { - reader = TypeReader(typeName, streamer, bits, QHash()); + if (localHashResult != remoteHashResult) { + streamer = TypeStreamerPointer(new MappedEnumTypeStreamer(baseStreamer, bits, QHash())); } } return *this; } - case TypeReader::LIST_TYPE: - case TypeReader::SET_TYPE: { - TypeReader valueReader; - *this >> valueReader; - if (streamer && streamer->getReaderType() == type && - valueReader.matchesExactly(streamer->getValueStreamer())) { - reader = TypeReader(typeName, streamer); - } else { - reader = TypeReader(typeName, streamer, (TypeReader::Type)type, - TypeReaderPointer(new TypeReader(valueReader))); + case TypeStreamer::LIST_CATEGORY: + case TypeStreamer::SET_CATEGORY: { + TypeStreamerPointer valueStreamer; + *this >> valueStreamer; + if (!(baseStreamer->getCategory() == category && valueStreamer == baseStreamer->getValueStreamer())) { + streamer = TypeStreamerPointer(category == TypeStreamer::LIST_CATEGORY ? + new MappedListTypeStreamer(baseStreamer, valueStreamer) : + new MappedSetTypeStreamer(baseStreamer, valueStreamer)); } return *this; } - case TypeReader::MAP_TYPE: { - TypeReader keyReader, valueReader; - *this >> keyReader >> valueReader; - if (streamer && streamer->getReaderType() == TypeReader::MAP_TYPE && - keyReader.matchesExactly(streamer->getKeyStreamer()) && - valueReader.matchesExactly(streamer->getValueStreamer())) { - reader = TypeReader(typeName, streamer); - } else { - reader = TypeReader(typeName, streamer, TypeReaderPointer(new TypeReader(keyReader)), - TypeReaderPointer(new TypeReader(valueReader))); + case TypeStreamer::MAP_CATEGORY: { + TypeStreamerPointer keyStreamer, valueStreamer; + *this >> keyStreamer >> valueStreamer; + if (!(baseStreamer->getCategory() == TypeStreamer::MAP_CATEGORY && + keyStreamer == baseStreamer->getKeyStreamer() && valueStreamer == baseStreamer->getValueStreamer())) { + streamer = TypeStreamerPointer(new MappedMapTypeStreamer(baseStreamer, keyStreamer, valueStreamer)); } return *this; } @@ -1324,70 +1317,60 @@ Bitstream& Bitstream::operator>(TypeReader& reader) { // streamable type int fieldCount; *this >> fieldCount; - QVector fields(fieldCount); + QVector fields(fieldCount); for (int i = 0; i < fieldCount; i++) { - TypeReader typeReader; - *this >> typeReader; + TypeStreamerPointer typeStreamer; + *this >> typeStreamer; int index = -1; if (_metadataType == FULL_METADATA) { QByteArray fieldName; *this >> fieldName; - if (streamer) { - index = streamer->getFieldIndex(fieldName); - } + index = baseStreamer->getFieldIndex(fieldName); } - fields[i] = FieldReader(typeReader, index); + fields[i] = StreamerIndexPair(typeStreamer, index); } // for hash metadata, check the names/types of the fields as well as the name hash against our own class if (_metadataType == HASH_METADATA) { QCryptographicHash hash(QCryptographicHash::Md5); bool matches = true; - if (streamer) { - const QVector& localFields = streamer->getMetaFields(); - if (fieldCount != localFields.size()) { - matches = false; - - } else { - if (fieldCount == 0) { - reader = TypeReader(typeName, streamer); - return *this; - } - for (int i = 0; i < fieldCount; i++) { - const MetaField& localField = localFields.at(i); - if (!fields.at(i).getReader().matchesExactly(localField.getStreamer())) { - matches = false; - break; - } - hash.addData(localField.getName().constData(), localField.getName().size() + 1); - } + const QVector& localFields = baseStreamer->getMetaFields(); + if (fieldCount != localFields.size()) { + matches = false; + + } else { + if (fieldCount == 0) { + return *this; } + for (int i = 0; i < fieldCount; i++) { + const MetaField& localField = localFields.at(i); + if (fields.at(i).first != localField.getStreamer()) { + matches = false; + break; + } + hash.addData(localField.getName().constData(), localField.getName().size() + 1); + } } QByteArray localHashResult = hash.result(); QByteArray remoteHashResult(localHashResult.size(), 0); read(remoteHashResult.data(), remoteHashResult.size() * BITS_IN_BYTE); - if (streamer && matches && localHashResult == remoteHashResult) { + if (matches && localHashResult == remoteHashResult) { // since everything is the same, we can use the default streamer - reader = TypeReader(typeName, streamer); return *this; } - } else if (streamer) { - // if all fields are the same type and in the right order, we can use the (more efficient) default streamer - const QVector& localFields = streamer->getMetaFields(); - if (fieldCount != localFields.size()) { - reader = TypeReader(typeName, streamer, fields); - return *this; - } - for (int i = 0; i < fieldCount; i++) { - const FieldReader& fieldReader = fields.at(i); - if (!fieldReader.getReader().matchesExactly(localFields.at(i).getStreamer()) || fieldReader.getIndex() != i) { - reader = TypeReader(typeName, streamer, fields); - return *this; - } - } - reader = TypeReader(typeName, streamer); + } + // if all fields are the same type and in the right order, we can use the (more efficient) default streamer + const QVector& localFields = baseStreamer->getMetaFields(); + if (fieldCount != localFields.size()) { + streamer = TypeStreamerPointer(new MappedStreamableTypeStreamer(baseStreamer, fields)); return *this; } - reader = TypeReader(typeName, streamer, fields); + for (int i = 0; i < fieldCount; i++) { + const StreamerIndexPair& field = fields.at(i); + if (field.first != localFields.at(i).getStreamer() || field.second != i) { + streamer = TypeStreamerPointer(new MappedStreamableTypeStreamer(baseStreamer, fields)); + return *this; + } + } return *this; } @@ -1443,12 +1426,12 @@ Bitstream& Bitstream::operator>(SharedObjectPointer& object) { QPointer reference = _sharedObjectReferences.value(originID); QPointer& pointer = _weakSharedObjectHash[id]; if (pointer) { - ObjectReader objectReader; - _metaObjectStreamer >> objectReader; + ObjectStreamerPointer objectStreamer; + _objectStreamerStreamer >> objectStreamer; if (reference) { - objectReader.readDelta(*this, reference.data(), pointer.data()); + objectStreamer->readRawDelta(*this, reference.data(), pointer.data()); } else { - objectReader.read(*this, pointer.data()); + objectStreamer->read(*this, pointer.data()); } } else { QObject* rawObject; @@ -1481,6 +1464,106 @@ void Bitstream::clearSharedObject(QObject* object) { } } +const int MD5_HASH_SIZE = 16; + +ObjectStreamerPointer Bitstream::readGenericObjectStreamer(const QByteArray& name) { + int propertyCount; + *this >> propertyCount; + QVector properties(propertyCount); + QByteArray hash; + if (propertyCount > 0) { + for (int i = 0; i < propertyCount; i++) { + TypeStreamerPointer streamer; + *this >> streamer; + QByteArray name; + if (_metadataType == FULL_METADATA) { + *this >> name; + } + properties[i] = StreamerNamePair(streamer, name); + } + if (_metadataType == HASH_METADATA) { + hash.resize(MD5_HASH_SIZE); + read(hash.data(), hash.size() * BITS_IN_BYTE); + } + } + ObjectStreamerPointer streamer = ObjectStreamerPointer(new GenericObjectStreamer(name, properties, hash)); + static_cast(streamer.data())->_weakSelf = streamer; + return streamer; +} + +TypeStreamerPointer Bitstream::readGenericTypeStreamer(const QByteArray& name, int category) { + TypeStreamerPointer streamer; + switch (category) { + case TypeStreamer::ENUM_CATEGORY: { + QVector values; + int bits; + QByteArray hash; + if (_metadataType == FULL_METADATA) { + int keyCount; + *this >> keyCount; + values.resize(keyCount); + int highestValue = 0; + for (int i = 0; i < keyCount; i++) { + QByteArray name; + int value; + *this >> name >> value; + values[i] = NameIntPair(name, value); + highestValue = qMax(highestValue, value); + } + bits = getBitsForHighestValue(highestValue); + + } else { + *this >> bits; + hash.resize(MD5_HASH_SIZE); + read(hash.data(), hash.size() * BITS_IN_BYTE); + } + streamer = TypeStreamerPointer(new GenericEnumTypeStreamer(name, values, bits, hash)); + break; + } + case TypeStreamer::STREAMABLE_CATEGORY: { + int fieldCount; + *this >> fieldCount; + QVector fields(fieldCount); + QByteArray hash; + if (fieldCount == 0) { + streamer = TypeStreamerPointer(new GenericStreamableTypeStreamer(name, fields, hash)); + break; + } + for (int i = 0; i < fieldCount; i++) { + TypeStreamerPointer streamer; + *this >> streamer; + QByteArray name; + if (_metadataType == FULL_METADATA) { + *this >> name; + } + fields[i] = StreamerNamePair(streamer, name); + } + if (_metadataType == HASH_METADATA) { + hash.resize(MD5_HASH_SIZE); + read(hash.data(), hash.size() * BITS_IN_BYTE); + } + streamer = TypeStreamerPointer(new GenericStreamableTypeStreamer(name, fields, hash)); + break; + } + case TypeStreamer::LIST_CATEGORY: + case TypeStreamer::SET_CATEGORY: { + TypeStreamerPointer valueStreamer; + *this >> valueStreamer; + streamer = TypeStreamerPointer(category == TypeStreamer::LIST_CATEGORY ? + new GenericListTypeStreamer(name, valueStreamer) : new GenericSetTypeStreamer(name, valueStreamer)); + break; + } + case TypeStreamer::MAP_CATEGORY: { + TypeStreamerPointer keyStreamer, valueStreamer; + *this >> keyStreamer >> valueStreamer; + streamer = TypeStreamerPointer(new GenericMapTypeStreamer(name, keyStreamer, valueStreamer)); + break; + } + } + static_cast(streamer.data())->_weakSelf = streamer; + return streamer; +} + QHash& Bitstream::getMetaObjects() { static QHash metaObjects; return metaObjects; @@ -1491,6 +1574,40 @@ QMultiHash& Bitstream::getMetaObjectSubC return metaObjectSubClasses; } +const QHash& Bitstream::getObjectStreamers() { + static QHash objectStreamers = createObjectStreamers(); + return objectStreamers; +} + +QHash Bitstream::createObjectStreamers() { + QHash objectStreamers; + foreach (const QMetaObject* metaObject, getMetaObjects()) { + QVector properties; + for (int i = 0; i < metaObject->propertyCount(); i++) { + QMetaProperty property = metaObject->property(i); + if (!property.isStored()) { + continue; + } + const TypeStreamer* streamer; + if (property.isEnumType()) { + QMetaEnum metaEnum = property.enumerator(); + streamer = getEnumStreamers().value(ScopeNamePair( + QByteArray::fromRawData(metaEnum.scope(), strlen(metaEnum.scope())), + QByteArray::fromRawData(metaEnum.name(), strlen(metaEnum.name())))); + } else { + streamer = getTypeStreamers().value(property.userType()); + } + if (streamer) { + properties.append(StreamerPropertyPair(streamer->getSelf(), property)); + } + } + ObjectStreamerPointer streamer = ObjectStreamerPointer(new MappedObjectStreamer(metaObject, properties)); + streamer->_self = streamer; + objectStreamers.insert(metaObject, streamer.data()); + } + return objectStreamers; +} + QHash& Bitstream::getTypeStreamers() { static QHash typeStreamers; return typeStreamers; @@ -1528,374 +1645,181 @@ QHash Bitstream::createEnumStreamersByName() { return enumStreamersByName; } -const QHash& Bitstream::getPropertyReaders() { - static QHash propertyReaders = createPropertyReaders(); - return propertyReaders; +const TypeStreamer* Bitstream::getInvalidTypeStreamer() { + const TypeStreamer* streamer = createInvalidTypeStreamer(); + return streamer; } -QHash Bitstream::createPropertyReaders() { - QHash propertyReaders; - foreach (const QMetaObject* metaObject, getMetaObjects()) { - PropertyReaderVector& readers = propertyReaders[metaObject]; - for (int i = 0; i < metaObject->propertyCount(); i++) { - QMetaProperty property = metaObject->property(i); - if (!property.isStored()) { - continue; - } - const TypeStreamer* streamer; - if (property.isEnumType()) { - QMetaEnum metaEnum = property.enumerator(); - streamer = getEnumStreamers().value(ScopeNamePair( - QByteArray::fromRawData(metaEnum.scope(), strlen(metaEnum.scope())), - QByteArray::fromRawData(metaEnum.name(), strlen(metaEnum.name())))); - } else { - streamer = getTypeStreamers().value(property.userType()); - } - if (streamer) { - readers.append(PropertyReader(TypeReader(QByteArray(), streamer), property)); - } - } - } - return propertyReaders; +const TypeStreamer* Bitstream::createInvalidTypeStreamer() { + TypeStreamer* streamer = new TypeStreamer(); + streamer->_type = QMetaType::UnknownType; + streamer->_self = TypeStreamerPointer(streamer); + return streamer; } -const QHash& Bitstream::getPropertyWriters() { - static QHash propertyWriters = createPropertyWriters(); - return propertyWriters; +ObjectStreamer::ObjectStreamer(const QMetaObject* metaObject) : + _metaObject(metaObject) { } -QHash Bitstream::createPropertyWriters() { - QHash propertyWriters; - foreach (const QMetaObject* metaObject, getMetaObjects()) { - PropertyWriterVector& writers = propertyWriters[metaObject]; - for (int i = 0; i < metaObject->propertyCount(); i++) { - QMetaProperty property = metaObject->property(i); - if (!property.isStored()) { - continue; - } - const TypeStreamer* streamer; - if (property.isEnumType()) { - QMetaEnum metaEnum = property.enumerator(); - streamer = getEnumStreamers().value(ScopeNamePair( - QByteArray::fromRawData(metaEnum.scope(), strlen(metaEnum.scope())), - QByteArray::fromRawData(metaEnum.name(), strlen(metaEnum.name())))); - } else { - streamer = getTypeStreamers().value(property.userType()); - } - if (streamer) { - writers.append(PropertyWriter(property, streamer)); - } - } - } - return propertyWriters; +const QVector& ObjectStreamer::getProperties() const { + static QVector emptyProperties; + return emptyProperties; } -TypeReader::TypeReader(const QByteArray& typeName, const TypeStreamer* streamer) : - _typeName(typeName), - _streamer(streamer), - _exactMatch(true) { -} - -TypeReader::TypeReader(const QByteArray& typeName, const TypeStreamer* streamer, int bits, const QHash& mappings) : - _typeName(typeName), - _streamer(streamer), - _exactMatch(false), - _type(ENUM_TYPE), - _bits(bits), - _mappings(mappings) { -} - -TypeReader::TypeReader(const QByteArray& typeName, const TypeStreamer* streamer, const QVector& fields) : - _typeName(typeName), - _streamer(streamer), - _exactMatch(false), - _type(STREAMABLE_TYPE), - _fields(fields) { -} - -TypeReader::TypeReader(const QByteArray& typeName, const TypeStreamer* streamer, - Type type, const TypeReaderPointer& valueReader) : - _typeName(typeName), - _streamer(streamer), - _exactMatch(false), - _type(type), - _valueReader(valueReader) { -} - -TypeReader::TypeReader(const QByteArray& typeName, const TypeStreamer* streamer, - const TypeReaderPointer& keyReader, const TypeReaderPointer& valueReader) : - _typeName(typeName), - _streamer(streamer), - _exactMatch(false), - _type(MAP_TYPE), - _keyReader(keyReader), - _valueReader(valueReader) { -} - -QVariant TypeReader::read(Bitstream& in) const { - if (_exactMatch) { - return _streamer->read(in); - } - QVariant object = _streamer ? QVariant(_streamer->getType(), 0) : QVariant(); - switch (_type) { - case ENUM_TYPE: { - int value = 0; - in.read(&value, _bits); - if (_streamer) { - _streamer->setEnumValue(object, value, _mappings); - } - break; - } - case STREAMABLE_TYPE: { - foreach (const FieldReader& field, _fields) { - field.read(in, _streamer, object); - } - break; - } - case LIST_TYPE: - case SET_TYPE: { - int size; - in >> size; - for (int i = 0; i < size; i++) { - QVariant value = _valueReader->read(in); - if (_streamer) { - _streamer->insert(object, value); - } - } - break; - } - case MAP_TYPE: { - int size; - in >> size; - for (int i = 0; i < size; i++) { - QVariant key = _keyReader->read(in); - QVariant value = _valueReader->read(in); - if (_streamer) { - _streamer->insert(object, key, value); - } - } - break; - } - default: - break; - } - return object; -} - -void TypeReader::readDelta(Bitstream& in, QVariant& object, const QVariant& reference) const { - if (_exactMatch) { - _streamer->readDelta(in, object, reference); - return; - } - bool changed; - in >> changed; - if (changed) { - readRawDelta(in, object, reference); - } else { - object = reference; - } -} - -void TypeReader::readRawDelta(Bitstream& in, QVariant& object, const QVariant& reference) const { - if (_exactMatch) { - _streamer->readRawDelta(in, object, reference); - return; - } - switch (_type) { - case ENUM_TYPE: { - int value = 0; - in.read(&value, _bits); - if (_streamer) { - _streamer->setEnumValue(object, value, _mappings); - } - break; - } - case STREAMABLE_TYPE: { - foreach (const FieldReader& field, _fields) { - field.readDelta(in, _streamer, object, reference); - } - break; - } - case LIST_TYPE: { - object = reference; - int size, referenceSize; - in >> size >> referenceSize; - if (_streamer) { - if (size < referenceSize) { - _streamer->prune(object, size); - } - for (int i = 0; i < size; i++) { - if (i < referenceSize) { - QVariant value; - _valueReader->readDelta(in, value, _streamer->getValue(reference, i)); - _streamer->setValue(object, i, value); - } else { - _streamer->insert(object, _valueReader->read(in)); - } - } - } else { - for (int i = 0; i < size; i++) { - if (i < referenceSize) { - QVariant value; - _valueReader->readDelta(in, value, QVariant()); - } else { - _valueReader->read(in); - } - } - } - break; - } - case SET_TYPE: { - object = reference; - int addedOrRemoved; - in >> addedOrRemoved; - for (int i = 0; i < addedOrRemoved; i++) { - QVariant value = _valueReader->read(in); - if (_streamer && !_streamer->remove(object, value)) { - _streamer->insert(object, value); - } - } - break; - } - case MAP_TYPE: { - object = reference; - int added; - in >> added; - for (int i = 0; i < added; i++) { - QVariant key = _keyReader->read(in); - QVariant value = _valueReader->read(in); - if (_streamer) { - _streamer->insert(object, key, value); - } - } - int modified; - in >> modified; - for (int i = 0; i < modified; i++) { - QVariant key = _keyReader->read(in); - QVariant value; - if (_streamer) { - _valueReader->readDelta(in, value, _streamer->getValue(reference, key)); - _streamer->insert(object, key, value); - } else { - _valueReader->readDelta(in, value, QVariant()); - } - } - int removed; - in >> removed; - for (int i = 0; i < removed; i++) { - QVariant key = _keyReader->read(in); - if (_streamer) { - _streamer->remove(object, key); - } - } - break; - } - default: - break; - } -} - -bool TypeReader::matchesExactly(const TypeStreamer* streamer) const { - return _exactMatch && _streamer == streamer; -} - -uint qHash(const TypeReader& typeReader, uint seed) { - return qHash(typeReader.getTypeName(), seed); -} - -QDebug& operator<<(QDebug& debug, const TypeReader& typeReader) { - return debug << typeReader.getTypeName(); -} - -FieldReader::FieldReader(const TypeReader& reader, int index) : - _reader(reader), - _index(index) { -} - -void FieldReader::read(Bitstream& in, const TypeStreamer* streamer, QVariant& object) const { - QVariant value = _reader.read(in); - if (_index != -1 && streamer) { - streamer->setField(object, _index, value); - } -} - -void FieldReader::readDelta(Bitstream& in, const TypeStreamer* streamer, QVariant& object, const QVariant& reference) const { - QVariant value; - if (_index != -1 && streamer) { - _reader.readDelta(in, value, streamer->getField(reference, _index)); - streamer->setField(object, _index, value); - } else { - _reader.readDelta(in, value, QVariant()); - } -} - -ObjectReader::ObjectReader(const QByteArray& className, const QMetaObject* metaObject, - const PropertyReaderVector& properties) : - _className(className), - _metaObject(metaObject), +MappedObjectStreamer::MappedObjectStreamer(const QMetaObject* metaObject, const QVector& properties) : + ObjectStreamer(metaObject), _properties(properties) { } -QObject* ObjectReader::read(Bitstream& in, QObject* object) const { +const char* MappedObjectStreamer::getName() const { + return _metaObject->className(); +} + +const QVector& MappedObjectStreamer::getProperties() const { + return _properties; +} + +void MappedObjectStreamer::writeMetadata(Bitstream& out, bool full) const { + out << _properties.size(); + if (_properties.isEmpty()) { + return; + } + QCryptographicHash hash(QCryptographicHash::Md5); + foreach (const StreamerPropertyPair& property, _properties) { + out << property.first.data(); + if (full) { + out << QByteArray::fromRawData(property.second.name(), strlen(property.second.name())); + } else { + hash.addData(property.second.name(), strlen(property.second.name()) + 1); + } + } + if (!full) { + QByteArray hashResult = hash.result(); + out.write(hashResult.constData(), hashResult.size() * BITS_IN_BYTE); + } +} + +void MappedObjectStreamer::write(Bitstream& out, const QObject* object) const { + foreach (const StreamerPropertyPair& property, _properties) { + property.first->write(out, property.second.read(object)); + } +} + +void MappedObjectStreamer::writeRawDelta(Bitstream& out, const QObject* object, const QObject* reference) const { + foreach (const StreamerPropertyPair& property, _properties) { + property.first->writeDelta(out, property.second.read(object), (reference && reference->metaObject() == _metaObject) ? + property.second.read(reference) : QVariant()); + } +} + +QObject* MappedObjectStreamer::read(Bitstream& in, QObject* object) const { if (!object && _metaObject) { object = _metaObject->newInstance(); } - foreach (const PropertyReader& property, _properties) { - property.read(in, object); + foreach (const StreamerPropertyPair& property, _properties) { + QVariant value = property.first->read(in); + if (property.second.isValid() && object) { + property.second.write(object, value); + } } return object; } -QObject* ObjectReader::readDelta(Bitstream& in, const QObject* reference, QObject* object) const { +QObject* MappedObjectStreamer::readRawDelta(Bitstream& in, const QObject* reference, QObject* object) const { if (!object && _metaObject) { object = _metaObject->newInstance(); } - foreach (const PropertyReader& property, _properties) { - property.readDelta(in, object, reference); + foreach (const StreamerPropertyPair& property, _properties) { + QVariant value; + property.first->readDelta(in, value, (property.second.isValid() && reference && + reference->metaObject() == _metaObject) ? property.second.read(reference) : QVariant()); + if (property.second.isValid() && object) { + property.second.write(object, value); + } } return object; } -uint qHash(const ObjectReader& objectReader, uint seed) { - return qHash(objectReader.getClassName(), seed); +GenericObjectStreamer::GenericObjectStreamer(const QByteArray& name, const QVector& properties, + const QByteArray& hash) : + ObjectStreamer(&GenericSharedObject::staticMetaObject), + _name(name), + _properties(properties), + _hash(hash) { } -QDebug& operator<<(QDebug& debug, const ObjectReader& objectReader) { - return debug << objectReader.getClassName(); +const char* GenericObjectStreamer::getName() const { + return _name.constData(); } -PropertyReader::PropertyReader(const TypeReader& reader, const QMetaProperty& property) : - _reader(reader), - _property(property) { -} - -void PropertyReader::read(Bitstream& in, QObject* object) const { - QVariant value = _reader.read(in); - if (_property.isValid() && object) { - _property.write(object, value); +void GenericObjectStreamer::writeMetadata(Bitstream& out, bool full) const { + out << _properties.size(); + if (_properties.isEmpty()) { + return; + } + foreach (const StreamerNamePair& property, _properties) { + out << property.first.data(); + if (full) { + out << property.second; + } + } + if (!full) { + if (_hash.isEmpty()) { + QCryptographicHash hash(QCryptographicHash::Md5); + foreach (const StreamerNamePair& property, _properties) { + hash.addData(property.second.constData(), property.second.size() + 1); + } + const_cast(this)->_hash = hash.result(); + } + out.write(_hash.constData(), _hash.size() * BITS_IN_BYTE); } } -void PropertyReader::readDelta(Bitstream& in, QObject* object, const QObject* reference) const { - QVariant value; - _reader.readDelta(in, value, (_property.isValid() && reference) ? _property.read(reference) : QVariant()); - if (_property.isValid() && object) { - _property.write(object, value); +void GenericObjectStreamer::write(Bitstream& out, const QObject* object) const { + const QVariantList& values = static_cast(object)->getValues(); + for (int i = 0; i < _properties.size(); i++) { + _properties.at(i).first->write(out, values.at(i)); } } -PropertyWriter::PropertyWriter(const QMetaProperty& property, const TypeStreamer* streamer) : - _property(property), - _streamer(streamer) { +void GenericObjectStreamer::writeRawDelta(Bitstream& out, const QObject* object, const QObject* reference) const { + const GenericSharedObject* genericObject = static_cast(object); + const GenericSharedObject* genericReference = (reference && + reference->metaObject() == &GenericSharedObject::staticMetaObject) ? + static_cast(reference) : NULL; + for (int i = 0; i < _properties.size(); i++) { + _properties.at(i).first->writeDelta(out, genericObject->getValues().at(i), + (genericReference && genericReference->getStreamer() == genericObject->getStreamer()) ? + genericReference->getValues().at(i) : QVariant()); + } } -void PropertyWriter::write(Bitstream& out, const QObject* object) const { - _streamer->write(out, _property.read(object)); +QObject* GenericObjectStreamer::read(Bitstream& in, QObject* object) const { + if (!object) { + object = new GenericSharedObject(_weakSelf); + } + QVariantList values; + foreach (const StreamerNamePair& property, _properties) { + values.append(property.first->read(in)); + } + static_cast(object)->setValues(values); + return object; } -void PropertyWriter::writeDelta(Bitstream& out, const QObject* object, const QObject* reference) const { - _streamer->writeDelta(out, _property.read(object), reference && object->metaObject() == reference->metaObject() ? - _property.read(reference) : QVariant()); +QObject* GenericObjectStreamer::readRawDelta(Bitstream& in, const QObject* reference, QObject* object) const { + if (!object) { + object = new GenericSharedObject(_weakSelf); + } + QVariantList values; + for (int i = 0; i < _properties.size(); i++) { + const StreamerNamePair& property = _properties.at(i); + QVariant value; + property.first->readDelta(in, value, reference ? + static_cast(reference)->getValues().at(i) : QVariant()); + values.append(value); + } + static_cast(object)->setValues(values); + return object; } MetaField::MetaField(const QByteArray& name, const TypeStreamer* streamer) : @@ -1903,6 +1827,19 @@ MetaField::MetaField(const QByteArray& name, const TypeStreamer* streamer) : _streamer(streamer) { } +GenericValue::GenericValue(const TypeStreamerPointer& streamer, const QVariant& value) : + _streamer(streamer), + _value(value) { +} + +bool GenericValue::operator==(const GenericValue& other) const { + return _streamer == other._streamer && _value == other._value; +} + +GenericSharedObject::GenericSharedObject(const ObjectStreamerPointer& streamer) : + _streamer(streamer) { +} + TypeStreamer::~TypeStreamer() { } @@ -1910,6 +1847,83 @@ const char* TypeStreamer::getName() const { return QMetaType::typeName(_type); } +const TypeStreamer* TypeStreamer::getStreamerToWrite(const QVariant& value) const { + return this; +} + +void TypeStreamer::writeMetadata(Bitstream& out, bool full) const { + if (getCategory() != STREAMABLE_CATEGORY) { + return; + } + // streamable type + const QVector& metaFields = getMetaFields(); + out << metaFields.size(); + if (metaFields.isEmpty()) { + return; + } + QCryptographicHash hash(QCryptographicHash::Md5); + foreach (const MetaField& metaField, metaFields) { + out << metaField.getStreamer(); + if (full) { + out << metaField.getName(); + } else { + hash.addData(metaField.getName().constData(), metaField.getName().size() + 1); + } + } + if (!full) { + QByteArray hashResult = hash.result(); + out.write(hashResult.constData(), hashResult.size() * BITS_IN_BYTE); + } +} + +bool TypeStreamer::equal(const QVariant& first, const QVariant& second) const { + return first == second; +} + +void TypeStreamer::write(Bitstream& out, const QVariant& value) const { + // nothing by default +} + +QVariant TypeStreamer::read(Bitstream& in) const { + return QVariant(); +} + +void TypeStreamer::writeVariant(Bitstream& out, const QVariant& value) const { + out << this; + write(out, value); +} + +QVariant TypeStreamer::readVariant(Bitstream& in) const { + return read(in); +} + +void TypeStreamer::writeDelta(Bitstream& out, const QVariant& value, const QVariant& reference) const { + if (value == reference) { + out << false; + } else { + out << true; + writeRawDelta(out, value, reference); + } +} + +void TypeStreamer::readDelta(Bitstream& in, QVariant& value, const QVariant& reference) const { + bool changed; + in >> changed; + if (changed) { + readRawDelta(in, value, reference); + } else { + value = reference; + } +} + +void TypeStreamer::writeRawDelta(Bitstream& out, const QVariant& value, const QVariant& reference) const { + // nothing by default +} + +void TypeStreamer::readRawDelta(Bitstream& in, QVariant& value, const QVariant& reference) const { + value = reference; +} + void TypeStreamer::setEnumValue(QVariant& object, int value, const QHash& mappings) const { // nothing by default } @@ -1931,8 +1945,8 @@ QVariant TypeStreamer::getField(const QVariant& object, int index) const { return QVariant(); } -TypeReader::Type TypeStreamer::getReaderType() const { - return TypeReader::SIMPLE_TYPE; +TypeStreamer::Category TypeStreamer::getCategory() const { + return SIMPLE_CATEGORY; } int TypeStreamer::getBits() const { @@ -1993,23 +2007,46 @@ EnumTypeStreamer::EnumTypeStreamer(const QMetaObject* metaObject, const char* na _name(QByteArray(metaObject->className()) + "::" + name), _bits(-1) { - setType(QMetaType::Int); + _type = QMetaType::Int; + _self = TypeStreamerPointer(this); } EnumTypeStreamer::EnumTypeStreamer(const QMetaEnum& metaEnum) : _name(QByteArray(metaEnum.scope()) + "::" + metaEnum.name()), _metaEnum(metaEnum), _bits(-1) { - - setType(QMetaType::Int); + + _type = QMetaType::Int; + _self = TypeStreamerPointer(this); } const char* EnumTypeStreamer::getName() const { return _name.constData(); } -TypeReader::Type EnumTypeStreamer::getReaderType() const { - return TypeReader::ENUM_TYPE; +void EnumTypeStreamer::writeMetadata(Bitstream& out, bool full) const { + QMetaEnum metaEnum = getMetaEnum(); + if (full) { + out << metaEnum.keyCount(); + for (int i = 0; i < metaEnum.keyCount(); i++) { + out << QByteArray::fromRawData(metaEnum.key(i), strlen(metaEnum.key(i))); + out << metaEnum.value(i); + } + } else { + out << getBits(); + QCryptographicHash hash(QCryptographicHash::Md5); + for (int i = 0; i < metaEnum.keyCount(); i++) { + hash.addData(metaEnum.key(i), strlen(metaEnum.key(i)) + 1); + qint32 value = metaEnum.value(i); + hash.addData((const char*)&value, sizeof(qint32)); + } + QByteArray hashResult = hash.result(); + out.write(hashResult.constData(), hashResult.size() * BITS_IN_BYTE); + } +} + +TypeStreamer::Category EnumTypeStreamer::getCategory() const { + return ENUM_CATEGORY; } int EnumTypeStreamer::getBits() const { @@ -2094,3 +2131,340 @@ void EnumTypeStreamer::setEnumValue(QVariant& object, int value, const QHash& mappings) : + _baseStreamer(baseStreamer), + _bits(bits), + _mappings(mappings) { +} + +QVariant MappedEnumTypeStreamer::read(Bitstream& in) const { + QVariant object = QVariant(_baseStreamer->getType(), 0); + int value = 0; + in.read(&value, _bits); + _baseStreamer->setEnumValue(object, value, _mappings); + return object; +} + +void MappedEnumTypeStreamer::readRawDelta(Bitstream& in, QVariant& object, const QVariant& reference) const { + int value = 0; + in.read(&value, _bits); + _baseStreamer->setEnumValue(object, value, _mappings); +} + +GenericTypeStreamer::GenericTypeStreamer(const QByteArray& name) : + _name(name) { +} + +const char* GenericTypeStreamer::getName() const { + return _name.constData(); +} + +QVariant GenericTypeStreamer::readVariant(Bitstream& in) const { + return QVariant::fromValue(GenericValue(_weakSelf, read(in))); +} + +GenericEnumTypeStreamer::GenericEnumTypeStreamer(const QByteArray& name, const QVector& values, + int bits, const QByteArray& hash) : + GenericTypeStreamer(name), + _values(values), + _bits(bits), + _hash(hash) { + + _type = qMetaTypeId(); +} + +void GenericEnumTypeStreamer::writeMetadata(Bitstream& out, bool full) const { + if (full) { + out << _values.size(); + foreach (const NameIntPair& value, _values) { + out << value.first << value.second; + } + } else { + out << _bits; + if (_hash.isEmpty()) { + QCryptographicHash hash(QCryptographicHash::Md5); + foreach (const NameIntPair& value, _values) { + hash.addData(value.first.constData(), value.first.size() + 1); + qint32 intValue = value.second; + hash.addData((const char*)&intValue, sizeof(qint32)); + } + const_cast(this)->_hash = hash.result(); + } + out.write(_hash.constData(), _hash.size() * BITS_IN_BYTE); + } +} + +void GenericEnumTypeStreamer::write(Bitstream& out, const QVariant& value) const { + int intValue = value.toInt(); + out.write(&intValue, _bits); +} + +QVariant GenericEnumTypeStreamer::read(Bitstream& in) const { + int intValue = 0; + in.read(&intValue, _bits); + return intValue; +} + +TypeStreamer::Category GenericEnumTypeStreamer::getCategory() const { + return ENUM_CATEGORY; +} + +MappedStreamableTypeStreamer::MappedStreamableTypeStreamer(const TypeStreamer* baseStreamer, + const QVector& fields) : + _baseStreamer(baseStreamer), + _fields(fields) { +} + +QVariant MappedStreamableTypeStreamer::read(Bitstream& in) const { + QVariant object = QVariant(_baseStreamer->getType(), 0); + foreach (const StreamerIndexPair& pair, _fields) { + QVariant value = pair.first->read(in); + if (pair.second != -1) { + _baseStreamer->setField(object, pair.second, value); + } + } + return object; +} + +void MappedStreamableTypeStreamer::readRawDelta(Bitstream& in, QVariant& object, const QVariant& reference) const { + foreach (const StreamerIndexPair& pair, _fields) { + QVariant value; + if (pair.second != -1) { + pair.first->readDelta(in, value, _baseStreamer->getField(reference, pair.second)); + _baseStreamer->setField(object, pair.second, value); + } else { + pair.first->readDelta(in, value, QVariant()); + } + } +} + +GenericStreamableTypeStreamer::GenericStreamableTypeStreamer(const QByteArray& name, + const QVector& fields, const QByteArray& hash) : + GenericTypeStreamer(name), + _fields(fields), + _hash(hash) { + + _type = qMetaTypeId(); +} + +void GenericStreamableTypeStreamer::writeMetadata(Bitstream& out, bool full) const { + out << _fields.size(); + if (_fields.isEmpty()) { + return; + } + foreach (const StreamerNamePair& field, _fields) { + out << field.first.data(); + if (full) { + out << field.second; + } + } + if (!full) { + if (_hash.isEmpty()) { + QCryptographicHash hash(QCryptographicHash::Md5); + foreach (const StreamerNamePair& field, _fields) { + hash.addData(field.second.constData(), field.second.size() + 1); + } + const_cast(this)->_hash = hash.result(); + } + out.write(_hash.constData(), _hash.size() * BITS_IN_BYTE); + } +} + +void GenericStreamableTypeStreamer::write(Bitstream& out, const QVariant& value) const { + QVariantList values = value.toList(); + for (int i = 0; i < _fields.size(); i++) { + _fields.at(i).first->write(out, values.at(i)); + } +} + +QVariant GenericStreamableTypeStreamer::read(Bitstream& in) const { + QVariantList values; + foreach (const StreamerNamePair& field, _fields) { + values.append(field.first->read(in)); + } + return values; +} + +TypeStreamer::Category GenericStreamableTypeStreamer::getCategory() const { + return STREAMABLE_CATEGORY; +} + +MappedListTypeStreamer::MappedListTypeStreamer(const TypeStreamer* baseStreamer, const TypeStreamerPointer& valueStreamer) : + _baseStreamer(baseStreamer), + _valueStreamer(valueStreamer) { +} + +QVariant MappedListTypeStreamer::read(Bitstream& in) const { + QVariant object = QVariant(_baseStreamer->getType(), 0); + int size; + in >> size; + for (int i = 0; i < size; i++) { + QVariant value = _valueStreamer->read(in); + _baseStreamer->insert(object, value); + } + return object; +} + +void MappedListTypeStreamer::readRawDelta(Bitstream& in, QVariant& object, const QVariant& reference) const { + object = reference; + int size, referenceSize; + in >> size >> referenceSize; + if (size < referenceSize) { + _baseStreamer->prune(object, size); + } + for (int i = 0; i < size; i++) { + if (i < referenceSize) { + QVariant value; + _valueStreamer->readDelta(in, value, _baseStreamer->getValue(reference, i)); + _baseStreamer->setValue(object, i, value); + } else { + _baseStreamer->insert(object, _valueStreamer->read(in)); + } + } +} + +GenericListTypeStreamer::GenericListTypeStreamer(const QByteArray& name, const TypeStreamerPointer& valueStreamer) : + GenericTypeStreamer(name), + _valueStreamer(valueStreamer) { + + _type = qMetaTypeId(); +} + +void GenericListTypeStreamer::writeMetadata(Bitstream& out, bool full) const { + out << _valueStreamer.data(); +} + +void GenericListTypeStreamer::write(Bitstream& out, const QVariant& value) const { + QVariantList values = value.toList(); + out << values.size(); + foreach (const QVariant& element, values) { + _valueStreamer->write(out, element); + } +} + +QVariant GenericListTypeStreamer::read(Bitstream& in) const { + QVariantList values; + int size; + in >> size; + for (int i = 0; i < size; i++) { + values.append(_valueStreamer->read(in)); + } + return values; +} + +TypeStreamer::Category GenericListTypeStreamer::getCategory() const { + return LIST_CATEGORY; +} + +MappedSetTypeStreamer::MappedSetTypeStreamer(const TypeStreamer* baseStreamer, const TypeStreamerPointer& valueStreamer) : + MappedListTypeStreamer(baseStreamer, valueStreamer) { +} + +void MappedSetTypeStreamer::readRawDelta(Bitstream& in, QVariant& object, const QVariant& reference) const { + object = reference; + int addedOrRemoved; + in >> addedOrRemoved; + for (int i = 0; i < addedOrRemoved; i++) { + QVariant value = _valueStreamer->read(in); + if (!_baseStreamer->remove(object, value)) { + _baseStreamer->insert(object, value); + } + } +} + +GenericSetTypeStreamer::GenericSetTypeStreamer(const QByteArray& name, const TypeStreamerPointer& valueStreamer) : + GenericListTypeStreamer(name, valueStreamer) { +} + +TypeStreamer::Category GenericSetTypeStreamer::getCategory() const { + return SET_CATEGORY; +} + +MappedMapTypeStreamer::MappedMapTypeStreamer(const TypeStreamer* baseStreamer, const TypeStreamerPointer& keyStreamer, + const TypeStreamerPointer& valueStreamer) : + _baseStreamer(baseStreamer), + _keyStreamer(keyStreamer), + _valueStreamer(valueStreamer) { +} + +QVariant MappedMapTypeStreamer::read(Bitstream& in) const { + QVariant object = QVariant(_baseStreamer->getType(), 0); + int size; + in >> size; + for (int i = 0; i < size; i++) { + QVariant key = _keyStreamer->read(in); + QVariant value = _valueStreamer->read(in); + _baseStreamer->insert(object, key, value); + } + return object; +} + +void MappedMapTypeStreamer::readRawDelta(Bitstream& in, QVariant& object, const QVariant& reference) const { + object = reference; + int added; + in >> added; + for (int i = 0; i < added; i++) { + QVariant key = _keyStreamer->read(in); + QVariant value = _valueStreamer->read(in); + _baseStreamer->insert(object, key, value); + } + int modified; + in >> modified; + for (int i = 0; i < modified; i++) { + QVariant key = _keyStreamer->read(in); + QVariant value; + _valueStreamer->readDelta(in, value, _baseStreamer->getValue(reference, key)); + _baseStreamer->insert(object, key, value); + } + int removed; + in >> removed; + for (int i = 0; i < removed; i++) { + QVariant key = _keyStreamer->read(in); + _baseStreamer->remove(object, key); + } +} + +GenericMapTypeStreamer::GenericMapTypeStreamer(const QByteArray& name, const TypeStreamerPointer& keyStreamer, + const TypeStreamerPointer& valueStreamer) : + GenericTypeStreamer(name), + _keyStreamer(keyStreamer), + _valueStreamer(valueStreamer) { + + _type = qMetaTypeId(); +} + +void GenericMapTypeStreamer::writeMetadata(Bitstream& out, bool full) const { + out << _keyStreamer.data() << _valueStreamer.data(); +} + +void GenericMapTypeStreamer::write(Bitstream& out, const QVariant& value) const { + QVariantPairList values = value.value(); + out << values.size(); + foreach (const QVariantPair& pair, values) { + _keyStreamer->write(out, pair.first); + _valueStreamer->write(out, pair.second); + } +} + +QVariant GenericMapTypeStreamer::read(Bitstream& in) const { + QVariantPairList values; + int size; + in >> size; + for (int i = 0; i < size; i++) { + QVariant key = _keyStreamer->read(in); + QVariant value = _valueStreamer->read(in); + values.append(QVariantPair(key, value)); + } + return QVariant::fromValue(values); +} + +TypeStreamer::Category GenericMapTypeStreamer::getCategory() const { + return MAP_CATEGORY; +} + +void GenericValueStreamer::writeVariant(Bitstream& out, const QVariant& value) const { + GenericValue genericValue = value.value(); + out << genericValue.getStreamer().data(); + genericValue.getStreamer()->write(out, genericValue.getValue()); +} + diff --git a/libraries/metavoxels/src/Bitstream.h b/libraries/metavoxels/src/Bitstream.h index d05f6574c0..1589473b0e 100644 --- a/libraries/metavoxels/src/Bitstream.h +++ b/libraries/metavoxels/src/Bitstream.h @@ -35,19 +35,25 @@ class QUrl; class Attribute; class AttributeValue; class Bitstream; -class FieldReader; +class GenericValue; class ObjectReader; +class ObjectStreamer; class OwnedAttributeValue; -class PropertyReader; -class PropertyWriter; -class TypeReader; class TypeStreamer; typedef SharedObjectPointerTemplate AttributePointer; typedef QPair ScopeNamePair; -typedef QVector PropertyReaderVector; -typedef QVector PropertyWriterVector; +typedef QPair NameIntPair; +typedef QSharedPointer ObjectStreamerPointer; +typedef QWeakPointer WeakObjectStreamerPointer; +typedef QSharedPointer TypeStreamerPointer; +typedef QWeakPointer WeakTypeStreamerPointer; + +typedef QPair QVariantPair; +typedef QList QVariantPairList; + +Q_DECLARE_METATYPE(QVariantPairList) /// Streams integer identifiers that conform to the following pattern: each ID encountered in the stream is either one that /// has been sent (received) before, or is one more than the highest previously encountered ID (starting at zero). This allows @@ -197,7 +203,7 @@ public: class WriteMappings { public: - QHash metaObjectOffsets; + QHash objectStreamerOffsets; QHash typeStreamerOffsets; QHash attributeOffsets; QHash scriptStringOffsets; @@ -206,8 +212,8 @@ public: class ReadMappings { public: - QHash metaObjectValues; - QHash typeStreamerValues; + QHash objectStreamerValues; + QHash typeStreamerValues; QHash attributeValues; QHash scriptStringValues; QHash sharedObjectValues; @@ -232,8 +238,17 @@ public: enum MetadataType { NO_METADATA, HASH_METADATA, FULL_METADATA }; + enum GenericsMode { NO_GENERICS, FALLBACK_GENERICS, ALL_GENERICS }; + /// Creates a new bitstream. Note: the stream may be used for reading or writing, but not both. - Bitstream(QDataStream& underlying, MetadataType metadataType = NO_METADATA, QObject* parent = NULL); + /// \param metadataType the metadata type, which determines the amount of version-resiliency: with no metadata, all types + /// must match exactly; with hash metadata, any types which do not match exactly will be ignored; with full metadata, + /// fields (and enum values, etc.) will be remapped by name + /// \param genericsMode the generics mode, which determines which types will be replaced by generic equivalents: with + /// no generics, no generics will be created; with fallback generics, generics will be created for any unknown types; with + /// all generics, generics will be created for all non-simple types + Bitstream(QDataStream& underlying, MetadataType metadataType = NO_METADATA, + GenericsMode = NO_GENERICS, QObject* parent = NULL); /// Substitutes the supplied metaobject for the given class name's default mapping. void addMetaObjectSubstitution(const QByteArray& className, const QMetaObject* metaObject); @@ -364,6 +379,9 @@ public: Bitstream& operator<<(const AttributeValue& attributeValue); Bitstream& operator>>(OwnedAttributeValue& attributeValue); + Bitstream& operator<<(const GenericValue& value); + Bitstream& operator>>(GenericValue& value); + template Bitstream& operator<<(const QList& list); template Bitstream& operator>>(QList& list); @@ -381,11 +399,14 @@ public: Bitstream& operator<<(const QMetaObject* metaObject); Bitstream& operator>>(const QMetaObject*& metaObject); - Bitstream& operator>>(ObjectReader& objectReader); + + Bitstream& operator<<(const ObjectStreamer* streamer); + Bitstream& operator>>(const ObjectStreamer*& streamer); + Bitstream& operator>>(ObjectStreamerPointer& streamer); Bitstream& operator<<(const TypeStreamer* streamer); Bitstream& operator>>(const TypeStreamer*& streamer); - Bitstream& operator>>(TypeReader& reader); + Bitstream& operator>>(TypeStreamerPointer& streamer); Bitstream& operator<<(const AttributePointer& attribute); Bitstream& operator>>(AttributePointer& attribute); @@ -399,11 +420,11 @@ public: Bitstream& operator<<(const SharedObjectPointer& object); Bitstream& operator>>(SharedObjectPointer& object); - Bitstream& operator<(const QMetaObject* metaObject); - Bitstream& operator>(ObjectReader& objectReader); + Bitstream& operator<(const ObjectStreamer* streamer); + Bitstream& operator>(ObjectStreamerPointer& streamer); Bitstream& operator<(const TypeStreamer* streamer); - Bitstream& operator>(TypeReader& reader); + Bitstream& operator>(TypeStreamerPointer& streamer); Bitstream& operator<(const AttributePointer& attribute); Bitstream& operator>(AttributePointer& attribute); @@ -424,14 +445,18 @@ private slots: private: + ObjectStreamerPointer readGenericObjectStreamer(const QByteArray& name); + TypeStreamerPointer readGenericTypeStreamer(const QByteArray& name, int category); + QDataStream& _underlying; quint8 _byte; int _position; MetadataType _metadataType; + GenericsMode _genericsMode; - RepeatedValueStreamer _metaObjectStreamer; - RepeatedValueStreamer _typeStreamerStreamer; + RepeatedValueStreamer _objectStreamerStreamer; + RepeatedValueStreamer _typeStreamerStreamer; RepeatedValueStreamer _attributeStreamer; RepeatedValueStreamer _scriptStringStreamer; RepeatedValueStreamer _sharedObjectStreamer; @@ -447,17 +472,17 @@ private: static QMultiHash& getMetaObjectSubClasses(); static QHash& getTypeStreamers(); + static const QHash& getObjectStreamers(); + static QHash createObjectStreamers(); + static const QHash& getEnumStreamers(); static QHash createEnumStreamers(); static const QHash& getEnumStreamersByName(); static QHash createEnumStreamersByName(); - static const QHash& getPropertyReaders(); - static QHash createPropertyReaders(); - - static const QHash& getPropertyWriters(); - static QHash createPropertyWriters(); + static const TypeStreamer* getInvalidTypeStreamer(); + static const TypeStreamer* createInvalidTypeStreamer(); }; template inline void Bitstream::writeDelta(const T& value, const T& reference) { @@ -741,133 +766,75 @@ template inline Bitstream& Bitstream::operator>>(QHash& return *this; } -typedef QSharedPointer TypeReaderPointer; +typedef QPair StreamerPropertyPair; -/// Contains the information required to read a type from the stream. -class TypeReader { +/// Contains the information required to stream an object. +class ObjectStreamer { public: - enum Type { SIMPLE_TYPE, ENUM_TYPE, STREAMABLE_TYPE, LIST_TYPE, SET_TYPE, MAP_TYPE }; - - TypeReader(const QByteArray& typeName = QByteArray(), const TypeStreamer* streamer = NULL); - - TypeReader(const QByteArray& typeName, const TypeStreamer* streamer, int bits, const QHash& mappings); - - TypeReader(const QByteArray& typeName, const TypeStreamer* streamer, const QVector& fields); + ObjectStreamer(const QMetaObject* metaObject); - TypeReader(const QByteArray& typeName, const TypeStreamer* streamer, Type type, - const TypeReaderPointer& valueReader); - - TypeReader(const QByteArray& typeName, const TypeStreamer* streamer, - const TypeReaderPointer& keyReader, const TypeReaderPointer& valueReader); - - const QByteArray& getTypeName() const { return _typeName; } - const TypeStreamer* getStreamer() const { return _streamer; } - - QVariant read(Bitstream& in) const; - void readDelta(Bitstream& in, QVariant& object, const QVariant& reference) const; - void readRawDelta(Bitstream& in, QVariant& object, const QVariant& reference) const; - - bool matchesExactly(const TypeStreamer* streamer) const; - - bool operator==(const TypeReader& other) const { return _typeName == other._typeName; } - bool operator!=(const TypeReader& other) const { return _typeName != other._typeName; } - -private: - - QByteArray _typeName; - const TypeStreamer* _streamer; - bool _exactMatch; - Type _type; - int _bits; - QHash _mappings; - TypeReaderPointer _keyReader; - TypeReaderPointer _valueReader; - QVector _fields; -}; - -uint qHash(const TypeReader& typeReader, uint seed = 0); - -QDebug& operator<<(QDebug& debug, const TypeReader& typeReader); - -/// Contains the information required to read a metatype field from the stream and apply it. -class FieldReader { -public: - - FieldReader(const TypeReader& reader = TypeReader(), int index = -1); - - const TypeReader& getReader() const { return _reader; } - int getIndex() const { return _index; } - - void read(Bitstream& in, const TypeStreamer* streamer, QVariant& object) const; - void readDelta(Bitstream& in, const TypeStreamer* streamer, QVariant& object, const QVariant& reference) const; - -private: - - TypeReader _reader; - int _index; -}; - -/// Contains the information required to read an object from the stream. -class ObjectReader { -public: - - ObjectReader(const QByteArray& className = QByteArray(), const QMetaObject* metaObject = NULL, - const QVector& properties = QVector()); - - const QByteArray& getClassName() const { return _className; } const QMetaObject* getMetaObject() const { return _metaObject; } + const ObjectStreamerPointer& getSelf() const { return _self; } + + virtual const char* getName() const = 0; + virtual const QVector& getProperties() const; + virtual void writeMetadata(Bitstream& out, bool full) const = 0; + virtual void write(Bitstream& out, const QObject* object) const = 0; + virtual void writeRawDelta(Bitstream& out, const QObject* object, const QObject* reference) const = 0; + virtual QObject* read(Bitstream& in, QObject* object = NULL) const = 0; + virtual QObject* readRawDelta(Bitstream& in, const QObject* reference, QObject* object = NULL) const = 0; + +protected: + + friend class Bitstream; - QObject* read(Bitstream& in, QObject* object = NULL) const; - QObject* readDelta(Bitstream& in, const QObject* reference, QObject* object = NULL) const; - - bool operator==(const ObjectReader& other) const { return _className == other._className; } - bool operator!=(const ObjectReader& other) const { return _className != other._className; } - -private: - - QByteArray _className; const QMetaObject* _metaObject; - QVector _properties; + ObjectStreamerPointer _self; }; -uint qHash(const ObjectReader& objectReader, uint seed = 0); - -QDebug& operator<<(QDebug& debug, const ObjectReader& objectReader); - -/// Contains the information required to read an object property from the stream and apply it. -class PropertyReader { +/// A streamer that maps to a local class. +class MappedObjectStreamer : public ObjectStreamer { public: - PropertyReader(const TypeReader& reader = TypeReader(), const QMetaProperty& property = QMetaProperty()); - - const TypeReader& getReader() const { return _reader; } - - void read(Bitstream& in, QObject* object) const; - void readDelta(Bitstream& in, QObject* object, const QObject* reference) const; - -private: - - TypeReader _reader; - QMetaProperty _property; -}; - -/// Contains the information required to obtain an object property and write it to the stream. -class PropertyWriter { -public: - - PropertyWriter(const QMetaProperty& property = QMetaProperty(), const TypeStreamer* streamer = NULL); - - const QMetaProperty& getProperty() const { return _property; } - const TypeStreamer* getStreamer() const { return _streamer; } - - void write(Bitstream& out, const QObject* object) const; - void writeDelta(Bitstream& out, const QObject* object, const QObject* reference) const; + MappedObjectStreamer(const QMetaObject* metaObject, const QVector& properties); + virtual const char* getName() const; + virtual const QVector& getProperties() const; + virtual void writeMetadata(Bitstream& out, bool full) const; + virtual void write(Bitstream& out, const QObject* object) const; + virtual void writeRawDelta(Bitstream& out, const QObject* object, const QObject* reference) const; + virtual QObject* read(Bitstream& in, QObject* object = NULL) const; + virtual QObject* readRawDelta(Bitstream& in, const QObject* reference, QObject* object = NULL) const; + private: - QMetaProperty _property; - const TypeStreamer* _streamer; + QVector _properties; +}; + +typedef QPair StreamerNamePair; + +/// A streamer for generic objects. +class GenericObjectStreamer : public ObjectStreamer { +public: + + GenericObjectStreamer(const QByteArray& name, const QVector& properties, const QByteArray& hash); + + virtual const char* getName() const; + virtual void writeMetadata(Bitstream& out, bool full) const; + virtual void write(Bitstream& out, const QObject* object) const; + virtual void writeRawDelta(Bitstream& out, const QObject* object, const QObject* reference) const; + virtual QObject* read(Bitstream& in, QObject* object = NULL) const; + virtual QObject* readRawDelta(Bitstream& in, const QObject* reference, QObject* object = NULL) const; + +private: + + friend class Bitstream; + + QByteArray _name; + WeakObjectStreamerPointer _weakSelf; + QVector _properties; + QByteArray _hash; }; /// Describes a metatype field. @@ -890,27 +857,75 @@ Q_DECLARE_METATYPE(const QMetaObject*) /// Macro for registering streamable meta-objects. #define REGISTER_META_OBJECT(x) static int x##Registration = Bitstream::registerMetaObject(#x, &x::staticMetaObject); +/// Contains a value along with a pointer to its streamer. +class GenericValue { +public: + + GenericValue(const TypeStreamerPointer& streamer = TypeStreamerPointer(), const QVariant& value = QVariant()); + + const TypeStreamerPointer& getStreamer() const { return _streamer; } + const QVariant& getValue() const { return _value; } + + bool operator==(const GenericValue& other) const; + +private: + + TypeStreamerPointer _streamer; + QVariant _value; +}; + +Q_DECLARE_METATYPE(GenericValue) + +/// Contains a list of property values along with a pointer to their metadata. +class GenericSharedObject : public SharedObject { + Q_OBJECT + +public: + + GenericSharedObject(const ObjectStreamerPointer& streamer); + + const ObjectStreamerPointer& getStreamer() const { return _streamer; } + + void setValues(const QVariantList& values) { _values = values; } + const QVariantList& getValues() const { return _values; } + +private: + + ObjectStreamerPointer _streamer; + QVariantList _values; +}; + /// Interface for objects that can write values to and read values from bitstreams. class TypeStreamer { public: + enum Category { SIMPLE_CATEGORY, ENUM_CATEGORY, STREAMABLE_CATEGORY, LIST_CATEGORY, SET_CATEGORY, MAP_CATEGORY }; + virtual ~TypeStreamer(); - void setType(int type) { _type = type; } int getType() const { return _type; } + const TypeStreamerPointer& getSelf() const { return _self; } + virtual const char* getName() const; - virtual bool equal(const QVariant& first, const QVariant& second) const = 0; + virtual const TypeStreamer* getStreamerToWrite(const QVariant& value) const; - virtual void write(Bitstream& out, const QVariant& value) const = 0; - virtual QVariant read(Bitstream& in) const = 0; + virtual void writeMetadata(Bitstream& out, bool full) const; + + virtual bool equal(const QVariant& first, const QVariant& second) const; + + virtual void write(Bitstream& out, const QVariant& value) const; + virtual QVariant read(Bitstream& in) const; - virtual void writeDelta(Bitstream& out, const QVariant& value, const QVariant& reference) const = 0; - virtual void readDelta(Bitstream& in, QVariant& value, const QVariant& reference) const = 0; + virtual void writeVariant(Bitstream& out, const QVariant& value) const; + virtual QVariant readVariant(Bitstream& in) const; - virtual void writeRawDelta(Bitstream& out, const QVariant& value, const QVariant& reference) const = 0; - virtual void readRawDelta(Bitstream& in, QVariant& value, const QVariant& reference) const = 0; + virtual void writeDelta(Bitstream& out, const QVariant& value, const QVariant& reference) const; + virtual void readDelta(Bitstream& in, QVariant& value, const QVariant& reference) const; + + virtual void writeRawDelta(Bitstream& out, const QVariant& value, const QVariant& reference) const; + virtual void readRawDelta(Bitstream& in, QVariant& value, const QVariant& reference) const; virtual void setEnumValue(QVariant& object, int value, const QHash& mappings) const; @@ -919,7 +934,7 @@ public: virtual void setField(QVariant& object, int index, const QVariant& value) const; virtual QVariant getField(const QVariant& object, int index) const; - virtual TypeReader::Type getReaderType() const; + virtual Category getCategory() const; virtual int getBits() const; virtual QMetaEnum getMetaEnum() const; @@ -937,9 +952,12 @@ public: virtual QVariant getValue(const QVariant& object, int index) const; virtual void setValue(QVariant& object, int index, const QVariant& value) const; -private: +protected: + + friend class Bitstream; int _type; + TypeStreamerPointer _self; }; QDebug& operator<<(QDebug& debug, const TypeStreamer* typeStreamer); @@ -971,7 +989,8 @@ public: EnumTypeStreamer(const QMetaEnum& metaEnum); virtual const char* getName() const; - virtual TypeReader::Type getReaderType() const; + virtual void writeMetadata(Bitstream& out, bool full) const; + virtual Category getCategory() const; virtual int getBits() const; virtual QMetaEnum getMetaEnum() const; virtual bool equal(const QVariant& first, const QVariant& second) const; @@ -992,11 +1011,62 @@ private: int _bits; }; +/// A streamer class for enums that maps to a local type. +class MappedEnumTypeStreamer : public TypeStreamer { +public: + + MappedEnumTypeStreamer(const TypeStreamer* baseStreamer, int bits, const QHash& mappings); + + virtual QVariant read(Bitstream& in) const; + virtual void readRawDelta(Bitstream& in, QVariant& object, const QVariant& reference) const; + +private: + + const TypeStreamer* _baseStreamer; + int _bits; + QHash _mappings; +}; + +/// Base class for generic type streamers, which contain all the metadata required to write out a type. +class GenericTypeStreamer : public TypeStreamer { +public: + + GenericTypeStreamer(const QByteArray& name); + + virtual const char* getName() const; + virtual QVariant readVariant(Bitstream& in) const; + +protected: + + friend class Bitstream; + + QByteArray _name; + WeakTypeStreamerPointer _weakSelf; +}; + +/// A streamer for generic enums. +class GenericEnumTypeStreamer : public GenericTypeStreamer { +public: + + GenericEnumTypeStreamer(const QByteArray& name, const QVector& values, int bits, const QByteArray& hash); + + virtual void writeMetadata(Bitstream& out, bool full) const; + virtual void write(Bitstream& out, const QVariant& value) const; + virtual QVariant read(Bitstream& in) const; + virtual Category getCategory() const; + +private: + + QVector _values; + int _bits; + QByteArray _hash; +}; + /// A streamer for types compiled by mtc. template class StreamableTypeStreamer : public SimpleTypeStreamer { public: - virtual TypeReader::Type getReaderType() const { return TypeReader::STREAMABLE_TYPE; } + virtual TypeStreamer::Category getCategory() const { return TypeStreamer::STREAMABLE_CATEGORY; } virtual const QVector& getMetaFields() const { return T::getMetaFields(); } virtual int getFieldIndex(const QByteArray& name) const { return T::getFieldIndex(name); } virtual void setField(QVariant& object, int index, const QVariant& value) const { @@ -1005,6 +1075,40 @@ public: return static_cast(object.constData())->getField(index); } }; +typedef QPair StreamerIndexPair; + +/// A streamer class for streamables that maps to a local type. +class MappedStreamableTypeStreamer : public TypeStreamer { +public: + + MappedStreamableTypeStreamer(const TypeStreamer* baseStreamer, const QVector& fields); + + virtual QVariant read(Bitstream& in) const; + virtual void readRawDelta(Bitstream& in, QVariant& object, const QVariant& reference) const; + +private: + + const TypeStreamer* _baseStreamer; + QVector _fields; +}; + +/// A streamer for generic enums. +class GenericStreamableTypeStreamer : public GenericTypeStreamer { +public: + + GenericStreamableTypeStreamer(const QByteArray& name, const QVector& fields, const QByteArray& hash); + + virtual void writeMetadata(Bitstream& out, bool full) const; + virtual void write(Bitstream& out, const QVariant& value) const; + virtual QVariant read(Bitstream& in) const; + virtual Category getCategory() const; + +private: + + QVector _fields; + QByteArray _hash; +}; + /// Base template for collection streamers. template class CollectionTypeStreamer : public SimpleTypeStreamer { }; @@ -1013,7 +1117,8 @@ template class CollectionTypeStreamer : public SimpleTypeStreamer { template class CollectionTypeStreamer > : public SimpleTypeStreamer > { public: - virtual TypeReader::Type getReaderType() const { return TypeReader::LIST_TYPE; } + virtual void writeMetadata(Bitstream& out, bool full) const { out << getValueStreamer(); } + virtual TypeStreamer::Category getCategory() const { return TypeStreamer::LIST_CATEGORY; } virtual const TypeStreamer* getValueStreamer() const { return Bitstream::getTypeStreamer(qMetaTypeId()); } virtual void insert(QVariant& object, const QVariant& value) const { static_cast*>(object.data())->append(value.value()); } @@ -1029,7 +1134,8 @@ public: template class CollectionTypeStreamer > : public SimpleTypeStreamer > { public: - virtual TypeReader::Type getReaderType() const { return TypeReader::LIST_TYPE; } + virtual void writeMetadata(Bitstream& out, bool full) const { out << getValueStreamer(); } + virtual TypeStreamer::Category getCategory() const { return TypeStreamer::LIST_CATEGORY; } virtual const TypeStreamer* getValueStreamer() const { return Bitstream::getTypeStreamer(qMetaTypeId()); } virtual void insert(QVariant& object, const QVariant& value) const { static_cast*>(object.data())->append(value.value()); } @@ -1041,11 +1147,43 @@ public: static_cast*>(object.data())->replace(index, value.value()); } }; +/// A streamer for lists that maps to a local type. +class MappedListTypeStreamer : public TypeStreamer { +public: + + MappedListTypeStreamer(const TypeStreamer* baseStreamer, const TypeStreamerPointer& valueStreamer); + + virtual QVariant read(Bitstream& in) const; + virtual void readRawDelta(Bitstream& in, QVariant& object, const QVariant& reference) const; + +protected: + + const TypeStreamer* _baseStreamer; + TypeStreamerPointer _valueStreamer; +}; + +/// A streamer for generic lists. +class GenericListTypeStreamer : public GenericTypeStreamer { +public: + + GenericListTypeStreamer(const QByteArray& name, const TypeStreamerPointer& valueStreamer); + + virtual void writeMetadata(Bitstream& out, bool full) const; + virtual void write(Bitstream& out, const QVariant& value) const; + virtual QVariant read(Bitstream& in) const; + virtual Category getCategory() const; + +private: + + TypeStreamerPointer _valueStreamer; +}; + /// A streamer for set types. template class CollectionTypeStreamer > : public SimpleTypeStreamer > { public: - virtual TypeReader::Type getReaderType() const { return TypeReader::SET_TYPE; } + virtual void writeMetadata(Bitstream& out, bool full) const { out << getValueStreamer(); } + virtual TypeStreamer::Category getCategory() const { return TypeStreamer::SET_CATEGORY; } virtual const TypeStreamer* getValueStreamer() const { return Bitstream::getTypeStreamer(qMetaTypeId()); } virtual void insert(QVariant& object, const QVariant& value) const { static_cast*>(object.data())->insert(value.value()); } @@ -1053,11 +1191,30 @@ public: return static_cast*>(object.data())->remove(key.value()); } }; +/// A streamer for sets that maps to a local type. +class MappedSetTypeStreamer : public MappedListTypeStreamer { +public: + + MappedSetTypeStreamer(const TypeStreamer* baseStreamer, const TypeStreamerPointer& valueStreamer); + + virtual void readRawDelta(Bitstream& in, QVariant& object, const QVariant& reference) const; +}; + +/// A streamer for generic sets. +class GenericSetTypeStreamer : public GenericListTypeStreamer { +public: + + GenericSetTypeStreamer(const QByteArray& name, const TypeStreamerPointer& valueStreamer); + + virtual Category getCategory() const; +}; + /// A streamer for hash types. template class CollectionTypeStreamer > : public SimpleTypeStreamer > { public: - virtual TypeReader::Type getReaderType() const { return TypeReader::MAP_TYPE; } + virtual void writeMetadata(Bitstream& out, bool full) const { out << getKeyStreamer() << getValueStreamer(); } + virtual TypeStreamer::Category getCategory() const { return TypeStreamer::MAP_CATEGORY; } virtual const TypeStreamer* getKeyStreamer() const { return Bitstream::getTypeStreamer(qMetaTypeId()); } virtual const TypeStreamer* getValueStreamer() const { return Bitstream::getTypeStreamer(qMetaTypeId()); } virtual void insert(QVariant& object, const QVariant& key, const QVariant& value) const { @@ -1068,6 +1225,48 @@ public: return QVariant::fromValue(static_cast*>(object.constData())->value(key.value())); } }; +/// A streamer for maps that maps to a local type. +class MappedMapTypeStreamer : public TypeStreamer { +public: + + MappedMapTypeStreamer(const TypeStreamer* baseStreamer, const TypeStreamerPointer& keyStreamer, + const TypeStreamerPointer& valueStreamer); + + virtual QVariant read(Bitstream& in) const; + virtual void readRawDelta(Bitstream& in, QVariant& object, const QVariant& reference) const; + +private: + + const TypeStreamer* _baseStreamer; + TypeStreamerPointer _keyStreamer; + TypeStreamerPointer _valueStreamer; +}; + +/// A streamer for generic maps. +class GenericMapTypeStreamer : public GenericTypeStreamer { +public: + + GenericMapTypeStreamer(const QByteArray& name, const TypeStreamerPointer& keyStreamer, + const TypeStreamerPointer& valueStreamer); + + virtual void writeMetadata(Bitstream& out, bool full) const; + virtual void write(Bitstream& out, const QVariant& value) const; + virtual QVariant read(Bitstream& in) const; + virtual Category getCategory() const; + +private: + + TypeStreamerPointer _keyStreamer; + TypeStreamerPointer _valueStreamer; +}; + +/// A streamer class for generic values. +class GenericValueStreamer : public SimpleTypeStreamer { +public: + + virtual void writeVariant(Bitstream& out, const QVariant& value) const; +}; + /// Macro for registering simple type streamers. #define REGISTER_SIMPLE_TYPE_STREAMER(X) static int X##Streamer = \ Bitstream::registerTypeStreamer(qMetaTypeId(), new SimpleTypeStreamer()); diff --git a/libraries/metavoxels/src/ScriptCache.cpp b/libraries/metavoxels/src/ScriptCache.cpp index 9648a047cb..ffd5200a2e 100644 --- a/libraries/metavoxels/src/ScriptCache.cpp +++ b/libraries/metavoxels/src/ScriptCache.cpp @@ -90,6 +90,7 @@ bool operator==(const QScriptValue& first, const QScriptValue& second) { return true; } else { + // if none of the above tests apply, first must be invalid return !second.isValid(); } } diff --git a/libraries/metavoxels/src/SharedObject.cpp b/libraries/metavoxels/src/SharedObject.cpp index 47d69f4abe..05af5f1bf8 100644 --- a/libraries/metavoxels/src/SharedObject.cpp +++ b/libraries/metavoxels/src/SharedObject.cpp @@ -30,6 +30,11 @@ SharedObject::SharedObject() : _weakHash.insert(_id, this); } +void SharedObject::setID(int id) { + _weakHash.remove(_id); + _weakHash.insert(_id = id, this); +} + void SharedObject::incrementReferenceCount() { _referenceCount.ref(); } diff --git a/libraries/metavoxels/src/SharedObject.h b/libraries/metavoxels/src/SharedObject.h index 41c3c01ffe..ba643b449c 100644 --- a/libraries/metavoxels/src/SharedObject.h +++ b/libraries/metavoxels/src/SharedObject.h @@ -41,6 +41,8 @@ public: /// Returns the unique local ID for this object. int getID() const { return _id; } + void setID(int id); + /// Returns the local origin ID for this object. int getOriginID() const { return _originID; } diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index b4aedbcb7c..918261a953 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -55,13 +55,13 @@ AccountManager::AccountManager() : { qRegisterMetaType("OAuthAccessToken"); qRegisterMetaTypeStreamOperators("OAuthAccessToken"); - + qRegisterMetaType("DataServerAccountInfo"); qRegisterMetaTypeStreamOperators("DataServerAccountInfo"); - + qRegisterMetaType("QNetworkAccessManager::Operation"); qRegisterMetaType("JSONCallbackParameters"); - + connect(&_accountInfo, &DataServerAccountInfo::balanceChanged, this, &AccountManager::accountInfoBalanceChanged); } @@ -70,18 +70,18 @@ const QString DOUBLE_SLASH_SUBSTITUTE = "slashslash"; void AccountManager::logout() { // a logout means we want to delete the DataServerAccountInfo we currently have for this URL, in-memory and in file _accountInfo = DataServerAccountInfo(); - + emit balanceChanged(0); connect(&_accountInfo, &DataServerAccountInfo::balanceChanged, this, &AccountManager::accountInfoBalanceChanged); - + QSettings settings; settings.beginGroup(ACCOUNTS_GROUP); - + QString keyURLString(_authURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE)); settings.remove(keyURLString); - + qDebug() << "Removed account info for" << _authURL << "from in-memory accounts and .ini file"; - + emit logoutComplete(); // the username has changed to blank emit usernameChanged(QString()); @@ -93,7 +93,7 @@ void AccountManager::updateBalance() { JSONCallbackParameters callbackParameters; callbackParameters.jsonCallbackReceiver = &_accountInfo; callbackParameters.jsonCallbackMethod = "setBalanceFromJSON"; - + authenticatedRequest("/api/v1/wallets/mine", QNetworkAccessManager::GetOperation, callbackParameters); } } @@ -105,28 +105,33 @@ void AccountManager::accountInfoBalanceChanged(qint64 newBalance) { void AccountManager::setAuthURL(const QUrl& authURL) { if (_authURL != authURL) { _authURL = authURL; - + qDebug() << "URL for node authentication has been changed to" << qPrintable(_authURL.toString()); qDebug() << "Re-setting authentication flow."; - + // check if there are existing access tokens to load from settings QSettings settings; settings.beginGroup(ACCOUNTS_GROUP); - + foreach(const QString& key, settings.allKeys()) { // take a key copy to perform the double slash replacement QString keyCopy(key); QUrl keyURL(keyCopy.replace("slashslash", "//")); - + if (keyURL == _authURL) { // pull out the stored access token and store it in memory _accountInfo = settings.value(key).value(); qDebug() << "Found a data-server access token for" << qPrintable(keyURL.toString()); - - emit accessTokenChanged(); + + // profile info isn't guaranteed to be saved too + if (_accountInfo.hasProfile()) { + emit profileChanged(); + } else { + requestProfile(); + } } } - + // tell listeners that the auth endpoint has changed emit authEndpointChanged(); } @@ -147,36 +152,36 @@ void AccountManager::authenticatedRequest(const QString& path, QNetworkAccessMan void AccountManager::invokedRequest(const QString& path, QNetworkAccessManager::Operation operation, const JSONCallbackParameters& callbackParams, const QByteArray& dataByteArray, QHttpMultiPart* dataMultiPart) { - + if (!_networkAccessManager) { _networkAccessManager = new QNetworkAccessManager(this); } - + if (hasValidAccessToken()) { QNetworkRequest authenticatedRequest; - + QUrl requestURL = _authURL; - + if (path.startsWith("/")) { requestURL.setPath(path); } else { requestURL.setPath("/" + path); } - + requestURL.setQuery("access_token=" + _accountInfo.getAccessToken().token); - + authenticatedRequest.setUrl(requestURL); - + if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qDebug() << "Making an authenticated request to" << qPrintable(requestURL.toString()); - + if (!dataByteArray.isEmpty()) { qDebug() << "The POST/PUT body -" << QString(dataByteArray); } } - + QNetworkReply* networkReply = NULL; - + switch (operation) { case QNetworkAccessManager::GetOperation: networkReply = _networkAccessManager->get(authenticatedRequest); @@ -198,24 +203,24 @@ void AccountManager::invokedRequest(const QString& path, QNetworkAccessManager:: networkReply = _networkAccessManager->put(authenticatedRequest, dataByteArray); } } - + break; default: // other methods not yet handled break; } - + if (networkReply) { if (!callbackParams.isEmpty()) { // if we have information for a callback, insert the callbackParams into our local map _pendingCallbackMap.insert(networkReply, callbackParams); - + if (callbackParams.updateReciever && !callbackParams.updateSlot.isEmpty()) { callbackParams.updateReciever->connect(networkReply, SIGNAL(uploadProgress(qint64, qint64)), callbackParams.updateSlot.toStdString().c_str()); } } - + // if we ended up firing of a request, hook up to it now connect(networkReply, SIGNAL(finished()), SLOT(processReply())); } @@ -224,7 +229,7 @@ void AccountManager::invokedRequest(const QString& path, QNetworkAccessManager:: void AccountManager::processReply() { QNetworkReply* requestReply = reinterpret_cast(sender()); - + if (requestReply->error() == QNetworkReply::NoError) { passSuccessToCallback(requestReply); } else { @@ -235,17 +240,17 @@ void AccountManager::processReply() { void AccountManager::passSuccessToCallback(QNetworkReply* requestReply) { QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); - + JSONCallbackParameters callbackParams = _pendingCallbackMap.value(requestReply); - + if (callbackParams.jsonCallbackReceiver) { // invoke the right method on the callback receiver QMetaObject::invokeMethod(callbackParams.jsonCallbackReceiver, qPrintable(callbackParams.jsonCallbackMethod), Q_ARG(const QJsonObject&, jsonResponse.object())); - + // remove the related reply-callback group from the map _pendingCallbackMap.remove(requestReply); - + } else { if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qDebug() << "Received JSON response from data-server that has no matching callback."; @@ -256,13 +261,13 @@ void AccountManager::passSuccessToCallback(QNetworkReply* requestReply) { void AccountManager::passErrorToCallback(QNetworkReply* requestReply) { JSONCallbackParameters callbackParams = _pendingCallbackMap.value(requestReply); - + if (callbackParams.errorCallbackReceiver) { // invoke the right method on the callback receiver QMetaObject::invokeMethod(callbackParams.errorCallbackReceiver, qPrintable(callbackParams.errorCallbackMethod), Q_ARG(QNetworkReply::NetworkError, requestReply->error()), Q_ARG(const QString&, requestReply->errorString())); - + // remove the related reply-callback group from the map _pendingCallbackMap.remove(requestReply); } else { @@ -274,12 +279,12 @@ void AccountManager::passErrorToCallback(QNetworkReply* requestReply) { } bool AccountManager::hasValidAccessToken() { - + if (_accountInfo.getAccessToken().token.isEmpty() || _accountInfo.getAccessToken().isExpired()) { if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qDebug() << "An access token is required for requests to" << qPrintable(_authURL.toString()); } - + return false; } else { return true; @@ -288,12 +293,12 @@ bool AccountManager::hasValidAccessToken() { bool AccountManager::checkAndSignalForAccessToken() { bool hasToken = hasValidAccessToken(); - + if (!hasToken) { // emit a signal so somebody can call back to us and request an access token given a username and password emit authRequired(); } - + return hasToken; } @@ -304,36 +309,36 @@ void AccountManager::requestAccessToken(const QString& login, const QString& pas } QNetworkRequest request; - + QUrl grantURL = _authURL; grantURL.setPath("/oauth/token"); - + const QString ACCOUNT_MANAGER_REQUESTED_SCOPE = "owner"; - + QByteArray postData; postData.append("grant_type=password&"); postData.append("username=" + login + "&"); postData.append("password=" + QUrl::toPercentEncoding(password) + "&"); postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); - + request.setUrl(grantURL); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - + QNetworkReply* requestReply = _networkAccessManager->post(request, postData); - connect(requestReply, &QNetworkReply::finished, this, &AccountManager::requestFinished); - connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); + connect(requestReply, &QNetworkReply::finished, this, &AccountManager::requestAccessTokenFinished); + connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError))); } -void AccountManager::requestFinished() { +void AccountManager::requestAccessTokenFinished() { QNetworkReply* requestReply = reinterpret_cast(sender()); - + QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); const QJsonObject& rootObject = jsonResponse.object(); - + if (!rootObject.contains("error")) { // construct an OAuthAccessToken from the json object - + if (!rootObject.contains("access_token") || !rootObject.contains("expires_in") || !rootObject.contains("token_type")) { // TODO: error handling - malformed token response @@ -342,23 +347,21 @@ void AccountManager::requestFinished() { // clear the path from the response URL so we have the right root URL for this access token QUrl rootURL = requestReply->url(); rootURL.setPath(""); - + qDebug() << "Storing an account with access-token for" << qPrintable(rootURL.toString()); - - _accountInfo = DataServerAccountInfo(rootObject); - + + _accountInfo = DataServerAccountInfo(); + _accountInfo.setAccessTokenFromJSON(rootObject); + emit loginComplete(rootURL); - // the username has changed to whatever came back - emit usernameChanged(_accountInfo.getUsername()); - - // we have found or requested an access token - emit accessTokenChanged(); - + // store this access token into the local settings QSettings localSettings; localSettings.beginGroup(ACCOUNTS_GROUP); localSettings.setValue(rootURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE), QVariant::fromValue(_accountInfo)); + + requestProfile(); } } else { // TODO: error handling @@ -367,7 +370,53 @@ void AccountManager::requestFinished() { } } -void AccountManager::requestError(QNetworkReply::NetworkError error) { +void AccountManager::requestAccessTokenError(QNetworkReply::NetworkError error) { // TODO: error handling qDebug() << "AccountManager requestError - " << error; } + +void AccountManager::requestProfile() { + if (!_networkAccessManager) { + _networkAccessManager = new QNetworkAccessManager(this); + } + + QUrl profileURL = _authURL; + profileURL.setPath("/api/v1/users/profile"); + profileURL.setQuery("access_token=" + _accountInfo.getAccessToken().token); + + QNetworkReply* profileReply = _networkAccessManager->get(QNetworkRequest(profileURL)); + connect(profileReply, &QNetworkReply::finished, this, &AccountManager::requestProfileFinished); + connect(profileReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestProfileError(QNetworkReply::NetworkError))); +} + +void AccountManager::requestProfileFinished() { + QNetworkReply* profileReply = reinterpret_cast(sender()); + + QJsonDocument jsonResponse = QJsonDocument::fromJson(profileReply->readAll()); + const QJsonObject& rootObject = jsonResponse.object(); + + if (rootObject.contains("status") && rootObject["status"].toString() == "success") { + _accountInfo.setProfileInfoFromJSON(rootObject); + + emit profileChanged(); + + // the username has changed to whatever came back + emit usernameChanged(_accountInfo.getUsername()); + + // store the whole profile into the local settings + QUrl rootURL = profileReply->url(); + rootURL.setPath(""); + QSettings localSettings; + localSettings.beginGroup(ACCOUNTS_GROUP); + localSettings.setValue(rootURL.toString().replace("//", DOUBLE_SLASH_SUBSTITUTE), + QVariant::fromValue(_accountInfo)); + } else { + // TODO: error handling + qDebug() << "Error in response for profile"; + } +} + +void AccountManager::requestProfileError(QNetworkReply::NetworkError error) { + // TODO: error handling + qDebug() << "AccountManager requestProfileError - " << error; +} diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 628b084ea8..c18836ca54 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -23,9 +23,9 @@ class JSONCallbackParameters { public: JSONCallbackParameters(); - + bool isEmpty() const { return !jsonCallbackReceiver && !errorCallbackReceiver; } - + QObject* jsonCallbackReceiver; QString jsonCallbackMethod; QObject* errorCallbackReceiver; @@ -38,30 +38,33 @@ class AccountManager : public QObject { Q_OBJECT public: static AccountManager& getInstance(); - + void authenticatedRequest(const QString& path, QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation, const JSONCallbackParameters& callbackParams = JSONCallbackParameters(), const QByteArray& dataByteArray = QByteArray(), QHttpMultiPart* dataMultiPart = NULL); - + const QUrl& getAuthURL() const { return _authURL; } void setAuthURL(const QUrl& authURL); bool hasAuthEndpoint() { return !_authURL.isEmpty(); } - + bool isLoggedIn() { return !_authURL.isEmpty() && hasValidAccessToken(); } bool hasValidAccessToken(); Q_INVOKABLE bool checkAndSignalForAccessToken(); - + void requestAccessToken(const QString& login, const QString& password); - + void requestProfile(); + const DataServerAccountInfo& getAccountInfo() const { return _accountInfo; } - + void destroy() { delete _networkAccessManager; } - + public slots: - void requestFinished(); - void requestError(QNetworkReply::NetworkError error); + void requestAccessTokenFinished(); + void requestProfileFinished(); + void requestAccessTokenError(QNetworkReply::NetworkError error); + void requestProfileError(QNetworkReply::NetworkError error); void logout(); void updateBalance(); void accountInfoBalanceChanged(qint64 newBalance); @@ -69,7 +72,7 @@ signals: void authRequired(); void authEndpointChanged(); void usernameChanged(const QString& username); - void accessTokenChanged(); + void profileChanged(); void loginComplete(const QUrl& authURL); void loginFailed(); void logoutComplete(); @@ -80,19 +83,19 @@ private: AccountManager(); AccountManager(AccountManager const& other); // not implemented void operator=(AccountManager const& other); // not implemented - + void passSuccessToCallback(QNetworkReply* reply); void passErrorToCallback(QNetworkReply* reply); - + Q_INVOKABLE void invokedRequest(const QString& path, QNetworkAccessManager::Operation operation, const JSONCallbackParameters& callbackParams, const QByteArray& dataByteArray, QHttpMultiPart* dataMultiPart); - + QUrl _authURL; QNetworkAccessManager* _networkAccessManager; QMap _pendingCallbackMap; - + DataServerAccountInfo _accountInfo; }; diff --git a/libraries/networking/src/DataServerAccountInfo.cpp b/libraries/networking/src/DataServerAccountInfo.cpp index 809f083e35..507c085d26 100644 --- a/libraries/networking/src/DataServerAccountInfo.cpp +++ b/libraries/networking/src/DataServerAccountInfo.cpp @@ -21,20 +21,7 @@ DataServerAccountInfo::DataServerAccountInfo() : _balance(0), _hasBalance(false) { - -} -DataServerAccountInfo::DataServerAccountInfo(const QJsonObject& jsonObject) : - _accessToken(jsonObject), - _username(), - _xmppPassword(), - _balance(0), - _hasBalance(false) -{ - QJsonObject userJSONObject = jsonObject["user"].toObject(); - setUsername(userJSONObject["username"].toString()); - setXMPPPassword(userJSONObject["xmpp_password"].toString()); - setDiscourseApiKey(userJSONObject["discourse_api_key"].toString()); } DataServerAccountInfo::DataServerAccountInfo(const DataServerAccountInfo& otherInfo) { @@ -54,7 +41,7 @@ DataServerAccountInfo& DataServerAccountInfo::operator=(const DataServerAccountI void DataServerAccountInfo::swap(DataServerAccountInfo& otherInfo) { using std::swap; - + swap(_accessToken, otherInfo._accessToken); swap(_username, otherInfo._username); swap(_xmppPassword, otherInfo._xmppPassword); @@ -63,10 +50,14 @@ void DataServerAccountInfo::swap(DataServerAccountInfo& otherInfo) { swap(_hasBalance, otherInfo._hasBalance); } +void DataServerAccountInfo::setAccessTokenFromJSON(const QJsonObject& jsonObject) { + _accessToken = OAuthAccessToken(jsonObject); +} + void DataServerAccountInfo::setUsername(const QString& username) { if (_username != username) { _username = username; - + qDebug() << "Username changed to" << username; } } @@ -87,18 +78,29 @@ void DataServerAccountInfo::setBalance(qint64 balance) { if (!_hasBalance || _balance != balance) { _balance = balance; _hasBalance = true; - + emit balanceChanged(_balance); } } void DataServerAccountInfo::setBalanceFromJSON(const QJsonObject& jsonObject) { if (jsonObject["status"].toString() == "success") { - qint64 balanceInSatoshis = jsonObject["data"].toObject()["wallet"].toObject()["balance"].toInt(); + qint64 balanceInSatoshis = jsonObject["data"].toObject()["wallet"].toObject()["balance"].toDouble(); setBalance(balanceInSatoshis); } } +bool DataServerAccountInfo::hasProfile() const { + return _username.length() > 0; +} + +void DataServerAccountInfo::setProfileInfoFromJSON(const QJsonObject& jsonObject) { + QJsonObject user = jsonObject["data"].toObject()["user"].toObject(); + setUsername(user["username"].toString()); + setXMPPPassword(user["xmpp_password"].toString()); + setDiscourseApiKey(user["discourse_api_key"].toString()); +} + QDataStream& operator<<(QDataStream &out, const DataServerAccountInfo& info) { out << info._accessToken << info._username << info._xmppPassword << info._discourseApiKey; return out; diff --git a/libraries/networking/src/DataServerAccountInfo.h b/libraries/networking/src/DataServerAccountInfo.h index e0209326f9..27b776e3ff 100644 --- a/libraries/networking/src/DataServerAccountInfo.h +++ b/libraries/networking/src/DataServerAccountInfo.h @@ -22,12 +22,12 @@ class DataServerAccountInfo : public QObject { Q_OBJECT public: DataServerAccountInfo(); - DataServerAccountInfo(const QJsonObject& jsonObject); DataServerAccountInfo(const DataServerAccountInfo& otherInfo); DataServerAccountInfo& operator=(const DataServerAccountInfo& otherInfo); - + const OAuthAccessToken& getAccessToken() const { return _accessToken; } - + void setAccessTokenFromJSON(const QJsonObject& jsonObject); + const QString& getUsername() const { return _username; } void setUsername(const QString& username); @@ -36,20 +36,25 @@ public: const QString& getDiscourseApiKey() const { return _discourseApiKey; } void setDiscourseApiKey(const QString& discourseApiKey); - + qint64 getBalance() const { return _balance; } + float getBalanceInSatoshis() const { return _balance / SATOSHIS_PER_CREDIT; } void setBalance(qint64 balance); bool hasBalance() const { return _hasBalance; } void setHasBalance(bool hasBalance) { _hasBalance = hasBalance; } Q_INVOKABLE void setBalanceFromJSON(const QJsonObject& jsonObject); + bool hasProfile() const; + + void setProfileInfoFromJSON(const QJsonObject& jsonObject); + friend QDataStream& operator<<(QDataStream &out, const DataServerAccountInfo& info); friend QDataStream& operator>>(QDataStream &in, DataServerAccountInfo& info); signals: qint64 balanceChanged(qint64 newBalance); private: void swap(DataServerAccountInfo& otherInfo); - + OAuthAccessToken _accessToken; QString _username; QString _xmppPassword; diff --git a/libraries/octree/src/AABox.cpp b/libraries/octree/src/AABox.cpp index 7aa4d76134..60f26a5533 100644 --- a/libraries/octree/src/AABox.cpp +++ b/libraries/octree/src/AABox.cpp @@ -180,6 +180,18 @@ static bool findIntersection(float origin, float direction, float corner, float return false; } +// finds the intersection between a ray and the inside facing plane on one axis +static bool findInsideOutIntersection(float origin, float direction, float corner, float size, float& distance) { + if (direction > EPSILON) { + distance = -1.0f * (origin - (corner + size)) / direction; + return true; + } else if (direction < -EPSILON) { + distance = -1.0f * (origin - corner) / direction; + return true; + } + return false; +} + bool AABox::expandedIntersectsSegment(const glm::vec3& start, const glm::vec3& end, float expansion) const { // handle the trivial cases where the expanded box contains the start or end if (expandedContains(start, expansion) || expandedContains(end, expansion)) { @@ -207,9 +219,34 @@ bool AABox::expandedIntersectsSegment(const glm::vec3& start, const glm::vec3& e bool AABox::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face) const { // handle the trivial case where the box contains the origin if (contains(origin)) { + // We still want to calculate the distance from the origin to the inside out plane + float axisDistance; + if ((findInsideOutIntersection(origin.x, direction.x, _corner.x, _scale.x, axisDistance) && axisDistance >= 0 && + isWithin(origin.y + axisDistance*direction.y, _corner.y, _scale.y) && + isWithin(origin.z + axisDistance*direction.z, _corner.z, _scale.z))) { + distance = axisDistance; + face = direction.x > 0 ? MAX_X_FACE : MIN_X_FACE; + return true; + } + if ((findInsideOutIntersection(origin.y, direction.y, _corner.y, _scale.y, axisDistance) && axisDistance >= 0 && + isWithin(origin.x + axisDistance*direction.x, _corner.x, _scale.x) && + isWithin(origin.z + axisDistance*direction.z, _corner.z, _scale.z))) { + distance = axisDistance; + face = direction.y > 0 ? MAX_Y_FACE : MIN_Y_FACE; + return true; + } + if ((findInsideOutIntersection(origin.z, direction.z, _corner.z, _scale.z, axisDistance) && axisDistance >= 0 && + isWithin(origin.y + axisDistance*direction.y, _corner.y, _scale.y) && + isWithin(origin.x + axisDistance*direction.x, _corner.x, _scale.x))) { + distance = axisDistance; + face = direction.z > 0 ? MAX_Z_FACE : MIN_Z_FACE; + return true; + } + // This case is unexpected, but mimics the previous behavior for inside out intersections distance = 0; return true; } + // check each axis float axisDistance; if ((findIntersection(origin.x, direction.x, _corner.x, _scale.x, axisDistance) && axisDistance >= 0 && diff --git a/libraries/octree/src/AACube.cpp b/libraries/octree/src/AACube.cpp index 443d725a38..e359eac9e9 100644 --- a/libraries/octree/src/AACube.cpp +++ b/libraries/octree/src/AACube.cpp @@ -169,6 +169,18 @@ static bool findIntersection(float origin, float direction, float corner, float return false; } +// finds the intersection between a ray and the inside facing plane on one axis +static bool findInsideOutIntersection(float origin, float direction, float corner, float size, float& distance) { + if (direction > EPSILON) { + distance = -1.0f * (origin - (corner + size)) / direction; + return true; + } else if (direction < -EPSILON) { + distance = -1.0f * (origin - corner) / direction; + return true; + } + return false; +} + bool AACube::expandedIntersectsSegment(const glm::vec3& start, const glm::vec3& end, float expansion) const { // handle the trivial cases where the expanded box contains the start or end if (expandedContains(start, expansion) || expandedContains(end, expansion)) { @@ -196,9 +208,35 @@ bool AACube::expandedIntersectsSegment(const glm::vec3& start, const glm::vec3& bool AACube::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face) const { // handle the trivial case where the box contains the origin if (contains(origin)) { + + // We still want to calculate the distance from the origin to the inside out plane + float axisDistance; + if ((findInsideOutIntersection(origin.x, direction.x, _corner.x, _scale, axisDistance) && axisDistance >= 0 && + isWithin(origin.y + axisDistance*direction.y, _corner.y, _scale) && + isWithin(origin.z + axisDistance*direction.z, _corner.z, _scale))) { + distance = axisDistance; + face = direction.x > 0 ? MAX_X_FACE : MIN_X_FACE; + return true; + } + if ((findInsideOutIntersection(origin.y, direction.y, _corner.y, _scale, axisDistance) && axisDistance >= 0 && + isWithin(origin.x + axisDistance*direction.x, _corner.x, _scale) && + isWithin(origin.z + axisDistance*direction.z, _corner.z, _scale))) { + distance = axisDistance; + face = direction.y > 0 ? MAX_Y_FACE : MIN_Y_FACE; + return true; + } + if ((findInsideOutIntersection(origin.z, direction.z, _corner.z, _scale, axisDistance) && axisDistance >= 0 && + isWithin(origin.y + axisDistance*direction.y, _corner.y, _scale) && + isWithin(origin.x + axisDistance*direction.x, _corner.x, _scale))) { + distance = axisDistance; + face = direction.z > 0 ? MAX_Z_FACE : MIN_Z_FACE; + return true; + } + // This case is unexpected, but mimics the previous behavior for inside out intersections distance = 0; return true; } + // check each axis float axisDistance; if ((findIntersection(origin.x, direction.x, _corner.x, _scale, axisDistance) && axisDistance >= 0 && diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index a4aae61248..b0cce114a9 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -147,7 +147,12 @@ ScriptEngine::ScriptEngine(const QUrl& scriptURL, QEventLoop loop; QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); - _scriptContents = reply->readAll(); + if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 200) { + _scriptContents = reply->readAll(); + } else { + qDebug() << "ERROR Loading file:" << url.toString(); + emit errorMessage("ERROR Loading file:" + url.toString()); + } } } } diff --git a/libraries/shared/src/CapsuleShape.cpp b/libraries/shared/src/CapsuleShape.cpp index 8e887107dc..5416ff92a6 100644 --- a/libraries/shared/src/CapsuleShape.cpp +++ b/libraries/shared/src/CapsuleShape.cpp @@ -13,6 +13,8 @@ #include #include "CapsuleShape.h" + +#include "GeometryUtil.h" #include "SharedUtil.h" @@ -84,3 +86,11 @@ void CapsuleShape::setEndPoints(const glm::vec3& startPoint, const glm::vec3& en updateBoundingRadius(); } +bool CapsuleShape::findRayIntersection(const glm::vec3& rayStart, const glm::vec3& rayDirection, float& distance) const { + glm::vec3 capsuleStart, capsuleEnd; + getStartPoint(capsuleStart); + getEndPoint(capsuleEnd); + // NOTE: findRayCapsuleIntersection returns 'true' with distance = 0 when rayStart is inside capsule. + // TODO: implement the raycast to return inside surface intersection for the internal rayStart. + return findRayCapsuleIntersection(rayStart, rayDirection, capsuleStart, capsuleEnd, _radius, distance); +} diff --git a/libraries/shared/src/CapsuleShape.h b/libraries/shared/src/CapsuleShape.h index 756ae18911..fdd6c3eda6 100644 --- a/libraries/shared/src/CapsuleShape.h +++ b/libraries/shared/src/CapsuleShape.h @@ -39,6 +39,8 @@ public: void setRadiusAndHalfHeight(float radius, float height); void setEndPoints(const glm::vec3& startPoint, const glm::vec3& endPoint); + bool findRayIntersection(const glm::vec3& rayStart, const glm::vec3& rayDirection, float& distance) const; + protected: void updateBoundingRadius() { _boundingRadius = _radius + _halfHeight; } diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index e8ab59ce2d..02cce80104 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -19,71 +19,70 @@ #include "HifiConfigVariantMap.h" QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringList& argumentList) { - + QVariantMap mergedMap; - + // Add anything in the CL parameter list to the variant map. // Take anything with a dash in it as a key, and the values after it as the value. - + const QString DASHED_KEY_REGEX_STRING = "(^-{1,2})([\\w-]+)"; QRegExp dashedKeyRegex(DASHED_KEY_REGEX_STRING); - + int keyIndex = argumentList.indexOf(dashedKeyRegex); int nextKeyIndex = 0; - + // check if there is a config file to read where we can pull config info not passed on command line const QString CONFIG_FILE_OPTION = "--config"; - + while (keyIndex != -1) { if (argumentList[keyIndex] != CONFIG_FILE_OPTION) { // we have a key - look forward to see how many values associate to it QString key = dashedKeyRegex.cap(2); - + nextKeyIndex = argumentList.indexOf(dashedKeyRegex, keyIndex + 1); - + if (nextKeyIndex == keyIndex + 1 || keyIndex == argumentList.size() - 1) { - // there's no value associated with this option, it's a boolean - // so add it to the variant map with NULL as value - mergedMap.insertMulti(key, QVariant()); + // this option is simply a switch, so add it to the map with a value of `true` + mergedMap.insertMulti(key, QVariant(true)); } else { - int maxIndex = (nextKeyIndex == -1) ? argumentList.size() - 1: nextKeyIndex; - + int maxIndex = (nextKeyIndex == -1) ? argumentList.size() : nextKeyIndex; + // there's at least one value associated with the option // pull the first value to start QString value = argumentList[keyIndex + 1]; - + // for any extra values, append them, with a space, to the value string - for (int i = keyIndex + 2; i <= maxIndex; i++) { + for (int i = keyIndex + 2; i < maxIndex; i++) { value += " " + argumentList[i]; } - + // add the finalized value to the merged map mergedMap.insert(key, value); } - + keyIndex = nextKeyIndex; } else { keyIndex = argumentList.indexOf(dashedKeyRegex, keyIndex + 1); } } - + int configIndex = argumentList.indexOf(CONFIG_FILE_OPTION); - + if (configIndex != -1) { // we have a config file - try and read it QString configFilePath = argumentList[configIndex + 1]; QFile configFile(configFilePath); - + if (configFile.exists()) { qDebug() << "Reading JSON config file at" << configFilePath; configFile.open(QIODevice::ReadOnly); - + QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll()); QJsonObject rootObject = configDocument.object(); - + // enumerate the keys of the configDocument object foreach(const QString& key, rootObject.keys()) { - + if (!mergedMap.contains(key)) { // no match in existing list, add it mergedMap.insert(key, QVariant(rootObject[key])); @@ -93,6 +92,6 @@ QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringL qDebug() << "Could not find JSON config file at" << configFilePath; } } - + return mergedMap; } diff --git a/libraries/shared/src/ListShape.h b/libraries/shared/src/ListShape.h index 7ba2410a23..17e7d7b2b6 100644 --- a/libraries/shared/src/ListShape.h +++ b/libraries/shared/src/ListShape.h @@ -55,6 +55,9 @@ public: void setShapes(QVector& shapes); + // TODO: either implement this or remove ListShape altogether + bool findRayIntersection(const glm::vec3& rayStart, const glm::vec3& rayDirection, float& distance) const { return false; } + protected: void clear(); void computeBoundingRadius(); diff --git a/libraries/shared/src/PlaneShape.cpp b/libraries/shared/src/PlaneShape.cpp index a8b4468c93..e9563c6d8b 100644 --- a/libraries/shared/src/PlaneShape.cpp +++ b/libraries/shared/src/PlaneShape.cpp @@ -30,7 +30,28 @@ PlaneShape::PlaneShape(const glm::vec4& coefficients) : } } +glm::vec3 PlaneShape::getNormal() const { + return _rotation * UNROTATED_NORMAL; +} + glm::vec4 PlaneShape::getCoefficients() const { glm::vec3 normal = _rotation * UNROTATED_NORMAL; return glm::vec4(normal.x, normal.y, normal.z, -glm::dot(normal, _position)); } + +bool PlaneShape::findRayIntersection(const glm::vec3& rayStart, const glm::vec3& rayDirection, float& distance) const { + glm::vec3 n = getNormal(); + float denominator = glm::dot(n, rayDirection); + if (fabsf(denominator) < EPSILON) { + // line is parallel to plane + return glm::dot(_position - rayStart, n) < EPSILON; + } else { + float d = glm::dot(_position - rayStart, n) / denominator; + if (d > 0.0f) { + // ray points toward plane + distance = d; + return true; + } + } + return false; +} diff --git a/libraries/shared/src/PlaneShape.h b/libraries/shared/src/PlaneShape.h index 524d53ec73..b8a93324b7 100644 --- a/libraries/shared/src/PlaneShape.h +++ b/libraries/shared/src/PlaneShape.h @@ -18,7 +18,10 @@ class PlaneShape : public Shape { public: PlaneShape(const glm::vec4& coefficients = glm::vec4(0.0f, 1.0f, 0.0f, 0.0f)); + glm::vec3 getNormal() const; glm::vec4 getCoefficients() const; + + bool findRayIntersection(const glm::vec3& rayStart, const glm::vec3& rayDirection, float& distance) const; }; #endif // hifi_PlaneShape_h diff --git a/libraries/shared/src/Shape.h b/libraries/shared/src/Shape.h index 87b84ea73b..3926f6cd07 100644 --- a/libraries/shared/src/Shape.h +++ b/libraries/shared/src/Shape.h @@ -38,6 +38,8 @@ public: virtual void setPosition(const glm::vec3& position) { _position = position; } virtual void setRotation(const glm::quat& rotation) { _rotation = rotation; } + virtual bool findRayIntersection(const glm::vec3& rayStart, const glm::vec3& rayDirection, float& distance) const = 0; + protected: // these ctors are protected (used by derived classes only) Shape(Type type) : _type(type), _boundingRadius(0.f), _position(0.f), _rotation() {} diff --git a/libraries/shared/src/ShapeCollider.cpp b/libraries/shared/src/ShapeCollider.cpp index 7c29fbae00..bbedeb401d 100644 --- a/libraries/shared/src/ShapeCollider.cpp +++ b/libraries/shared/src/ShapeCollider.cpp @@ -765,5 +765,24 @@ bool capsuleAACube(const CapsuleShape* capsuleA, const glm::vec3& cubeCenter, fl return sphereAACube(nearestApproach, capsuleA->getRadius(), cubeCenter, cubeSide, collisions); } +bool findRayIntersectionWithShapes(const QVector shapes, const glm::vec3& rayStart, const glm::vec3& rayDirection, float& minDistance) { + float hitDistance = FLT_MAX; + int numShapes = shapes.size(); + for (int i = 0; i < numShapes; ++i) { + Shape* shape = shapes.at(i); + if (shape) { + float distance; + if (shape->findRayIntersection(rayStart, rayDirection, distance)) { + if (distance < hitDistance) { + hitDistance = distance; + } + } + } + } + if (hitDistance < FLT_MAX) { + minDistance = hitDistance; + } + return false; +} } // namespace ShapeCollider diff --git a/libraries/shared/src/ShapeCollider.h b/libraries/shared/src/ShapeCollider.h index 9e83e31571..8261aceaf3 100644 --- a/libraries/shared/src/ShapeCollider.h +++ b/libraries/shared/src/ShapeCollider.h @@ -21,8 +21,8 @@ namespace ShapeCollider { - /// \param shapeA pointer to first shape - /// \param shapeB pointer to second shape + /// \param shapeA pointer to first shape (cannot be NULL) + /// \param shapeB pointer to second shape (cannot be NULL) /// \param collisions[out] collision details /// \return true if shapes collide bool collideShapes(const Shape* shapeA, const Shape* shapeB, CollisionList& collisions); @@ -33,123 +33,130 @@ namespace ShapeCollider { /// \return true if any shapes collide bool collideShapesCoarse(const QVector& shapesA, const QVector& shapesB, CollisionInfo& collision); - /// \param shapeA a pointer to a shape + /// \param shapeA a pointer to a shape (cannot be NULL) /// \param cubeCenter center of cube /// \param cubeSide lenght of side of cube /// \param collisions[out] average collision details /// \return true if shapeA collides with axis aligned cube bool collideShapeWithAACube(const Shape* shapeA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions); - /// \param sphereA pointer to first shape - /// \param sphereB pointer to second shape + /// \param sphereA pointer to first shape (cannot be NULL) + /// \param sphereB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool sphereSphere(const SphereShape* sphereA, const SphereShape* sphereB, CollisionList& collisions); - /// \param sphereA pointer to first shape - /// \param capsuleB pointer to second shape + /// \param sphereA pointer to first shape (cannot be NULL) + /// \param capsuleB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool sphereCapsule(const SphereShape* sphereA, const CapsuleShape* capsuleB, CollisionList& collisions); - /// \param sphereA pointer to first shape - /// \param planeB pointer to second shape + /// \param sphereA pointer to first shape (cannot be NULL) + /// \param planeB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool spherePlane(const SphereShape* sphereA, const PlaneShape* planeB, CollisionList& collisions); - /// \param capsuleA pointer to first shape - /// \param sphereB pointer to second shape + /// \param capsuleA pointer to first shape (cannot be NULL) + /// \param sphereB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool capsuleSphere(const CapsuleShape* capsuleA, const SphereShape* sphereB, CollisionList& collisions); - /// \param capsuleA pointer to first shape - /// \param capsuleB pointer to second shape + /// \param capsuleA pointer to first shape (cannot be NULL) + /// \param capsuleB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool capsuleCapsule(const CapsuleShape* capsuleA, const CapsuleShape* capsuleB, CollisionList& collisions); - /// \param capsuleA pointer to first shape - /// \param planeB pointer to second shape + /// \param capsuleA pointer to first shape (cannot be NULL) + /// \param planeB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool capsulePlane(const CapsuleShape* capsuleA, const PlaneShape* planeB, CollisionList& collisions); - /// \param planeA pointer to first shape - /// \param sphereB pointer to second shape + /// \param planeA pointer to first shape (cannot be NULL) + /// \param sphereB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool planeSphere(const PlaneShape* planeA, const SphereShape* sphereB, CollisionList& collisions); - /// \param planeA pointer to first shape - /// \param capsuleB pointer to second shape + /// \param planeA pointer to first shape (cannot be NULL) + /// \param capsuleB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool planeCapsule(const PlaneShape* planeA, const CapsuleShape* capsuleB, CollisionList& collisions); - /// \param planeA pointer to first shape - /// \param planeB pointer to second shape + /// \param planeA pointer to first shape (cannot be NULL) + /// \param planeB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool planePlane(const PlaneShape* planeA, const PlaneShape* planeB, CollisionList& collisions); - /// \param sphereA pointer to first shape - /// \param listB pointer to second shape + /// \param sphereA pointer to first shape (cannot be NULL) + /// \param listB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool sphereList(const SphereShape* sphereA, const ListShape* listB, CollisionList& collisions); - /// \param capuleA pointer to first shape - /// \param listB pointer to second shape + /// \param capuleA pointer to first shape (cannot be NULL) + /// \param listB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool capsuleList(const CapsuleShape* capsuleA, const ListShape* listB, CollisionList& collisions); - /// \param planeA pointer to first shape - /// \param listB pointer to second shape + /// \param planeA pointer to first shape (cannot be NULL) + /// \param listB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool planeList(const PlaneShape* planeA, const ListShape* listB, CollisionList& collisions); - /// \param listA pointer to first shape - /// \param sphereB pointer to second shape + /// \param listA pointer to first shape (cannot be NULL) + /// \param sphereB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool listSphere(const ListShape* listA, const SphereShape* sphereB, CollisionList& collisions); - /// \param listA pointer to first shape - /// \param capsuleB pointer to second shape + /// \param listA pointer to first shape (cannot be NULL) + /// \param capsuleB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool listCapsule(const ListShape* listA, const CapsuleShape* capsuleB, CollisionList& collisions); - /// \param listA pointer to first shape - /// \param planeB pointer to second shape + /// \param listA pointer to first shape (cannot be NULL) + /// \param planeB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool listPlane(const ListShape* listA, const PlaneShape* planeB, CollisionList& collisions); - /// \param listA pointer to first shape - /// \param capsuleB pointer to second shape + /// \param listA pointer to first shape (cannot be NULL) + /// \param capsuleB pointer to second shape (cannot be NULL) /// \param[out] collisions where to append collision details /// \return true if shapes collide bool listList(const ListShape* listA, const ListShape* listB, CollisionList& collisions); - /// \param sphereA pointer to sphere + /// \param sphereA pointer to sphere (cannot be NULL) /// \param cubeCenter center of cube /// \param cubeSide lenght of side of cube /// \param[out] collisions where to append collision details /// \return true if sphereA collides with axis aligned cube bool sphereAACube(const SphereShape* sphereA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions); - /// \param capsuleA pointer to capsule + /// \param capsuleA pointer to capsule (cannot be NULL) /// \param cubeCenter center of cube /// \param cubeSide lenght of side of cube /// \param[out] collisions where to append collision details /// \return true if capsuleA collides with axis aligned cube bool capsuleAACube(const CapsuleShape* capsuleA, const glm::vec3& cubeCenter, float cubeSide, CollisionList& collisions); + /// \param shapes list of pointers to shapes (shape pointers may be NULL) + /// \param startPoint beginning of ray + /// \param direction direction of ray + /// \param minDistance[out] shortest distance to intersection of ray with a shapes + /// \return true if ray hits any shape in shapes + bool findRayIntersectionWithShapes(const QVector shapes, const glm::vec3& startPoint, const glm::vec3& direction, float& minDistance); + } // namespace ShapeCollider #endif // hifi_ShapeCollider_h diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index f29e8e3345..e4d2e1c835 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include "OctalCode.h" #include "SharedUtil.h" @@ -415,13 +416,17 @@ void printVoxelCode(unsigned char* voxelCode) { #ifdef _WIN32 void usleep(int waitTime) { - __int64 time1 = 0, time2 = 0, sysFreq = 0; - - QueryPerformanceCounter((LARGE_INTEGER *)&time1); - QueryPerformanceFrequency((LARGE_INTEGER *)&sysFreq); - do { - QueryPerformanceCounter((LARGE_INTEGER *)&time2); - } while( (time2 - time1) < waitTime); + const quint64 BUSY_LOOP_USECS = 2000; + quint64 compTime = waitTime + usecTimestampNow(); + quint64 compTimeSleep = compTime - BUSY_LOOP_USECS; + while (true) { + if (usecTimestampNow() < compTimeSleep) { + QThread::msleep(1); + } + if (usecTimestampNow() >= compTime) { + break; + } + } } #endif diff --git a/libraries/shared/src/SphereShape.cpp b/libraries/shared/src/SphereShape.cpp new file mode 100644 index 0000000000..49137fac43 --- /dev/null +++ b/libraries/shared/src/SphereShape.cpp @@ -0,0 +1,44 @@ +// +// SphereShape.cpp +// libraries/shared/src +// +// Created by Andrew Meadows on 2014.06.17 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "SphereShape.h" + +bool SphereShape::findRayIntersection(const glm::vec3& rayStart, const glm::vec3& rayDirection, float& distance) const { + float r2 = _boundingRadius * _boundingRadius; + + // compute closest approach (CA) + float a = glm::dot(_position - rayStart, rayDirection); // a = distance from ray-start to CA + float b2 = glm::distance2(_position, rayStart + a * rayDirection); // b2 = squared distance from sphere-center to CA + if (b2 > r2) { + // ray does not hit sphere + return false; + } + float c = sqrtf(r2 - b2); // c = distance from CA to sphere surface along rayDirection + float d2 = glm::distance2(rayStart, _position); // d2 = squared distance from sphere-center to ray-start + if (a < 0.0f) { + // ray points away from sphere-center + if (d2 > r2) { + // ray starts outside sphere + return false; + } + // ray starts inside sphere + distance = c + a; + } else if (d2 > r2) { + // ray starts outside sphere + distance = a - c; + } else { + // ray starts inside sphere + distance = a + c; + } + return true; +} diff --git a/libraries/shared/src/SphereShape.h b/libraries/shared/src/SphereShape.h index 62783ab340..e87b8acab1 100644 --- a/libraries/shared/src/SphereShape.h +++ b/libraries/shared/src/SphereShape.h @@ -29,6 +29,8 @@ public: float getRadius() const { return _boundingRadius; } void setRadius(float radius) { _boundingRadius = radius; } + + bool findRayIntersection(const glm::vec3& rayStart, const glm::vec3& rayDirection, float& distance) const; }; #endif // hifi_SphereShape_h diff --git a/tests/metavoxels/src/MetavoxelTests.cpp b/tests/metavoxels/src/MetavoxelTests.cpp index 6ec2331b14..6bd99a6c82 100644 --- a/tests/metavoxels/src/MetavoxelTests.cpp +++ b/tests/metavoxels/src/MetavoxelTests.cpp @@ -214,6 +214,35 @@ static bool testSerialization(Bitstream::MetadataType metadataType) { return true; } + // go back to the beginning and read everything as generics + inStream.device()->seek(0); + Bitstream genericIn(inStream, metadataType, Bitstream::ALL_GENERICS); + genericIn >> testObjectReadA; + genericIn >> testObjectReadB; + genericIn >> messageRead; + genericIn >> endRead; + + // reassign the ids + testObjectReadA->setID(testObjectWrittenA->getID()); + testObjectReadA->setOriginID(testObjectWrittenA->getOriginID()); + testObjectReadB->setID(testObjectWrittenB->getID()); + testObjectReadB->setOriginID(testObjectWrittenB->getOriginID()); + + // write it back out and compare + QByteArray compareArray; + QDataStream compareOutStream(&compareArray, QIODevice::WriteOnly); + Bitstream compareOut(compareOutStream, metadataType); + compareOut << testObjectReadA; + compareOut << testObjectReadB; + compareOut << messageRead; + compareOut << endRead; + compareOut.flush(); + + if (array != compareArray) { + qDebug() << "Mismatch between written/generic written streams."; + return true; + } + return false; } diff --git a/tests/octree/src/AABoxCubeTests.cpp b/tests/octree/src/AABoxCubeTests.cpp new file mode 100644 index 0000000000..85787e279b --- /dev/null +++ b/tests/octree/src/AABoxCubeTests.cpp @@ -0,0 +1,100 @@ +// +// AABoxCubeTests.h +// tests/octree/src +// +// Created by Brad Hefta-Gaub on 06/04/2014. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include +#include + +#include "AABoxCubeTests.h" + +void AABoxCubeTests::AABoxCubeTests() { + qDebug() << "******************************************************************************************"; + qDebug() << "AABoxCubeTests::AABoxCubeTests()"; + + { + qDebug() << "Test 1: AABox.findRayIntersection() inside out MIN_X_FACE"; + + glm::vec3 corner(0.0f, 0.0f, 0.0f); + float size = 1.0f; + + AABox box(corner, size); + glm::vec3 origin(0.5f, 0.5f, 0.5f); + glm::vec3 direction(-1.0f, 0.0f, 0.0f); + float distance; + BoxFace face; + + bool intersects = box.findRayIntersection(origin, direction, distance, face); + + if (intersects && distance == 0.5f && face == MIN_X_FACE) { + qDebug() << "Test 1: PASSED"; + } else { + qDebug() << "intersects=" << intersects << "expected=" << true; + qDebug() << "distance=" << distance << "expected=" << 0.5f; + qDebug() << "face=" << face << "expected=" << MIN_X_FACE; + + } + } + + { + qDebug() << "Test 2: AABox.findRayIntersection() inside out MAX_X_FACE"; + + glm::vec3 corner(0.0f, 0.0f, 0.0f); + float size = 1.0f; + + AABox box(corner, size); + glm::vec3 origin(0.5f, 0.5f, 0.5f); + glm::vec3 direction(1.0f, 0.0f, 0.0f); + float distance; + BoxFace face; + + bool intersects = box.findRayIntersection(origin, direction, distance, face); + + if (intersects && distance == 0.5f && face == MAX_X_FACE) { + qDebug() << "Test 2: PASSED"; + } else { + qDebug() << "intersects=" << intersects << "expected=" << true; + qDebug() << "distance=" << distance << "expected=" << 0.5f; + qDebug() << "face=" << face << "expected=" << MAX_X_FACE; + + } + } + + { + qDebug() << "Test 3: AABox.findRayIntersection() outside in"; + + glm::vec3 corner(0.5f, 0.0f, 0.0f); + float size = 0.5f; + + AABox box(corner, size); + glm::vec3 origin(0.25f, 0.25f, 0.25f); + glm::vec3 direction(1.0f, 0.0f, 0.0f); + float distance; + BoxFace face; + + bool intersects = box.findRayIntersection(origin, direction, distance, face); + + if (intersects && distance == 0.25f && face == MIN_X_FACE) { + qDebug() << "Test 3: PASSED"; + } else { + qDebug() << "intersects=" << intersects << "expected=" << true; + qDebug() << "distance=" << distance << "expected=" << 0.5f; + qDebug() << "face=" << face << "expected=" << MIN_X_FACE; + + } + } + + qDebug() << "******************************************************************************************"; +} + +void AABoxCubeTests::runAllTests() { + AABoxCubeTests(); +} diff --git a/tests/octree/src/AABoxCubeTests.h b/tests/octree/src/AABoxCubeTests.h new file mode 100644 index 0000000000..8d1ece51f8 --- /dev/null +++ b/tests/octree/src/AABoxCubeTests.h @@ -0,0 +1,20 @@ +// +// AABoxCubeTests.h +// tests/octree/src +// +// Created by Brad Hefta-Gaub on 06/04/2014. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AABoxCubeTests_h +#define hifi_AABoxCubeTests_h + +namespace AABoxCubeTests { + void AABoxCubeTests(); + void runAllTests(); +} + +#endif // hifi_AABoxCubeTests_h diff --git a/tests/octree/src/OctreeTests.cpp b/tests/octree/src/OctreeTests.cpp index 4f829ddb70..bbd2ef5040 100644 --- a/tests/octree/src/OctreeTests.cpp +++ b/tests/octree/src/OctreeTests.cpp @@ -1,6 +1,6 @@ // // OctreeTests.h -// tests/physics/src +// tests/octree/src // // Created by Brad Hefta-Gaub on 06/04/2014. // Copyright 2014 High Fidelity, Inc. diff --git a/tests/octree/src/OctreeTests.h b/tests/octree/src/OctreeTests.h index f8bb0d8422..11d61b3fe5 100644 --- a/tests/octree/src/OctreeTests.h +++ b/tests/octree/src/OctreeTests.h @@ -1,6 +1,6 @@ // // OctreeTests.h -// tests/physics/src +// tests/octree/src // // Created by Brad Hefta-Gaub on 06/04/2014. // Copyright 2014 High Fidelity, Inc. diff --git a/tests/octree/src/main.cpp b/tests/octree/src/main.cpp index 3869f4d3ae..a48ea7400d 100644 --- a/tests/octree/src/main.cpp +++ b/tests/octree/src/main.cpp @@ -8,15 +8,15 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "AABoxCubeTests.h" #include "OctreeTests.h" #include "SharedUtil.h" int main(int argc, const char* argv[]) { const char* VERBOSE = "--verbose"; bool verbose = cmdOptionExists(argc, argv, VERBOSE); - qDebug() << "OctreeTests::runAllTests()"; - OctreeTests::runAllTests(verbose); + AABoxCubeTests::runAllTests(); return 0; } diff --git a/tests/physics/src/ShapeColliderTests.cpp b/tests/physics/src/ShapeColliderTests.cpp index 7b3d956065..608e012998 100644 --- a/tests/physics/src/ShapeColliderTests.cpp +++ b/tests/physics/src/ShapeColliderTests.cpp @@ -11,6 +11,7 @@ //#include #include +#include #include #include @@ -897,6 +898,405 @@ void ShapeColliderTests::sphereMissesAACube() { } } +void ShapeColliderTests::rayHitsSphere() { + float startDistance = 3.0f; + glm::vec3 rayStart(-startDistance, 0.0f, 0.0f); + glm::vec3 rayDirection(1.0f, 0.0f, 0.0f); + + float radius = 1.0f; + glm::vec3 center(0.0f); + + SphereShape sphere(radius, center); + + // very simple ray along xAxis + { + float distance = FLT_MAX; + if (!sphere.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should intersect sphere" << std::endl; + } + + float expectedDistance = startDistance - radius; + float relativeError = fabsf(distance - expectedDistance) / startDistance; + if (relativeError > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray sphere intersection distance error = " << relativeError << std::endl; + } + } + + // ray along a diagonal axis + { + rayStart = glm::vec3(startDistance, startDistance, 0.0f); + rayDirection = - glm::normalize(rayStart); + + float distance = FLT_MAX; + if (!sphere.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should intersect sphere" << std::endl; + } + + float expectedDistance = SQUARE_ROOT_OF_2 * startDistance - radius; + float relativeError = fabsf(distance - expectedDistance) / startDistance; + if (relativeError > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray sphere intersection distance error = " << relativeError << std::endl; + } + } + + // rotated and displaced ray and sphere + { + startDistance = 7.41f; + radius = 3.917f; + + glm::vec3 axis = glm::normalize(glm::vec3(1.0f, 2.0f, 3.0f)); + glm::quat rotation = glm::angleAxis(0.987654321f, axis); + glm::vec3 translation(35.7f, 2.46f, -1.97f); + + glm::vec3 unrotatedRayDirection(-1.0f, 0.0f, 0.0f); + glm::vec3 untransformedRayStart(startDistance, 0.0f, 0.0f); + + rayStart = rotation * (untransformedRayStart + translation); + rayDirection = rotation * unrotatedRayDirection; + + sphere.setRadius(radius); + sphere.setPosition(rotation * translation); + + float distance = FLT_MAX; + if (!sphere.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should intersect sphere" << std::endl; + } + + float expectedDistance = startDistance - radius; + float relativeError = fabsf(distance - expectedDistance) / startDistance; + if (relativeError > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray sphere intersection distance error = " << relativeError << std::endl; + } + } +} + +void ShapeColliderTests::rayBarelyHitsSphere() { + float radius = 1.0f; + glm::vec3 center(0.0f); + float delta = 2.0f * EPSILON; + + float startDistance = 3.0f; + glm::vec3 rayStart(-startDistance, radius - delta, 0.0f); + glm::vec3 rayDirection(1.0f, 0.0f, 0.0f); + + SphereShape sphere(radius, center); + + // very simple ray along xAxis + float distance = FLT_MAX; + if (!sphere.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should just barely hit sphere" << std::endl; + } + + // translate and rotate the whole system... + glm::vec3 axis = glm::normalize(glm::vec3(1.0f, 2.0f, 3.0f)); + glm::quat rotation = glm::angleAxis(0.987654321f, axis); + glm::vec3 translation(35.7f, 2.46f, -1.97f); + + rayStart = rotation * (rayStart + translation); + rayDirection = rotation * rayDirection; + sphere.setPosition(rotation * translation); + + // ...and test again + distance = FLT_MAX; + if (!sphere.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should just barely hit sphere" << std::endl; + } +} + + +void ShapeColliderTests::rayBarelyMissesSphere() { + // same as the barely-hits case, but this time we move the ray away from sphere + float radius = 1.0f; + glm::vec3 center(0.0f); + float delta = 2.0f * EPSILON; + + float startDistance = 3.0f; + glm::vec3 rayStart(-startDistance, radius + delta, 0.0f); + glm::vec3 rayDirection(1.0f, 0.0f, 0.0f); + + SphereShape sphere(radius, center); + + // very simple ray along xAxis + float distance = FLT_MAX; + if (sphere.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should just barely miss sphere" << std::endl; + } + if (distance != FLT_MAX) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: distance should be unchanged after intersection miss" << std::endl; + } + + // translate and rotate the whole system... + glm::vec3 axis = glm::normalize(glm::vec3(1.0f, 2.0f, 3.0f)); + glm::quat rotation = glm::angleAxis(0.987654321f, axis); + glm::vec3 translation(35.7f, 2.46f, -1.97f); + + rayStart = rotation * (rayStart + translation); + rayDirection = rotation * rayDirection; + sphere.setPosition(rotation * translation); + + // ...and test again + distance = FLT_MAX; + if (sphere.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should just barely miss sphere" << std::endl; + } + if (distance != FLT_MAX) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: distance should be unchanged after intersection miss" << std::endl; + } +} + +void ShapeColliderTests::rayHitsCapsule() { + float startDistance = 3.0f; + float radius = 1.0f; + float halfHeight = 2.0f; + glm::vec3 center(0.0f); + CapsuleShape capsule(radius, halfHeight); + + { // simple test along xAxis + // toward capsule center + glm::vec3 rayStart(startDistance, 0.0f, 0.0f); + glm::vec3 rayDirection(-1.0f, 0.0f, 0.0f); + float distance = FLT_MAX; + if (!capsule.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should hit capsule" << std::endl; + } + float expectedDistance = startDistance - radius; + float relativeError = fabsf(distance - expectedDistance) / startDistance; + if (relativeError > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray capsule intersection distance error = " << relativeError << std::endl; + } + + // toward top of cylindrical wall + rayStart.y = halfHeight; + distance = FLT_MAX; + if (!capsule.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should hit capsule" << std::endl; + } + relativeError = fabsf(distance - expectedDistance) / startDistance; + if (relativeError > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray capsule intersection distance error = " << relativeError << std::endl; + } + + // toward top cap + float delta = 2.0f * EPSILON; + rayStart.y = halfHeight + delta; + distance = FLT_MAX; + if (!capsule.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should hit capsule" << std::endl; + } + relativeError = fabsf(distance - expectedDistance) / startDistance; + if (relativeError > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray capsule intersection distance error = " << relativeError << std::endl; + } + + const float EDGE_CASE_SLOP_FACTOR = 20.0f; + + // toward tip of top cap + rayStart.y = halfHeight + radius - delta; + distance = FLT_MAX; + if (!capsule.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should hit capsule" << std::endl; + } + expectedDistance = startDistance - radius * sqrtf(2.0f * delta); // using small angle approximation of cosine + relativeError = fabsf(distance - expectedDistance) / startDistance; + // for edge cases we allow a LOT of error + if (relativeError > EDGE_CASE_SLOP_FACTOR * EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray capsule intersection distance error = " << relativeError << std::endl; + } + + // toward tip of bottom cap + rayStart.y = - halfHeight - radius + delta; + distance = FLT_MAX; + if (!capsule.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should hit capsule" << std::endl; + } + expectedDistance = startDistance - radius * sqrtf(2.0f * delta); // using small angle approximation of cosine + relativeError = fabsf(distance - expectedDistance) / startDistance; + // for edge cases we allow a LOT of error + if (relativeError > EDGE_CASE_SLOP_FACTOR * EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray capsule intersection distance error = " << relativeError << std::endl; + } + + // toward edge of capsule cylindrical face + rayStart.y = 0.0f; + rayStart.z = radius - delta; + distance = FLT_MAX; + if (!capsule.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should hit capsule" << std::endl; + } + expectedDistance = startDistance - radius * sqrtf(2.0f * delta); // using small angle approximation of cosine + relativeError = fabsf(distance - expectedDistance) / startDistance; + // for edge cases we allow a LOT of error + if (relativeError > EDGE_CASE_SLOP_FACTOR * EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray capsule intersection distance error = " << relativeError << std::endl; + } + } + // TODO: test at steep angles near cylinder/cap junction +} + +void ShapeColliderTests::rayMissesCapsule() { + // same as edge case hit tests, but shifted in the opposite direction + float startDistance = 3.0f; + float radius = 1.0f; + float halfHeight = 2.0f; + glm::vec3 center(0.0f); + CapsuleShape capsule(radius, halfHeight); + + { // simple test along xAxis + // toward capsule center + glm::vec3 rayStart(startDistance, 0.0f, 0.0f); + glm::vec3 rayDirection(-1.0f, 0.0f, 0.0f); + float delta = 2.0f * EPSILON; + + // over top cap + rayStart.y = halfHeight + radius + delta; + float distance = FLT_MAX; + if (capsule.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should miss capsule" << std::endl; + } + if (distance != FLT_MAX) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: distance should be unchanged after intersection miss" << std::endl; + } + + // below bottom cap + rayStart.y = - halfHeight - radius - delta; + distance = FLT_MAX; + if (capsule.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should miss capsule" << std::endl; + } + if (distance != FLT_MAX) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: distance should be unchanged after intersection miss" << std::endl; + } + + // past edge of capsule cylindrical face + rayStart.y = 0.0f; + rayStart.z = radius + delta; + distance = FLT_MAX; + if (capsule.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should miss capsule" << std::endl; + } + if (distance != FLT_MAX) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: distance should be unchanged after intersection miss" << std::endl; + } + } + // TODO: test at steep angles near edge +} + +void ShapeColliderTests::rayHitsPlane() { + // make a simple plane + float planeDistanceFromOrigin = 3.579; + glm::vec3 planePosition(0.0f, planeDistanceFromOrigin, 0.0f); + PlaneShape plane; + plane.setPosition(planePosition); + + // make a simple ray + float startDistance = 1.234f; + glm::vec3 rayStart(-startDistance, 0.0f, 0.0f); + glm::vec3 rayDirection = glm::normalize(glm::vec3(1.0f, 1.0f, 1.0f)); + + float distance = FLT_MAX; + if (!plane.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should hit plane" << std::endl; + } + + float expectedDistance = SQUARE_ROOT_OF_3 * planeDistanceFromOrigin; + float relativeError = fabsf(distance - expectedDistance) / planeDistanceFromOrigin; + if (relativeError > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray plane intersection distance error = " << relativeError << std::endl; + } + + // rotate the whole system and try again + float angle = 37.8f; + glm::vec3 axis = glm::normalize( glm::vec3(-7.0f, 2.8f, 9.3f) ); + glm::quat rotation = glm::angleAxis(angle, axis); + + plane.setPosition(rotation * planePosition); + plane.setRotation(rotation); + rayStart = rotation * rayStart; + rayDirection = rotation * rayDirection; + + distance = FLT_MAX; + if (!plane.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should hit plane" << std::endl; + } + + expectedDistance = SQUARE_ROOT_OF_3 * planeDistanceFromOrigin; + relativeError = fabsf(distance - expectedDistance) / planeDistanceFromOrigin; + if (relativeError > EPSILON) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray plane intersection distance error = " << relativeError << std::endl; + } +} + +void ShapeColliderTests::rayMissesPlane() { + // make a simple plane + float planeDistanceFromOrigin = 3.579; + glm::vec3 planePosition(0.0f, planeDistanceFromOrigin, 0.0f); + PlaneShape plane; + plane.setPosition(planePosition); + + { // parallel rays should miss + float startDistance = 1.234f; + glm::vec3 rayStart(-startDistance, 0.0f, 0.0f); + glm::vec3 rayDirection = glm::normalize(glm::vec3(-1.0f, 0.0f, -1.0f)); + + float distance = FLT_MAX; + if (plane.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should miss plane" << std::endl; + } + if (distance != FLT_MAX) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: distance should be unchanged after intersection miss" << std::endl; + } + + // rotate the whole system and try again + float angle = 37.8f; + glm::vec3 axis = glm::normalize( glm::vec3(-7.0f, 2.8f, 9.3f) ); + glm::quat rotation = glm::angleAxis(angle, axis); + + plane.setPosition(rotation * planePosition); + plane.setRotation(rotation); + rayStart = rotation * rayStart; + rayDirection = rotation * rayDirection; + + distance = FLT_MAX; + if (plane.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should miss plane" << std::endl; + } + if (distance != FLT_MAX) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: distance should be unchanged after intersection miss" << std::endl; + } + } + + { // make a simple ray that points away from plane + float startDistance = 1.234f; + glm::vec3 rayStart(-startDistance, 0.0f, 0.0f); + glm::vec3 rayDirection = glm::normalize(glm::vec3(-1.0f, -1.0f, -1.0f)); + + float distance = FLT_MAX; + if (plane.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should miss plane" << std::endl; + } + if (distance != FLT_MAX) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: distance should be unchanged after intersection miss" << std::endl; + } + + // rotate the whole system and try again + float angle = 37.8f; + glm::vec3 axis = glm::normalize( glm::vec3(-7.0f, 2.8f, 9.3f) ); + glm::quat rotation = glm::angleAxis(angle, axis); + + plane.setPosition(rotation * planePosition); + plane.setRotation(rotation); + rayStart = rotation * rayStart; + rayDirection = rotation * rayDirection; + + distance = FLT_MAX; + if (plane.findRayIntersection(rayStart, rayDirection, distance)) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: ray should miss plane" << std::endl; + } + if (distance != FLT_MAX) { + std::cout << __FILE__ << ":" << __LINE__ << " ERROR: distance should be unchanged after intersection miss" << std::endl; + } + } +} void ShapeColliderTests::runAllTests() { sphereMissesSphere(); @@ -911,4 +1311,12 @@ void ShapeColliderTests::runAllTests() { sphereTouchesAACubeFaces(); sphereTouchesAACubeEdges(); sphereMissesAACube(); + + rayHitsSphere(); + rayBarelyHitsSphere(); + rayBarelyMissesSphere(); + rayHitsCapsule(); + rayMissesCapsule(); + rayHitsPlane(); + rayMissesPlane(); } diff --git a/tests/physics/src/ShapeColliderTests.h b/tests/physics/src/ShapeColliderTests.h index b51c48a61e..fd9f1f9706 100644 --- a/tests/physics/src/ShapeColliderTests.h +++ b/tests/physics/src/ShapeColliderTests.h @@ -27,6 +27,14 @@ namespace ShapeColliderTests { void sphereTouchesAACubeEdges(); void sphereMissesAACube(); + void rayHitsSphere(); + void rayBarelyHitsSphere(); + void rayBarelyMissesSphere(); + void rayHitsCapsule(); + void rayMissesCapsule(); + void rayHitsPlane(); + void rayMissesPlane(); + void runAllTests(); }