diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index 0421195612..e657587a7a 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -13,9 +13,25 @@ setup_memory_debugger() link_hifi_libraries( audio avatars octree gpu model fbx entities networking animation recording shared script-engine embedded-webserver - controllers physics plugins midi baking image + controllers physics plugins midi image ) +add_dependencies(${TARGET_NAME} oven) + +if (WIN32) + add_custom_command( + TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + $ + $) +else() + add_custom_command( + TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E create_symlink + $ + $/oven) +endif() + if (WIN32) package_libraries_for_deployment() endif() diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index ca0f222e0c..1b533f10f3 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -29,11 +29,10 @@ #include #include -#include -#include #include #include #include +#include #include "AssetServerLogging.h" #include "BakeAssetTask.h" @@ -250,7 +249,7 @@ AssetServer::AssetServer(ReceivedMessage& message) : image::setNormalTexturesCompressionEnabled(true); image::setCubeTexturesCompressionEnabled(true); - BAKEABLE_TEXTURE_EXTENSIONS = TextureBaker::getSupportedFormats(); + BAKEABLE_TEXTURE_EXTENSIONS = image::getSupportedFormats(); qDebug() << "Supported baking texture formats:" << BAKEABLE_MODEL_EXTENSIONS; // Most of the work will be I/O bound, reading from disk and constructing packet objects, @@ -416,6 +415,9 @@ void AssetServer::completeSetup() { if (assetsFilesizeLimit != 0 && assetsFilesizeLimit < MAX_UPLOAD_SIZE) { _filesizeLimit = assetsFilesizeLimit * BITS_PER_MEGABITS; } + + PathUtils::removeTemporaryApplicationDirs(); + PathUtils::removeTemporaryApplicationDirs("Oven"); } void AssetServer::cleanupUnmappedFiles() { diff --git a/assignment-client/src/assets/BakeAssetTask.cpp b/assignment-client/src/assets/BakeAssetTask.cpp index 6c78d2baf3..49322ca4cb 100644 --- a/assignment-client/src/assets/BakeAssetTask.cpp +++ b/assignment-client/src/assets/BakeAssetTask.cpp @@ -11,11 +11,18 @@ #include "BakeAssetTask.h" -#include +#include + +#include +#include -#include #include -#include + +static const int OVEN_STATUS_CODE_SUCCESS { 0 }; +static const int OVEN_STATUS_CODE_FAIL { 1 }; +static const int OVEN_STATUS_CODE_ABORT { 2 }; + +std::once_flag registerMetaTypesFlag; BakeAssetTask::BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) : _assetHash(assetHash), @@ -23,6 +30,10 @@ BakeAssetTask::BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetP _filePath(filePath) { + std::call_once(registerMetaTypesFlag, []() { + qRegisterMetaType("QProcess::ProcessError"); + qRegisterMetaType("QProcess::ExitStatus"); + }); } void cleanupTempFiles(QString tempOutputDir, std::vector files) { @@ -41,67 +52,76 @@ void cleanupTempFiles(QString tempOutputDir, std::vector files) { }; void BakeAssetTask::run() { - _isBaking.store(true); - - qRegisterMetaType >("QVector"); - TextureBakerThreadGetter fn = []() -> QThread* { return QThread::currentThread(); }; - - QString tempOutputDir; - - if (_assetPath.endsWith(".fbx")) { - tempOutputDir = PathUtils::generateTemporaryDir(); - _baker = std::unique_ptr { - new FBXBaker(QUrl("file:///" + _filePath), fn, tempOutputDir) - }; - } else if (_assetPath.endsWith(".js", Qt::CaseInsensitive)) { - _baker = std::unique_ptr{ - new JSBaker(QUrl("file:///" + _filePath), PathUtils::generateTemporaryDir()) - }; - } else { - tempOutputDir = PathUtils::generateTemporaryDir(); - _baker = std::unique_ptr { - new TextureBaker(QUrl("file:///" + _filePath), image::TextureUsage::CUBE_TEXTURE, - tempOutputDir) - }; + if (_isBaking.exchange(true)) { + qWarning() << "Tried to start bake asset task while already baking"; + return; } - QEventLoop loop; - connect(_baker.get(), &Baker::finished, &loop, &QEventLoop::quit); - connect(_baker.get(), &Baker::aborted, &loop, &QEventLoop::quit); - QMetaObject::invokeMethod(_baker.get(), "bake", Qt::QueuedConnection); - loop.exec(); + QString tempOutputDir = PathUtils::generateTemporaryDir(); + auto base = QFileInfo(QCoreApplication::applicationFilePath()).absoluteDir(); + QString path = base.absolutePath() + "/oven"; + QString extension = _assetPath.mid(_assetPath.lastIndexOf('.') + 1); + QStringList args { + "-i", _filePath, + "-o", tempOutputDir, + "-t", extension, + }; - if (_baker->wasAborted()) { - qDebug() << "Aborted baking: " << _assetHash << _assetPath; + _ovenProcess.reset(new QProcess()); - _wasAborted.store(true); + connect(_ovenProcess.get(), static_cast(&QProcess::finished), + this, [this, tempOutputDir](int exitCode, QProcess::ExitStatus exitStatus) { + qDebug() << "Baking process finished: " << exitCode << exitStatus; - cleanupTempFiles(tempOutputDir, _baker->getOutputFiles()); + if (exitStatus == QProcess::CrashExit) { + if (_wasAborted) { + emit bakeAborted(_assetHash, _assetPath); + } else { + QString errors = "Fatal error occurred while baking"; + emit bakeFailed(_assetHash, _assetPath, errors); + } + } else if (exitCode == OVEN_STATUS_CODE_SUCCESS) { + QDir outputDir = tempOutputDir; + auto files = outputDir.entryInfoList(QDir::Files); + QVector outputFiles; + for (auto& file : files) { + outputFiles.push_back(file.absoluteFilePath()); + } - emit bakeAborted(_assetHash, _assetPath); - } else if (_baker->hasErrors()) { - qDebug() << "Failed to bake: " << _assetHash << _assetPath << _baker->getErrors(); + emit bakeComplete(_assetHash, _assetPath, tempOutputDir, outputFiles); + } else if (exitStatus == QProcess::NormalExit && exitCode == OVEN_STATUS_CODE_ABORT) { + _wasAborted.store(true); + emit bakeAborted(_assetHash, _assetPath); + } else { + QString errors; + if (exitCode == OVEN_STATUS_CODE_FAIL) { + QDir outputDir = tempOutputDir; + auto errorFilePath = outputDir.absoluteFilePath("errors.txt"); + QFile errorFile { errorFilePath }; + if (errorFile.open(QIODevice::ReadOnly)) { + errors = errorFile.readAll(); + errorFile.close(); + } else { + errors = "Unknown error occurred while baking"; + } + } + emit bakeFailed(_assetHash, _assetPath, errors); + } - auto errors = _baker->getErrors().join('\n'); // Join error list into a single string for convenience - - _didFinish.store(true); - - cleanupTempFiles(tempOutputDir, _baker->getOutputFiles()); + }); + qDebug() << "Starting oven for " << _assetPath; + _ovenProcess->start(path, args, QIODevice::ReadOnly); + if (!_ovenProcess->waitForStarted(-1)) { + QString errors = "Oven process failed to start"; emit bakeFailed(_assetHash, _assetPath, errors); - } else { - auto vectorOutputFiles = QVector::fromStdVector(_baker->getOutputFiles()); - - qDebug() << "Finished baking: " << _assetHash << _assetPath << vectorOutputFiles; - - _didFinish.store(true); - - emit bakeComplete(_assetHash, _assetPath, tempOutputDir, vectorOutputFiles); + return; } + _ovenProcess->waitForFinished(); } void BakeAssetTask::abort() { - if (_baker) { - _baker->abort(); + if (!_wasAborted.exchange(true)) { + _ovenProcess->terminate(); } } diff --git a/assignment-client/src/assets/BakeAssetTask.h b/assignment-client/src/assets/BakeAssetTask.h index 90458ac223..c73a8bff65 100644 --- a/assignment-client/src/assets/BakeAssetTask.h +++ b/assignment-client/src/assets/BakeAssetTask.h @@ -17,9 +17,10 @@ #include #include #include +#include +#include #include -#include class BakeAssetTask : public QObject, public QRunnable { Q_OBJECT @@ -32,7 +33,6 @@ public: void abort(); bool wasAborted() const { return _wasAborted.load(); } - bool didFinish() const { return _didFinish.load(); } signals: void bakeComplete(QString assetHash, QString assetPath, QString tempOutputDir, QVector outputFiles); @@ -44,9 +44,8 @@ private: AssetHash _assetHash; AssetPath _assetPath; QString _filePath; - std::unique_ptr _baker; + std::unique_ptr _ovenProcess { nullptr }; std::atomic _wasAborted { false }; - std::atomic _didFinish { false }; }; #endif // hifi_BakeAssetTask_h diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index c67e998dd4..3ca924b007 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -870,8 +870,8 @@ AvatarMixerClientData* AvatarMixer::getOrCreateClientData(SharedNodePointer node node->setLinkedData(std::unique_ptr { new AvatarMixerClientData(node->getUUID()) }); clientData = dynamic_cast(node->getLinkedData()); auto& avatar = clientData->getAvatar(); - avatar.setDomainMinimumScale(_domainMinimumScale); - avatar.setDomainMaximumScale(_domainMaximumScale); + avatar.setDomainMinimumHeight(_domainMinimumHeight); + avatar.setDomainMaximumHeight(_domainMaximumHeight); } return clientData; @@ -939,21 +939,21 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { const QString AVATARS_SETTINGS_KEY = "avatars"; - static const QString MIN_SCALE_OPTION = "min_avatar_scale"; - float settingMinScale = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MIN_SCALE_OPTION].toDouble(MIN_AVATAR_SCALE); - _domainMinimumScale = glm::clamp(settingMinScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE); + static const QString MIN_HEIGHT_OPTION = "min_avatar_height"; + float settingMinHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT); + _domainMinimumHeight = glm::clamp(settingMinHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); - static const QString MAX_SCALE_OPTION = "max_avatar_scale"; - float settingMaxScale = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MAX_SCALE_OPTION].toDouble(MAX_AVATAR_SCALE); - _domainMaximumScale = glm::clamp(settingMaxScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE); + static const QString MAX_HEIGHT_OPTION = "max_avatar_height"; + float settingMaxHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT); + _domainMaximumHeight = glm::clamp(settingMaxHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); // make sure that the domain owner didn't flip min and max - if (_domainMinimumScale > _domainMaximumScale) { - std::swap(_domainMinimumScale, _domainMaximumScale); + if (_domainMinimumHeight > _domainMaximumHeight) { + std::swap(_domainMinimumHeight, _domainMaximumHeight); } - qCDebug(avatars) << "This domain requires a minimum avatar scale of" << _domainMinimumScale - << "and a maximum avatar scale of" << _domainMaximumScale; + qCDebug(avatars) << "This domain requires a minimum avatar height of" << _domainMinimumHeight + << "and a maximum avatar height of" << _domainMaximumHeight; const QString AVATAR_WHITELIST_DEFAULT{ "" }; static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist"; diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 610da8ad57..cb5f536faa 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -90,8 +90,8 @@ private: float _maxKbpsPerNode = 0.0f; - float _domainMinimumScale { MIN_AVATAR_SCALE }; - float _domainMaximumScale { MAX_AVATAR_SCALE }; + float _domainMinimumHeight { MIN_AVATAR_HEIGHT }; + float _domainMaximumHeight { MAX_AVATAR_HEIGHT }; RateCounter<> _broadcastRate; p_high_resolution_clock::time_point _lastDebugMessage; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index a4bf8fa253..288652715a 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -25,6 +25,23 @@ AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID) : _avatar->setID(nodeID); } +uint64_t AvatarMixerClientData::getLastOtherAvatarEncodeTime(QUuid otherAvatar) const { + std::unordered_map::const_iterator itr = _lastOtherAvatarEncodeTime.find(otherAvatar); + if (itr != _lastOtherAvatarEncodeTime.end()) { + return itr->second; + } + return 0; +} + +void AvatarMixerClientData::setLastOtherAvatarEncodeTime(const QUuid& otherAvatar, const uint64_t& time) { + std::unordered_map::iterator itr = _lastOtherAvatarEncodeTime.find(otherAvatar); + if (itr != _lastOtherAvatarEncodeTime.end()) { + itr->second = time; + } else { + _lastOtherAvatarEncodeTime.emplace(std::pair(otherAvatar, time)); + } +} + void AvatarMixerClientData::queuePacket(QSharedPointer message, SharedNodePointer node) { if (!_packetQueue.node) { _packetQueue.node = node; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index d5c7784da7..acd9be0702 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -110,16 +110,10 @@ public: bool getRequestsDomainListData() { return _requestsDomainListData; } void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; } - ViewFrustum getViewFrustom() const { return _currentViewFrustum; } + ViewFrustum getViewFrustum() const { return _currentViewFrustum; } - quint64 getLastOtherAvatarEncodeTime(QUuid otherAvatar) { - quint64 result = 0; - if (_lastOtherAvatarEncodeTime.find(otherAvatar) != _lastOtherAvatarEncodeTime.end()) { - result = _lastOtherAvatarEncodeTime[otherAvatar]; - } - _lastOtherAvatarEncodeTime[otherAvatar] = usecTimestampNow(); - return result; - } + uint64_t getLastOtherAvatarEncodeTime(QUuid otherAvatar) const; + void setLastOtherAvatarEncodeTime(const QUuid& otherAvatar, const uint64_t& time); QVector& getLastOtherAvatarSentJoints(QUuid otherAvatar) { _lastOtherAvatarSentJoints[otherAvatar].resize(_avatar->getJointCount()); @@ -143,7 +137,7 @@ private: // this is a map of the last time we encoded an "other" avatar for // sending to "this" node - std::unordered_map _lastOtherAvatarEncodeTime; + std::unordered_map _lastOtherAvatarEncodeTime; std::unordered_map> _lastOtherAvatarSentJoints; uint64_t _identityChangeTimestamp; diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index dd045c24ea..9ea1ed3637 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,10 @@ #include "AvatarMixerClientData.h" #include "AvatarMixerSlave.h" +namespace PrioritySortUtil { + // we declare this callback here but override it later + std::function getAvatarAgeCallback = [] (const AvatarSharedPointer& avatar) { return 0; }; +} void AvatarMixerSlave::configure(ConstIter begin, ConstIter end) { _begin = begin; @@ -184,10 +189,8 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // setup list of AvatarData as well as maps to map betweeen the AvatarData and the original nodes - // for calling the AvatarData::sortAvatars() function and getting our sorted list of client nodes - QList avatarList; + std::vector avatarsToSort; std::unordered_map avatarDataToNodes; - std::for_each(_begin, _end, [&](const SharedNodePointer& otherNode) { // make sure this is an agent that we have avatar data for before considering it for inclusion if (otherNode->getType() == NodeType::Agent @@ -195,36 +198,61 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) const AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); AvatarSharedPointer otherAvatar = otherNodeData->getAvatarSharedPointer(); - avatarList << otherAvatar; + avatarsToSort.push_back(otherAvatar); avatarDataToNodes[otherAvatar] = otherNode; } }); - AvatarSharedPointer thisAvatar = nodeData->getAvatarSharedPointer(); - ViewFrustum cameraView = nodeData->getViewFrustom(); - std::priority_queue sortedAvatars; - AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars, - [&](AvatarSharedPointer avatar)->uint64_t { - auto avatarNode = avatarDataToNodes[avatar]; - assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map - return nodeData->getLastBroadcastTime(avatarNode->getUUID()); - }, [&](AvatarSharedPointer avatar)->float{ - glm::vec3 nodeBoxHalfScale = (avatar->getWorldPosition() - avatar->getGlobalBoundingBoxCorner() * avatar->getSensorToWorldScale()); - return glm::max(nodeBoxHalfScale.x, glm::max(nodeBoxHalfScale.y, nodeBoxHalfScale.z)); - }, [&](AvatarSharedPointer avatar)->bool { + // now that we've assembled the avatarDataToNodes map we can replace PrioritySortUtil::getAvatarAgeCallback + // with the true implementation + PrioritySortUtil::getAvatarAgeCallback = [&] (const AvatarSharedPointer& avatar) { + auto avatarNode = avatarDataToNodes[avatar]; + assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map + return nodeData->getLastOtherAvatarEncodeTime(avatarNode->getUUID()); + }; + + class SortableAvatar: public PrioritySortUtil::Sortable { + public: + SortableAvatar() = delete; + SortableAvatar(const AvatarSharedPointer& avatar) : _avatar(avatar) {} + glm::vec3 getPosition() const override { return _avatar->getWorldPosition(); } + float getRadius() const override { + glm::vec3 nodeBoxHalfScale = (_avatar->getWorldPosition() - _avatar->getGlobalBoundingBoxCorner() * _avatar->getSensorToWorldScale()); + return glm::max(nodeBoxHalfScale.x, glm::max(nodeBoxHalfScale.y, nodeBoxHalfScale.z)); + } + uint64_t getTimestamp() const override { + // use the callback implemented above + return PrioritySortUtil::getAvatarAgeCallback(_avatar); + } + const AvatarSharedPointer& getAvatar() const { return _avatar; } + + private: + AvatarSharedPointer _avatar; + }; + + // prepare to sort + ViewFrustum cameraView = nodeData->getViewFrustum(); + PrioritySortUtil::PriorityQueue sortedAvatars(cameraView, + AvatarData::_avatarSortCoefficientSize, + AvatarData::_avatarSortCoefficientCenter, + AvatarData::_avatarSortCoefficientAge); + + // ignore or sort + const AvatarSharedPointer& thisAvatar = nodeData->getAvatarSharedPointer(); + for (const auto& avatar : avatarsToSort) { if (avatar == thisAvatar) { - return true; // ignore ourselves... + // don't echo updates to self + continue; } bool shouldIgnore = false; - - // We will also ignore other nodes for a couple of different reasons: + // We ignore other nodes for a couple of reasons: // 1) ignore bubbles and ignore specific node // 2) the node hasn't really updated it's frame data recently, this can // happen if for example the avatar is connected on a desktop and sending // updates at ~30hz. So every 3 frames we skip a frame. - auto avatarNode = avatarDataToNodes[avatar]; + auto avatarNode = avatarDataToNodes[avatar]; assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map const AvatarMixerClientData* avatarNodeData = reinterpret_cast(avatarNode->getLinkedData()); @@ -240,7 +268,6 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) || (avatarNode->isIgnoringNodeWithID(node->getUUID()) && !getsAnyIgnored)) { shouldIgnore = true; } else { - // Check to see if the space bubble is enabled // Don't bother with these checks if the other avatar has their bubble enabled and we're gettingAnyIgnored if (node->isIgnoreRadiusEnabled() || (avatarNode->isIgnoreRadiusEnabled() && !getsAnyIgnored)) { @@ -267,8 +294,6 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) nodeData->removeFromRadiusIgnoringSet(node, avatarNode->getUUID()); } } - quint64 endIgnoreCalculation = usecTimestampNow(); - _stats.ignoreCalculationElapsedTime += (endIgnoreCalculation - startIgnoreCalculation); if (!shouldIgnore) { AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getUUID()); @@ -292,20 +317,21 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) ++numAvatarsWithSkippedFrames; } } - return shouldIgnore; - }); + quint64 endIgnoreCalculation = usecTimestampNow(); + _stats.ignoreCalculationElapsedTime += (endIgnoreCalculation - startIgnoreCalculation); + + if (!shouldIgnore) { + // sort this one for later + sortedAvatars.push(SortableAvatar(avatar)); + } + } // loop through our sorted avatars and allocate our bandwidth to them accordingly - int avatarRank = 0; - // this is overly conservative, because it includes some avatars we might not consider int remainingAvatars = (int)sortedAvatars.size(); - while (!sortedAvatars.empty()) { - AvatarPriority sortData = sortedAvatars.top(); + const auto& avatarData = sortedAvatars.top().getAvatar(); sortedAvatars.pop(); - const auto& avatarData = sortData.avatar; - avatarRank++; remainingAvatars--; auto otherNode = avatarDataToNodes[avatarData]; @@ -332,10 +358,8 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) nodeData->setLastBroadcastTime(otherNode->getUUID(), usecTimestampNow()); } + // determine if avatar is in view which determines how much data to send glm::vec3 otherPosition = otherAvatar->getClientGlobalPosition(); - - - // determine if avatar is in view, to determine how much data to include... glm::vec3 otherNodeBoxScale = (otherPosition - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f * otherAvatar->getSensorToWorldScale(); AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale); bool isInView = nodeData->otherAvatarInView(otherNodeBox); @@ -405,14 +429,18 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // set the last sent sequence number for this sender on the receiver nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(), otherNodeData->getLastReceivedSequenceNumber()); + nodeData->setLastOtherAvatarEncodeTime(otherNode->getUUID(), usecTimestampNow()); } + } else { + // TODO? this avatar is not included now, and will probably not be included next frame. + // It would be nice if we could tweak its future sort priority to put it at the back of the list. } avatarPacketList->endSegment(); quint64 endAvatarDataPacking = usecTimestampNow(); _stats.avatarDataPackingElapsedTime += (endAvatarDataPacking - startAvatarDataPacking); - }; + } quint64 startPacketSending = usecTimestampNow(); diff --git a/cmake/macros/GenerateInstallers.cmake b/cmake/macros/GenerateInstallers.cmake index 6c131168d5..702636dd01 100644 --- a/cmake/macros/GenerateInstallers.cmake +++ b/cmake/macros/GenerateInstallers.cmake @@ -29,10 +29,6 @@ macro(GENERATE_INSTALLERS) if (WIN32) - # Do not install the Visual Studio C runtime libraries. The installer will do this automatically - set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) - - include(InstallRequiredSystemLibraries) set(CPACK_NSIS_MUI_ICON "${HF_CMAKE_DIR}/installer/installer.ico") # install and reference the Add/Remove icon @@ -49,6 +45,10 @@ macro(GENERATE_INSTALLERS) set(_UNINSTALLER_HEADER_BAD_PATH "${HF_CMAKE_DIR}/installer/uninstaller-header.bmp") set(UNINSTALLER_HEADER_IMAGE "") fix_path_for_nsis(${_UNINSTALLER_HEADER_BAD_PATH} UNINSTALLER_HEADER_IMAGE) + + # grab the latest VC redist (2017) and add it to the installer, our NSIS template + # will call it during the install + install(CODE "file(DOWNLOAD https://go.microsoft.com/fwlink/?LinkId=746572 \"\${CMAKE_INSTALL_PREFIX}/vcredist_x64.exe\")") elseif (APPLE) # produce a drag and drop DMG on OS X set(CPACK_GENERATOR "DragNDrop") @@ -84,4 +84,3 @@ macro(GENERATE_INSTALLERS) include(CPack) endmacro() - diff --git a/cmake/templates/FixupBundlePostBuild.cmake.in b/cmake/templates/FixupBundlePostBuild.cmake.in index d4726884c2..57379bb48b 100644 --- a/cmake/templates/FixupBundlePostBuild.cmake.in +++ b/cmake/templates/FixupBundlePostBuild.cmake.in @@ -45,5 +45,4 @@ else() endif() file(GLOB EXTRA_PLUGINS "${BUNDLE_PLUGIN_DIR}/*.${PLUGIN_EXTENSION}") -fixup_bundle("${BUNDLE_EXECUTABLE}" "${EXTRA_PLUGINS}" "@FIXUP_LIBS@") - +fixup_bundle("${BUNDLE_EXECUTABLE}" "${EXTRA_PLUGINS}" "@FIXUP_LIBS@" IGNORE_ITEM "vcredist_x86.exe;vcredist_x64.exe") diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 31f224667c..c6d118a87b 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 2.0, + "version": 2.1, "settings": [ { "name": "label", @@ -1015,20 +1015,20 @@ "assignment-types": [ 1, 2 ], "settings": [ { - "name": "min_avatar_scale", + "name": "min_avatar_height", "type": "double", - "label": "Minimum Avatar Scale", - "help": "Limits the scale of avatars in your domain. Must be at least 0.005.", - "placeholder": 0.25, - "default": 0.25 + "label": "Minimum Avatar Height (meters)", + "help": "Limits the height of avatars in your domain. Must be at least 0.009.", + "placeholder": 0.4, + "default": 0.4 }, { - "name": "max_avatar_scale", + "name": "max_avatar_height", "type": "double", - "label": "Maximum Avatar Scale", - "help": "Limits the scale of avatars in your domain. Cannot be greater than 1000.", - "placeholder": 3.0, - "default": 3.0 + "label": "Maximum Avatar Height (meters)", + "help": "Limits the scale of avatars in your domain. Cannot be greater than 1755.", + "placeholder": 5.2, + "default": 5.2 }, { "name": "avatar_whitelist", diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index a4d3e675aa..05227d35b7 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -304,6 +304,26 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList *wizardCompletedOnce = QVariant(true); } + if (oldVersion < 2.1) { + // convert old avatar scale settings into avatar height. + + const QString AVATAR_MIN_SCALE_KEYPATH = "avatars.min_avatar_scale"; + const QString AVATAR_MAX_SCALE_KEYPATH = "avatars.max_avatar_scale"; + const QString AVATAR_MIN_HEIGHT_KEYPATH = "avatars.min_avatar_height"; + const QString AVATAR_MAX_HEIGHT_KEYPATH = "avatars.max_avatar_height"; + + QVariant* avatarMinScale = _configMap.valueForKeyPath(AVATAR_MIN_SCALE_KEYPATH); + if (avatarMinScale) { + float scale = avatarMinScale->toFloat(); + _configMap.valueForKeyPath(AVATAR_MIN_HEIGHT_KEYPATH, scale * DEFAULT_AVATAR_HEIGHT); + } + + QVariant* avatarMaxScale = _configMap.valueForKeyPath(AVATAR_MAX_SCALE_KEYPATH); + if (avatarMaxScale) { + float scale = avatarMaxScale->toFloat(); + _configMap.valueForKeyPath(AVATAR_MAX_HEIGHT_KEYPATH, scale * DEFAULT_AVATAR_HEIGHT); + } + } // write the current description version to our settings *versionVariant = _descriptionVersion; diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json index 03fc1cbefb..b818d371e3 100644 --- a/interface/resources/controllers/oculus_touch.json +++ b/interface/resources/controllers/oculus_touch.json @@ -13,11 +13,11 @@ { "from": "OculusTouch.LY", "to": "Standard.LY", "filters": [ - { "type": "deadZone", "min": 0.3 }, + { "type": "deadZone", "min": 0.7 }, "invert" ] }, - { "from": "OculusTouch.LX", "filters": { "type": "deadZone", "min": 0.3 }, "to": "Standard.LX" }, + { "from": "OculusTouch.LX", "filters": { "type": "deadZone", "min": 0.7 }, "to": "Standard.LX" }, { "from": "OculusTouch.LT", "to": "Standard.LTClick", "peek": true, "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] @@ -29,11 +29,11 @@ { "from": "OculusTouch.RY", "to": "Standard.RY", "filters": [ - { "type": "deadZone", "min": 0.3 }, + { "type": "deadZone", "min": 0.7 }, "invert" ] }, - { "from": "OculusTouch.RX", "filters": { "type": "deadZone", "min": 0.3 }, "to": "Standard.RX" }, + { "from": "OculusTouch.RX", "filters": { "type": "deadZone", "min": 0.7 }, "to": "Standard.RX" }, { "from": "OculusTouch.RT", "to": "Standard.RTClick", "peek": true, "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] diff --git a/interface/resources/html/createGlobalEventBridge.js b/interface/resources/html/createGlobalEventBridge.js index bccbdfaf7c..b85aa33e33 100644 --- a/interface/resources/html/createGlobalEventBridge.js +++ b/interface/resources/html/createGlobalEventBridge.js @@ -65,21 +65,23 @@ var EventBridge; // we need to listen to events that might precede the addition of this elements. // A more robust hack will be to add a setInterval that look for DOM changes every 100-300 ms (low performance?) - window.onload = function(){ + window.addEventListener("load",function(event) { setTimeout(function() { EventBridge.forceHtmlAudioOutputDeviceUpdate(); }, 1200); - }; - document.onclick = function(){ + }, false); + + document.addEventListener("click",function(){ setTimeout(function() { EventBridge.forceHtmlAudioOutputDeviceUpdate(); }, 1200); - }; - document.onchange = function(){ + }, false); + + document.addEventListener("change",function(){ setTimeout(function() { EventBridge.forceHtmlAudioOutputDeviceUpdate(); }, 1200); - }; + }, false); tempEventBridge._callbacks.forEach(function (callback) { EventBridge.scriptEventReceived.connect(callback); diff --git a/interface/resources/qml/AudioScope.qml b/interface/resources/qml/AudioScope.qml new file mode 100644 index 0000000000..aea1473c3d --- /dev/null +++ b/interface/resources/qml/AudioScope.qml @@ -0,0 +1,634 @@ +// +// AudioScope.qml +// +// Created by Luis Cuenca on 11/22/2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "styles-uit" +import "controls-uit" as HifiControlsUit + +Item { + id: root + width: parent.width + height: parent.height + + property var _scopeInputData + property var _scopeOutputLeftData + property var _scopeOutputRightData + + property var _triggerInputData + property var _triggerOutputLeftData + property var _triggerOutputRightData + + property var _triggerValues: QtObject{ + property int x: parent.width/2 + property int y: parent.height/3 + } + + property var _triggered: false + property var _steps + property var _refreshMs: 32 + property var _framesPerSecond: AudioScope.getFramesPerSecond() + property var _isFrameUnits: true + + property var _holdStart: QtObject{ + property int x: 0 + property int y: 0 + } + + property var _holdEnd: QtObject{ + property int x: 0 + property int y: 0 + } + + property var _timeBeforeHold: 300 + property var _pressedTime: 0 + property var _isPressed: false + + property var _recOpacity : 0.0 + property var _recSign : 0.05 + + property var _outputLeftState: false + property var _outputRightState: false + + property var _wavFilePath: "" + + function isHolding() { + return (_pressedTime > _timeBeforeHold); + } + + function updateMeasureUnits() { + timeButton.text = _isFrameUnits ? "Display Frames" : "Milliseconds"; + fiveLabel.text = _isFrameUnits ? "5" : "" + (Math.round(1000 * 5.0/_framesPerSecond)); + twentyLabel.text = _isFrameUnits ? "20" : "" + (Math.round(1000 * 20.0/_framesPerSecond)); + fiftyLabel.text = _isFrameUnits ? "50" : "" + (Math.round(1000 * 50.0/_framesPerSecond)); + } + + function collectScopeData() { + if (inputCh.checked) { + _scopeInputData = AudioScope.scopeInput; + } + if (outputLeftCh.checked) { + _scopeOutputLeftData = AudioScope.scopeOutputLeft; + } + if (outputRightCh.checked) { + _scopeOutputRightData = AudioScope.scopeOutputRight; + } + } + + function collectTriggerData() { + if (inputCh.checked) { + _triggerInputData = AudioScope.triggerInput; + } + if (outputLeftCh.checked) { + _triggerOutputLeftData = AudioScope.triggerOutputLeft; + } + if (outputRightCh.checked) { + _triggerOutputRightData = AudioScope.triggerOutputRight; + } + } + + function setRecordingLabelOpacity(opacity) { + _recOpacity = opacity; + recCircle.opacity = _recOpacity; + recText.opacity = _recOpacity; + } + + function updateRecordingLabel() { + _recOpacity += _recSign; + if (_recOpacity > 1.0 || _recOpacity < 0.0) { + _recOpacity = _recOpacity > 1.0 ? 1.0 : 0.0; + _recSign *= -1; + } + setRecordingLabelOpacity(_recOpacity); + } + + function pullFreshValues() { + if (Audio.getRecording()) { + updateRecordingLabel(); + } + + if (!AudioScope.getPause()) { + if (!_triggered) { + collectScopeData(); + } + } + if (inputCh.checked || outputLeftCh.checked || outputRightCh.checked) { + mycanvas.requestPaint(); + } + } + + function startRecording() { + _wavFilePath = (new Date()).toISOString(); // yyyy-mm-ddThh:mm:ss.sssZ + _wavFilePath = _wavFilePath.replace(/[\-:]|\.\d*Z$/g, "").replace("T", "-") + ".wav"; + // Using controller recording default directory + _wavFilePath = Recording.getDefaultRecordingSaveDirectory() + _wavFilePath; + if (!Audio.startRecording(_wavFilePath)) { + Messages.sendMessage("Hifi-Notifications", JSON.stringify({message:"Error creating: "+_wavFilePath})); + updateRecordingUI(false); + } + } + + function stopRecording() { + Audio.stopRecording(); + setRecordingLabelOpacity(0.0); + Messages.sendMessage("Hifi-Notifications", JSON.stringify({message:"Saved: "+_wavFilePath})); + } + + function updateRecordingUI(isRecording) { + if (!isRecording) { + recordButton.text = "Record"; + recordButton.color = hifi.buttons.black; + outputLeftCh.checked = _outputLeftState; + outputRightCh.checked = _outputRightState; + } else { + recordButton.text = "Stop"; + recordButton.color = hifi.buttons.red; + _outputLeftState = outputLeftCh.checked; + _outputRightState = outputRightCh.checked; + outputLeftCh.checked = true; + outputRightCh.checked = true; + } + } + + function toggleRecording() { + if (Audio.getRecording()) { + updateRecordingUI(false); + stopRecording(); + } else { + updateRecordingUI(true); + startRecording(); + } + } + + Timer { + interval: _refreshMs; running: true; repeat: true + onTriggered: pullFreshValues() + } + + Canvas { + id: mycanvas + anchors.fill:parent + + onPaint: { + + function displayMeasureArea(ctx) { + + ctx.fillStyle = Qt.rgba(0.1, 0.1, 0.1, 1); + ctx.fillRect(_holdStart.x, 0, _holdEnd.x - _holdStart.x, height); + + ctx.lineWidth = "2"; + ctx.strokeStyle = "#555555"; + + ctx.beginPath(); + ctx.moveTo(_holdStart.x, 0); + ctx.lineTo(_holdStart.x, height); + ctx.moveTo(_holdEnd.x, 0); + ctx.lineTo(_holdEnd.x, height); + + ctx.moveTo(_holdStart.x, _holdStart.y); + ctx.lineTo(_holdEnd.x, _holdStart.y); + ctx.moveTo(_holdEnd.x, _holdEnd.y); + ctx.lineTo(_holdStart.x, _holdEnd.y); + + ctx.stroke(); + } + + function displayTrigger(ctx, lineWidth, color) { + var crossSize = 3; + var holeSize = 2; + + ctx.lineWidth = lineWidth; + ctx.strokeStyle = color; + + ctx.beginPath(); + ctx.moveTo(_triggerValues.x - (crossSize + holeSize), _triggerValues.y); + ctx.lineTo(_triggerValues.x - holeSize, _triggerValues.y); + ctx.moveTo(_triggerValues.x + holeSize, _triggerValues.y); + ctx.lineTo(_triggerValues.x + (crossSize + holeSize), _triggerValues.y); + + ctx.moveTo(_triggerValues.x, _triggerValues.y - (crossSize + holeSize)); + ctx.lineTo(_triggerValues.x, _triggerValues.y - holeSize); + ctx.moveTo(_triggerValues.x, _triggerValues.y + holeSize); + ctx.lineTo(_triggerValues.x, _triggerValues.y + (crossSize + holeSize)); + + ctx.stroke(); + } + + function displayBackground(ctx, datawidth, steps, lineWidth, color) { + var verticalPadding = 100; + + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + + ctx.moveTo(0, height/2); + ctx.lineTo(datawidth, height/2); + + var gap = datawidth/steps; + for (var i = 0; i < steps; i++) { + ctx.moveTo(i*gap + 1, verticalPadding); + ctx.lineTo(i*gap + 1, height-verticalPadding); + } + ctx.moveTo(datawidth-1, verticalPadding); + ctx.lineTo(datawidth-1, height-verticalPadding); + + ctx.stroke(); + } + + function drawScope(ctx, data, width, color) { + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.lineWidth = width; + var x = 0; + for (var i = 0; i < data.length-1; i++) { + ctx.moveTo(x, data[i] + height/2); + ctx.lineTo(++x, data[i+1] + height/2); + } + ctx.stroke(); + } + + function getMeasurementText(dist) { + var datasize = _scopeInputData.length; + var value = 0; + if (fiveFrames.checked) { + value = (_isFrameUnits) ? 5.0*dist/datasize : (Math.round(1000 * 5.0/_framesPerSecond))*dist/datasize; + } else if (twentyFrames.checked) { + value = (_isFrameUnits) ? 20.0*dist/datasize : (Math.round(1000 * 20.0/_framesPerSecond))*dist/datasize; + } else if (fiftyFrames.checked) { + value = (_isFrameUnits) ? 50.0*dist/datasize : (Math.round(1000 * 50.0/_framesPerSecond))*dist/datasize; + } + value = Math.abs(Math.round(value*100)/100); + var measureText = "" + value + (_isFrameUnits ? " frames" : " milliseconds"); + return measureText; + } + + function drawMeasurements(ctx, color) { + ctx.fillStyle = color; + ctx.font = "normal 16px sans-serif"; + var fontwidth = 8; + var measureText = getMeasurementText(_holdEnd.x - _holdStart.x); + if (_holdStart.x < _holdEnd.x) { + ctx.fillText("" + height/2 - _holdStart.y, _holdStart.x-40, _holdStart.y); + ctx.fillText("" + height/2 - _holdEnd.y, _holdStart.x-40, _holdEnd.y); + ctx.fillText(measureText, _holdEnd.x+10, _holdEnd.y); + } else { + ctx.fillText("" + height/2 - _holdStart.y, _holdStart.x+10, _holdStart.y); + ctx.fillText("" + height/2 - _holdEnd.y, _holdStart.x+10, _holdEnd.y); + ctx.fillText(measureText, _holdEnd.x-fontwidth*measureText.length, _holdEnd.y); + } + } + + var ctx = getContext("2d"); + + ctx.fillStyle = Qt.rgba(0, 0, 0, 1); + ctx.fillRect(0, 0, width, height); + + if (isHolding()) { + displayMeasureArea(ctx); + } + + var guideLinesColor = "#555555" + var guideLinesWidth = "1" + + displayBackground(ctx, _scopeInputData.length, _steps, guideLinesWidth, guideLinesColor); + + var triggerWidth = "3" + var triggerColor = "#EFB400" + + if (AudioScope.getAutoTrigger()) { + displayTrigger(ctx, triggerWidth, triggerColor); + } + + var scopeWidth = "2" + var scopeInputColor = "#00B4EF" + var scopeOutputLeftColor = "#BB0000" + var scopeOutputRightColor = "#00BB00" + + if (!_triggered) { + if (inputCh.checked) { + drawScope(ctx, _scopeInputData, scopeWidth, scopeInputColor); + } + if (outputLeftCh.checked) { + drawScope(ctx, _scopeOutputLeftData, scopeWidth, scopeOutputLeftColor); + } + if (outputRightCh.checked) { + drawScope(ctx, _scopeOutputRightData, scopeWidth, scopeOutputRightColor); + } + } else { + if (inputCh.checked) { + drawScope(ctx, _triggerInputData, scopeWidth, scopeInputColor); + } + if (outputLeftCh.checked) { + drawScope(ctx, _triggerOutputLeftData, scopeWidth, scopeOutputLeftColor); + } + if (outputRightCh.checked) { + drawScope(ctx, _triggerOutputRightData, scopeWidth, scopeOutputRightColor); + } + } + + if (isHolding()) { + drawMeasurements(ctx, "#eeeeee"); + } + + if (_isPressed) { + _pressedTime += _refreshMs; + } + } + } + + MouseArea { + id: hitbox + anchors.fill: mycanvas + hoverEnabled: true + onPressed: { + _isPressed = true; + _pressedTime = 0; + _holdStart.x = mouseX; + _holdStart.y = mouseY; + } + onPositionChanged: { + _holdEnd.x = mouseX; + _holdEnd.y = mouseY; + } + onReleased: { + if (!isHolding() && AudioScope.getAutoTrigger()) { + _triggerValues.x = mouseX + _triggerValues.y = mouseY + AudioScope.setTriggerValues(mouseX, mouseY-height/2); + } + _isPressed = false; + _pressedTime = 0; + } + } + + HifiControlsUit.CheckBox { + id: activated + boxSize: 20 + anchors.top: parent.top; + anchors.left: parent.left; + anchors.topMargin: 8; + anchors.leftMargin: 20; + checked: AudioScope.getVisible(); + onCheckedChanged: { + AudioScope.setVisible(checked); + activelabel.text = AudioScope.getVisible() ? "On" : "Off" + } + } + + HifiControlsUit.Label { + id: activelabel + text: AudioScope.getVisible() ? "On" : "Off" + anchors.top: activated.top; + anchors.left: activated.right; + } + + HifiControlsUit.CheckBox { + id: outputLeftCh + boxSize: 20 + text: "Output L" + anchors.horizontalCenter: parent.horizontalCenter; + anchors.top: parent.top; + anchors.topMargin: 8; + onCheckedChanged: { + AudioScope.setServerEcho(outputLeftCh.checked || outputRightCh.checked); + } + } + + HifiControlsUit.Label { + text: "Channels"; + anchors.horizontalCenter: outputLeftCh.horizontalCenter; + anchors.bottom: outputLeftCh.top; + anchors.bottomMargin: 8; + } + + HifiControlsUit.CheckBox { + id: inputCh + boxSize: 20 + text: "Input Mono" + anchors.bottom: outputLeftCh.bottom; + anchors.right: outputLeftCh.left; + anchors.rightMargin: 40; + onCheckedChanged: { + AudioScope.setLocalEcho(checked); + } + } + + HifiControlsUit.CheckBox { + id: outputRightCh + boxSize: 20 + text: "Output R" + anchors.bottom: outputLeftCh.bottom; + anchors.left: outputLeftCh.right; + anchors.leftMargin: 40; + onCheckedChanged: { + AudioScope.setServerEcho(outputLeftCh.checked || outputRightCh.checked); + } + } + + HifiControlsUit.Button { + id: recordButton; + text: "Record"; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + anchors.rightMargin: 30; + anchors.bottomMargin: 8; + width: 95; + height: 55; + onClicked: { + toggleRecording(); + } + } + + HifiControlsUit.Button { + id: pauseButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.right: recordButton.left; + anchors.bottom: parent.bottom; + anchors.rightMargin: 30; + anchors.bottomMargin: 8; + height: 55; + width: 95; + text: " Pause "; + onClicked: { + AudioScope.togglePause(); + } + } + + HifiControlsUit.CheckBox { + id: twentyFrames + boxSize: 20 + anchors.left: parent.horizontalCenter; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; + onCheckedChanged: { + if (checked){ + fiftyFrames.checked = false; + fiveFrames.checked = false; + AudioScope.selectAudioScopeTwentyFrames(); + _steps = 20; + AudioScope.setPause(false); + } + } + } + + HifiControlsUit.Label { + id:twentyLabel + anchors.left: twentyFrames.right; + anchors.verticalCenter: twentyFrames.verticalCenter; + } + + HifiControlsUit.Button { + id: timeButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + text: "Display Frames"; + anchors.horizontalCenter: twentyFrames.horizontalCenter; + anchors.bottom: twentyFrames.top; + anchors.bottomMargin: 8; + height: 26; + onClicked: { + _isFrameUnits = !_isFrameUnits; + updateMeasureUnits(); + } + } + + HifiControlsUit.CheckBox { + id: fiveFrames + boxSize: 20 + anchors.horizontalCenter: parent.horizontalCenter; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; + anchors.horizontalCenterOffset: -50; + checked: true; + onCheckedChanged: { + if (checked) { + fiftyFrames.checked = false; + twentyFrames.checked = false; + AudioScope.selectAudioScopeFiveFrames(); + _steps = 5; + AudioScope.setPause(false); + } + } + } + + HifiControlsUit.Label { + id:fiveLabel + anchors.left: fiveFrames.right; + anchors.verticalCenter: fiveFrames.verticalCenter; + } + + HifiControlsUit.CheckBox { + id: fiftyFrames + boxSize: 20 + anchors.horizontalCenter: parent.horizontalCenter; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; + anchors.horizontalCenterOffset: 70; + onCheckedChanged: { + if (checked) { + twentyFrames.checked = false; + fiveFrames.checked = false; + AudioScope.selectAudioScopeFiftyFrames(); + _steps = 50; + AudioScope.setPause(false); + } + } + } + + HifiControlsUit.Label { + id:fiftyLabel + anchors.left: fiftyFrames.right; + anchors.verticalCenter: fiftyFrames.verticalCenter; + } + + HifiControlsUit.Switch { + id: triggerSwitch; + height: 26; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.leftMargin: 75; + anchors.bottomMargin: 8; + labelTextOff: "Off"; + labelTextOn: "On"; + onCheckedChanged: { + if (!checked) AudioScope.setPause(false); + AudioScope.setPause(false); + AudioScope.setAutoTrigger(checked); + AudioScope.setTriggerValues(_triggerValues.x, _triggerValues.y-root.height/2); + } + } + + HifiControlsUit.Label { + text: "Trigger"; + anchors.left: triggerSwitch.left; + anchors.leftMargin: -15; + anchors.bottom: triggerSwitch.top; + } + + Rectangle { + id: recordIcon; + width:110; + height:40; + anchors.right: parent.right; + anchors.top: parent.top; + anchors.topMargin: 8; + color: "transparent" + + Text { + id: recText + text: "REC" + color: "red" + font.pixelSize: 30; + anchors.left: recCircle.right; + anchors.leftMargin: 10; + opacity: _recOpacity; + y: -8; + } + + Rectangle { + id: recCircle; + width: 25; + height: 25; + radius: width*0.5 + opacity: _recOpacity; + color: "red"; + } + } + + Component.onCompleted: { + _steps = AudioScope.getFramesPerScope(); + AudioScope.setTriggerValues(_triggerValues.x, _triggerValues.y-root.height/2); + activated.checked = true; + inputCh.checked = true; + updateMeasureUnits(); + } + + Connections { + target: AudioScope + onPauseChanged: { + if (!AudioScope.getPause()) { + pauseButton.text = "Pause"; + pauseButton.color = hifi.buttons.black; + AudioScope.setTriggered(false); + _triggered = false; + } else { + pauseButton.text = "Continue"; + pauseButton.color = hifi.buttons.blue; + } + } + onTriggered: { + _triggered = true; + collectTriggerData(); + AudioScope.setPause(true); + } + } +} diff --git a/interface/resources/qml/controls/FlickableWebViewCore.qml b/interface/resources/qml/controls/FlickableWebViewCore.qml index 1e0a936bf0..6ab4fd9758 100644 --- a/interface/resources/qml/controls/FlickableWebViewCore.qml +++ b/interface/resources/qml/controls/FlickableWebViewCore.qml @@ -21,6 +21,8 @@ Item { signal newViewRequestedCallback(var request) signal loadingChangedCallback(var loadRequest) + width: parent.width + property bool interactive: false StylesUIt.HifiConstants { @@ -58,7 +60,8 @@ Item { WebEngineView { id: webViewCore - anchors.fill: parent + width: parent.width + height: parent.height profile: HFWebEngineProfile; settings.pluginsEnabled: true @@ -91,20 +94,19 @@ Item { userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] - property string newUrl: "" - Component.onCompleted: { webChannel.registerObject("eventBridge", eventBridge); webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); - // Ensure the JS from the web-engine makes it to our logging - webViewCore.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { - console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); - }); + if (webViewCoreUserAgent !== undefined) { webViewCore.profile.httpUserAgent = webViewCoreUserAgent } else { webViewCore.profile.httpUserAgent += " (HighFidelityInterface)"; } + // Ensure the JS from the web-engine makes it to our logging + webViewCore.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); + }); } onFeaturePermissionRequested: { diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index efcf6ccfcf..64f61f0d69 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -26,6 +26,7 @@ Rectangle { // Style color: "#E3E3E3"; // Properties + property bool debug: false; property int myCardWidth: width - upperRightInfoContainer.width; property int myCardHeight: 80; property int rowHeight: 60; @@ -1120,7 +1121,9 @@ Rectangle { break; case 'connections': var data = message.params; - console.log('Got connection data: ', JSON.stringify(data)); + if (pal.debug) { + console.log('Got connection data: ', JSON.stringify(data)); + } connectionsUserModelData = data; sortConnectionsModel(); connectionsLoading.visible = false; diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 6c4e020694..c8a63a4d2d 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -79,10 +79,12 @@ Rectangle { if (result.status !== 'success') { failureErrorText.text = result.message; root.activeView = "checkoutFailure"; + UserActivityLogger.commercePurchaseFailure(root.itemId, root.itemPrice, !root.alreadyOwned, result.message); } else { root.itemHref = result.data.download_url; root.isWearable = result.data.categories.indexOf("Wearables") > -1; root.activeView = "checkoutSuccess"; + UserActivityLogger.commercePurchaseSuccess(root.itemId, root.itemPrice, !root.alreadyOwned); } } @@ -599,6 +601,7 @@ Rectangle { sendToScript({method: 'checkout_rezClicked', itemHref: root.itemHref, isWearable: root.isWearable}); rezzedNotifContainer.visible = true; rezzedNotifContainerTimer.start(); + UserActivityLogger.commerceEntityRezzed(root.itemId, "checkout", root.isWearable ? "rez" : "wear"); } } RalewaySemiBold { diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index 15ebada0c4..f7913e5b1e 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -349,6 +349,7 @@ Item { sendToPurchases({method: 'purchases_rezClicked', itemHref: root.itemHref, isWearable: root.isWearable}); rezzedNotifContainer.visible = true; rezzedNotifContainerTimer.start(); + UserActivityLogger.commerceEntityRezzed(root.itemId, "purchases", root.isWearable ? "rez" : "wear"); } style: ButtonStyle { diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml index d967a36b68..582052c4c3 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml @@ -206,16 +206,6 @@ Item { root.isPasswordField = (focus && passphraseField.echoMode === TextInput.Password); } - MouseArea { - anchors.fill: parent; - - onClicked: { - root.keyboardRaised = true; - root.isPasswordField = (passphraseField.echoMode === TextInput.Password); - mouse.accepted = false; - } - } - onAccepted: { submitPassphraseInputButton.enabled = false; commerce.setPassphrase(passphraseField.text); @@ -362,25 +352,6 @@ Item { right: parent.right; } - Image { - id: lowerKeyboardButton; - z: 999; - source: "images/lowerKeyboard.png"; - anchors.right: keyboard.right; - anchors.top: keyboard.showMirrorText ? keyboard.top : undefined; - anchors.bottom: keyboard.showMirrorText ? undefined : keyboard.bottom; - height: 50; - width: 60; - - MouseArea { - anchors.fill: parent; - - onClicked: { - root.keyboardRaised = false; - } - } - } - HifiControlsUit.Keyboard { id: keyboard; raised: HMD.mounted && root.keyboardRaised; diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml index ffeedde8f0..9ba066a9fd 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -82,17 +82,6 @@ Item { if (focus) { var hidePassword = (currentPassphraseField.echoMode === TextInput.Password); sendSignalToWallet({method: 'walletSetup_raiseKeyboard', isPasswordField: hidePassword}); - } else if (!passphraseFieldAgain.focus) { - sendSignalToWallet({method: 'walletSetup_lowerKeyboard', isPasswordField: false}); - } - } - - MouseArea { - anchors.fill: parent; - onPressed: { - var hidePassword = (currentPassphraseField.echoMode === TextInput.Password); - sendSignalToWallet({method: 'walletSetup_raiseKeyboard', isPasswordField: hidePassword}); - mouse.accepted = false; } } @@ -115,21 +104,10 @@ Item { activeFocusOnPress: true; activeFocusOnTab: true; - MouseArea { - anchors.fill: parent; - onPressed: { - var hidePassword = (passphraseField.echoMode === TextInput.Password); - sendSignalToWallet({method: 'walletSetup_raiseKeyboard', isPasswordField: hidePassword}); - mouse.accepted = false; - } - } - onFocusChanged: { if (focus) { var hidePassword = (passphraseField.echoMode === TextInput.Password); sendMessageToLightbox({method: 'walletSetup_raiseKeyboard', isPasswordField: hidePassword}); - } else if (!passphraseFieldAgain.focus) { - sendMessageToLightbox({method: 'walletSetup_lowerKeyboard', isPasswordField: false}); } } @@ -151,21 +129,10 @@ Item { activeFocusOnPress: true; activeFocusOnTab: true; - MouseArea { - anchors.fill: parent; - onPressed: { - var hidePassword = (passphraseFieldAgain.echoMode === TextInput.Password); - sendSignalToWallet({method: 'walletSetup_raiseKeyboard', isPasswordField: hidePassword}); - mouse.accepted = false; - } - } - onFocusChanged: { if (focus) { var hidePassword = (passphraseFieldAgain.echoMode === TextInput.Password); sendMessageToLightbox({method: 'walletSetup_raiseKeyboard', isPasswordField: hidePassword}); - } else if (!passphraseField.focus) { - sendMessageToLightbox({method: 'walletSetup_lowerKeyboard', isPasswordField: false}); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 0424dd1edd..ac05bf7c84 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -48,6 +48,11 @@ Rectangle { if (root.activeView !== "walletSetup") { root.activeView = "walletSetup"; commerce.resetLocalWalletOnly(); + var timestamp = new Date(); + walletSetup.startingTimestamp = timestamp; + walletSetup.setupAttemptID = generateUUID(); + UserActivityLogger.commerceWalletSetupStarted(timestamp, setupAttemptID, walletSetup.setupFlowVersion, walletSetup.referrer ? walletSetup.referrer : "wallet app", + (AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : '')); } } else if (walletStatus === 2) { if (root.activeView !== "passphraseModal") { @@ -173,7 +178,7 @@ Rectangle { Connections { onSendSignalToWallet: { if (msg.method === 'walletSetup_finished') { - if (msg.referrer === '') { + if (msg.referrer === '' || msg.referrer === 'marketplace cta') { root.activeView = "initialize"; commerce.getWalletStatus(); } else if (msg.referrer === 'purchases') { @@ -667,25 +672,6 @@ Rectangle { right: parent.right; } - Image { - id: lowerKeyboardButton; - z: 999; - source: "images/lowerKeyboard.png"; - anchors.right: keyboard.right; - anchors.top: keyboard.showMirrorText ? keyboard.top : undefined; - anchors.bottom: keyboard.showMirrorText ? undefined : keyboard.bottom; - height: 50; - width: 60; - - MouseArea { - anchors.fill: parent; - - onClicked: { - root.keyboardRaised = false; - } - } - } - HifiControlsUit.Keyboard { id: keyboard; raised: HMD.mounted && root.keyboardRaised; @@ -720,12 +706,28 @@ Rectangle { case 'updateWalletReferrer': walletSetup.referrer = message.referrer; break; + case 'inspectionCertificate_resetCert': + // NOP + break; default: console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); } } signal sendToScript(var message); + // generateUUID() taken from: + // https://stackoverflow.com/a/8809472 + function generateUUID() { // Public Domain/MIT + var d = new Date().getTime(); + if (typeof performance !== 'undefined' && typeof performance.now === 'function'){ + d += performance.now(); //use high-precision timer if available + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); + } // // FUNCTION DEFINITIONS END // diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index ec7468651d..d23079d3f3 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -68,6 +68,7 @@ Item { Connections { target: GlobalServices onMyUsernameChanged: { + transactionHistoryModel.clear(); usernameText.text = Account.username; } } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml index 8de831ef75..d7859d2800 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml @@ -31,6 +31,10 @@ Item { property bool hasShownSecurityImageTip: false; property string referrer; property string keyFilePath; + property date startingTimestamp; + property string setupAttemptID; + readonly property int setupFlowVersion: 1; + readonly property var setupStepNames: [ "Setup Prompt", "Security Image Selection", "Passphrase Selection", "Private Keys Ready" ]; Image { anchors.fill: parent; @@ -67,6 +71,13 @@ Item { anchors.fill: parent; } + onActiveViewChanged: { + var timestamp = new Date(); + var currentStepNumber = root.activeView.substring(5); + UserActivityLogger.commerceWalletSetupProgress(timestamp, root.setupAttemptID, + Math.round((timestamp - root.startingTimestamp)/1000), currentStepNumber, root.setupStepNames[currentStepNumber - 1]); + } + // // TITLE BAR START // @@ -730,6 +741,9 @@ Item { root.visible = false; root.hasShownSecurityImageTip = false; sendSignalToWallet({method: 'walletSetup_finished', referrer: root.referrer ? root.referrer : ""}); + + var timestamp = new Date(); + UserActivityLogger.commerceWalletSetupFinished(timestamp, setupAttemptID, Math.round((timestamp - root.startingTimestamp)/1000)); } } } diff --git a/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml b/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml index 5c9d6822c8..0e0786975e 100644 --- a/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml +++ b/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml @@ -26,6 +26,7 @@ Item { } Connections { + id: onAttachmentsChangedConnection target: MyAvatar onAttachmentsChanged: reload() } @@ -34,6 +35,12 @@ Item { reload() } + function setAttachmentsVariant(attachments) { + onAttachmentsChangedConnection.enabled = false; + MyAvatar.setAttachmentsVariant(attachments); + onAttachmentsChangedConnection.enabled = true; + } + Column { width: pane.width @@ -92,11 +99,15 @@ Item { attachments.splice(index, 1); listView.model.remove(index, 1); } - onUpdateAttachment: MyAvatar.setAttachmentsVariant(attachments); + onUpdateAttachment: { + setAttachmentsVariant(attachments); + } } } - onCountChanged: MyAvatar.setAttachmentsVariant(attachments); + onCountChanged: { + setAttachmentsVariant(attachments); + } /* // DEBUG @@ -220,7 +231,7 @@ Item { }; attachments.push(template); listView.model.append({}); - MyAvatar.setAttachmentsVariant(attachments); + setAttachmentsVariant(attachments); } } @@ -250,7 +261,7 @@ Item { id: cancelAction text: "Cancel" onTriggered: { - MyAvatar.setAttachmentsVariant(originalAttachments); + setAttachmentsVariant(originalAttachments); closeDialog(); } } @@ -263,7 +274,7 @@ Item { console.log("Attachment " + i + ": " + attachments[i]); } - MyAvatar.setAttachmentsVariant(attachments); + setAttachmentsVariant(attachments); closeDialog(); } } diff --git a/interface/resources/qml/hifi/overlays/ImageOverlay.qml b/interface/resources/qml/hifi/overlays/ImageOverlay.qml index 8c638679f9..6899c38e67 100644 --- a/interface/resources/qml/hifi/overlays/ImageOverlay.qml +++ b/interface/resources/qml/hifi/overlays/ImageOverlay.qml @@ -9,33 +9,30 @@ Overlay { Image { id: image - property bool scaleFix: true; - property real xOffset: 0 - property real yOffset: 0 + property bool scaleFix: true + property real xStart: 0 + property real yStart: 0 + property real xSize: 0 + property real ySize: 0 property real imageScale: 1.0 property var resizer: Timer { interval: 50 repeat: false running: false onTriggered: { - var targetAspect = root.width / root.height; - var sourceAspect = image.sourceSize.width / image.sourceSize.height; - if (sourceAspect <= targetAspect) { - if (root.width === image.sourceSize.width) { - return; - } - image.imageScale = root.width / image.sourceSize.width; - } else if (sourceAspect > targetAspect){ - if (root.height === image.sourceSize.height) { - return; - } - image.imageScale = root.height / image.sourceSize.height; + if (image.xSize === 0) { + image.xSize = image.sourceSize.width - image.xStart; } - image.sourceSize = Qt.size(image.sourceSize.width * image.imageScale, image.sourceSize.height * image.imageScale); + if (image.ySize === 0) { + image.ySize = image.sourceSize.height - image.yStart; + } + + image.anchors.leftMargin = -image.xStart * root.width / image.xSize; + image.anchors.topMargin = -image.yStart * root.height / image.ySize; + image.anchors.rightMargin = (image.xStart + image.xSize - image.sourceSize.width) * root.width / image.xSize; + image.anchors.bottomMargin = (image.yStart + image.ySize - image.sourceSize.height) * root.height / image.ySize; } } - x: -1 * xOffset * imageScale - y: -1 * yOffset * imageScale onSourceSizeChanged: { if (sourceSize.width !== 0 && sourceSize.height !== 0 && progress === 1.0 && scaleFix) { @@ -43,6 +40,8 @@ Overlay { resizer.start(); } } + + anchors.fill: parent } ColorOverlay { @@ -57,8 +56,10 @@ Overlay { var key = keys[i]; var value = subImage[key]; switch (key) { - case "x": image.xOffset = value; break; - case "y": image.yOffset = value; break; + case "x": image.xStart = value; break; + case "y": image.yStart = value; break; + case "width": image.xSize = value; break; + case "height": image.ySize = value; break; } } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d7fcbf6467..0176acf108 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2812,10 +2812,10 @@ static int getEventQueueSize(QThread* thread) { static void dumpEventQueue(QThread* thread) { auto threadData = QThreadData::get2(thread); QMutexLocker locker(&threadData->postEventList.mutex); - qDebug() << "AJT: event list, size =" << threadData->postEventList.size(); + qDebug() << "Event list, size =" << threadData->postEventList.size(); for (auto& postEvent : threadData->postEventList) { QEvent::Type type = (postEvent.event ? postEvent.event->type() : QEvent::None); - qDebug() << "AJT: " << type; + qDebug() << " " << type; } } #endif // DEBUG_EVENT_QUEUE @@ -5974,7 +5974,7 @@ bool Application::acceptURL(const QString& urlString, bool defaultUpload) { } } - if (defaultUpload) { + if (defaultUpload && !url.fileName().isEmpty() && url.isLocalFile()) { showAssetServerWidget(urlString); } return defaultUpload; @@ -7075,11 +7075,11 @@ QRect Application::getRecommendedHUDRect() const { return result; } -QSize Application::getDeviceSize() const { +glm::vec2 Application::getDeviceSize() const { static const int MIN_SIZE = 1; - QSize result(MIN_SIZE, MIN_SIZE); + glm::vec2 result(MIN_SIZE); if (_displayPlugin) { - result = fromGlm(getActiveDisplayPlugin()->getRecommendedRenderSize()); + result = getActiveDisplayPlugin()->getRecommendedRenderSize(); } return result; } @@ -7098,10 +7098,6 @@ bool Application::hasFocus() const { return (QApplication::activeWindow() != nullptr); } -glm::vec2 Application::getViewportDimensions() const { - return toGlm(getDeviceSize()); -} - void Application::setMaxOctreePacketsPerSecond(int maxOctreePPS) { if (maxOctreePPS != _maxOctreePPS) { _maxOctreePPS = maxOctreePPS; diff --git a/interface/src/Application.h b/interface/src/Application.h index 5d9028f835..9542c5ccb6 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -158,7 +158,7 @@ public: glm::uvec2 getUiSize() const; QRect getRecommendedHUDRect() const; - QSize getDeviceSize() const; + glm::vec2 getDeviceSize() const; bool hasFocus() const; void showCursor(const Cursor::Icon& cursor); @@ -228,8 +228,6 @@ public: FileLogger* getLogger() const { return _logger; } - glm::vec2 getViewportDimensions() const; - NodeToJurisdictionMap& getEntityServerJurisdictions() { return _entityServerJurisdictions; } float getRenderResolutionScale() const; diff --git a/interface/src/Application_render.cpp b/interface/src/Application_render.cpp index 44d9dfee03..1231e5834b 100644 --- a/interface/src/Application_render.cpp +++ b/interface/src/Application_render.cpp @@ -104,8 +104,7 @@ void Application::paintGL() { PerformanceTimer perfTimer("renderOverlay"); // NOTE: There is no batch associated with this renderArgs // the ApplicationOverlay class assumes it's viewport is setup to be the device size - QSize size = getDeviceSize(); - renderArgs._viewport = glm::ivec4(0, 0, size.width(), size.height()); + renderArgs._viewport = glm::ivec4(0, 0, getDeviceSize()); _applicationOverlay.renderOverlay(&renderArgs); } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 9ec5cc6034..9bbb72357b 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -679,36 +679,16 @@ Menu::Menu() { }); auto audioIO = DependencyManager::get(); - addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoServerAudio, 0, false, - audioIO.data(), SLOT(toggleServerEcho())); - addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoLocalAudio, 0, false, - audioIO.data(), SLOT(toggleLocalEcho())); addActionToQMenuAndActionHash(audioDebugMenu, MenuOption::MuteEnvironment, 0, audioIO.data(), SLOT(sendMuteEnvironmentPacket())); - auto scope = DependencyManager::get(); - MenuWrapper* audioScopeMenu = audioDebugMenu->addMenu("Audio Scope"); - addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScope, Qt::CTRL | Qt::Key_F2, false, - scope.data(), SLOT(toggle())); - addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScopePause, Qt::CTRL | Qt::SHIFT | Qt::Key_F2, false, - scope.data(), SLOT(togglePause())); - - addDisabledActionAndSeparator(audioScopeMenu, "Display Frames"); - { - QAction* fiveFrames = addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScopeFiveFrames, - 0, true, scope.data(), SLOT(selectAudioScopeFiveFrames())); - - QAction* twentyFrames = addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScopeTwentyFrames, - 0, false, scope.data(), SLOT(selectAudioScopeTwentyFrames())); - - QAction* fiftyFrames = addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScopeFiftyFrames, - 0, false, scope.data(), SLOT(selectAudioScopeFiftyFrames())); - - QActionGroup* audioScopeFramesGroup = new QActionGroup(audioScopeMenu); - audioScopeFramesGroup->addAction(fiveFrames); - audioScopeFramesGroup->addAction(twentyFrames); - audioScopeFramesGroup->addAction(fiftyFrames); - } + action = addActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioScope); + connect(action, &QAction::triggered, [] { + auto scriptEngines = DependencyManager::get(); + QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); + defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/utilities/audio/audioScope.js"); + scriptEngines->loadScript(defaultScriptsLoc.toString()); + }); // Developer > Physics >>> MenuWrapper* physicsOptionsMenu = developerMenu->addMenu("Physics"); diff --git a/interface/src/audio/AudioScope.cpp b/interface/src/audio/AudioScope.cpp index cf9984e32b..1a2e867d51 100644 --- a/interface/src/audio/AudioScope.cpp +++ b/interface/src/audio/AudioScope.cpp @@ -9,6 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include @@ -21,13 +22,14 @@ #include "AudioScope.h" static const unsigned int DEFAULT_FRAMES_PER_SCOPE = 5; -static const unsigned int SCOPE_WIDTH = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * DEFAULT_FRAMES_PER_SCOPE; static const unsigned int MULTIPLIER_SCOPE_HEIGHT = 20; static const unsigned int SCOPE_HEIGHT = 2 * 15 * MULTIPLIER_SCOPE_HEIGHT; AudioScope::AudioScope() : _isEnabled(false), _isPaused(false), + _isTriggered(false), + _autoTrigger(false), _scopeInputOffset(0), _scopeOutputOffset(0), _framesPerScope(DEFAULT_FRAMES_PER_SCOPE), @@ -43,6 +45,7 @@ AudioScope::AudioScope() : _outputRightD(DependencyManager::get()->allocateID()) { auto audioIO = DependencyManager::get(); + connect(&audioIO->getReceivedAudioStream(), &MixedProcessedAudioStream::addedSilence, this, &AudioScope::addStereoSilenceToScope); connect(&audioIO->getReceivedAudioStream(), &MixedProcessedAudioStream::addedLastFrameRepeatedWithFade, @@ -75,6 +78,18 @@ void AudioScope::selectAudioScopeFiftyFrames() { reallocateScope(50); } +void AudioScope::setLocalEcho(bool localEcho) { + DependencyManager::get()->setLocalEcho(localEcho); +} + +void AudioScope::setServerEcho(bool serverEcho) { + DependencyManager::get()->setServerEcho(serverEcho); +} + +float AudioScope::getFramesPerSecond(){ + return AudioConstants::NETWORK_FRAMES_PER_SEC; +} + void AudioScope::allocateScope() { _scopeInputOffset = 0; _scopeOutputOffset = 0; @@ -108,63 +123,14 @@ void AudioScope::freeScope() { } } -void AudioScope::render(RenderArgs* renderArgs, int width, int height) { - - if (!_isEnabled) { - return; - } - - static const glm::vec4 backgroundColor = { 0.4f, 0.4f, 0.4f, 0.6f }; - static const glm::vec4 gridColor = { 0.7f, 0.7f, 0.7f, 1.0f }; - static const glm::vec4 inputColor = { 0.3f, 1.0f, 0.3f, 1.0f }; - static const glm::vec4 outputLeftColor = { 1.0f, 0.3f, 0.3f, 1.0f }; - static const glm::vec4 outputRightColor = { 0.3f, 0.3f, 1.0f, 1.0f }; - static const int gridCols = 2; - int gridRows = _framesPerScope; - - int x = (width - (int)SCOPE_WIDTH) / 2; - int y = (height - (int)SCOPE_HEIGHT) / 2; - int w = (int)SCOPE_WIDTH; - int h = (int)SCOPE_HEIGHT; - - gpu::Batch& batch = *renderArgs->_batch; - auto geometryCache = DependencyManager::get(); - - // Grid uses its own pipeline, so draw it before setting another - const float GRID_EDGE = 0.005f; - geometryCache->renderGrid(batch, glm::vec2(x, y), glm::vec2(x + w, y + h), - gridRows, gridCols, GRID_EDGE, gridColor, true, _audioScopeGrid); - - geometryCache->useSimpleDrawPipeline(batch); - auto textureCache = DependencyManager::get(); - batch.setResourceTexture(0, textureCache->getWhiteTexture()); - - // FIXME - do we really need to reset this here? we know that we're called inside of ApplicationOverlay::renderOverlays - // which already set up our batch for us to have these settings - mat4 legacyProjection = glm::ortho(0, width, height, 0, -1000, 1000); - batch.setProjectionTransform(legacyProjection); - batch.setModelTransform(Transform()); - batch.resetViewTransform(); - - geometryCache->renderQuad(batch, x, y, w, h, backgroundColor, _audioScopeBackground); - renderLineStrip(batch, _inputID, inputColor, x, y, _samplesPerScope, _scopeInputOffset, _scopeInput); - renderLineStrip(batch, _outputLeftID, outputLeftColor, x, y, _samplesPerScope, _scopeOutputOffset, _scopeOutputLeft); - renderLineStrip(batch, _outputRightD, outputRightColor, x, y, _samplesPerScope, _scopeOutputOffset, _scopeOutputRight); -} - -void AudioScope::renderLineStrip(gpu::Batch& batch, int id, const glm::vec4& color, int x, int y, int n, int offset, const QByteArray* byteArray) { - +QVector AudioScope::getScopeVector(const QByteArray* byteArray, int offset) { int16_t sample; - int16_t* samples = ((int16_t*) byteArray->data()) + offset; + QVector points; + if (!_isEnabled || byteArray == NULL) return points; + int16_t* samples = ((int16_t*)byteArray->data()) + offset; int numSamplesToAverage = _framesPerScope / DEFAULT_FRAMES_PER_SCOPE; - int count = (n - offset) / numSamplesToAverage; - int remainder = (n - offset) % numSamplesToAverage; - y += SCOPE_HEIGHT / 2; - - auto geometryCache = DependencyManager::get(); - - QVector points; - + int count = (_samplesPerScope - offset) / numSamplesToAverage; + int remainder = (_samplesPerScope - offset) % numSamplesToAverage; // Compute and draw the sample averages from the offset position for (int i = count; --i >= 0; ) { @@ -173,7 +139,7 @@ void AudioScope::renderLineStrip(gpu::Batch& batch, int id, const glm::vec4& col sample += *samples++; } sample /= numSamplesToAverage; - points << glm::vec2(x++, y - sample); + points << -sample; } // Compute and draw the sample average across the wrap boundary @@ -182,16 +148,17 @@ void AudioScope::renderLineStrip(gpu::Batch& batch, int id, const glm::vec4& col for (int j = remainder; --j >= 0; ) { sample += *samples++; } - - samples = (int16_t*) byteArray->data(); + + samples = (int16_t*)byteArray->data(); for (int j = numSamplesToAverage - remainder; --j >= 0; ) { sample += *samples++; } sample /= numSamplesToAverage; - points << glm::vec2(x++, y - sample); - } else { - samples = (int16_t*) byteArray->data(); + points << -sample; + } + else { + samples = (int16_t*)byteArray->data(); } // Compute and draw the sample average from the beginning to the offset @@ -202,12 +169,51 @@ void AudioScope::renderLineStrip(gpu::Batch& batch, int id, const glm::vec4& col sample += *samples++; } sample /= numSamplesToAverage; - points << glm::vec2(x++, y - sample); + + points << -sample; + } + return points; +} + +bool AudioScope::shouldTrigger(const QVector& scope) { + int threshold = 4; + if (_autoTrigger && _triggerValues.x < scope.size()) { + for (int i = -4*threshold; i < +4*threshold; i++) { + int idx = _triggerValues.x + i; + idx = (idx < 0) ? 0 : (idx < scope.size() ? idx : scope.size() - 1); + int dif = abs(_triggerValues.y - scope[idx]); + if (dif < threshold) { + return true; + } + } + } + return false; +} + +void AudioScope::storeTriggerValues() { + _triggerInputData = _scopeInputData; + _triggerOutputLeftData = _scopeOutputLeftData; + _triggerOutputRightData = _scopeOutputRightData; + _isTriggered = true; + emit triggered(); +} + +void AudioScope::computeInputData() { + _scopeInputData = getScopeVector(_scopeInput, _scopeInputOffset); + if (shouldTrigger(_scopeInputData)) { + storeTriggerValues(); + } +} + +void AudioScope::computeOutputData() { + _scopeOutputLeftData = getScopeVector(_scopeOutputLeft, _scopeOutputOffset); + if (shouldTrigger(_scopeOutputLeftData)) { + storeTriggerValues(); + } + _scopeOutputRightData = getScopeVector(_scopeOutputRight, _scopeOutputOffset); + if (shouldTrigger(_scopeOutputRightData)) { + storeTriggerValues(); } - - - geometryCache->updateVertices(id, points, color); - geometryCache->renderVertices(batch, gpu::LINE_STRIP, id); } int AudioScope::addBufferToScope(QByteArray* byteArray, int frameOffset, const int16_t* source, int sourceSamplesPerChannel, @@ -231,7 +237,7 @@ int AudioScope::addBufferToScope(QByteArray* byteArray, int frameOffset, const i } int AudioScope::addSilenceToScope(QByteArray* byteArray, int frameOffset, int silentSamples) { - + // Short int pointer to mapped samples in byte array int16_t* destination = (int16_t*)byteArray->data(); @@ -271,6 +277,7 @@ void AudioScope::addStereoSamplesToScope(const QByteArray& samples) { _scopeOutputOffset = addBufferToScope(_scopeOutputRight, _scopeOutputOffset, samplesData, samplesPerChannel, 1, AudioConstants::STEREO); _scopeLastFrame = samples.right(AudioConstants::NETWORK_FRAME_BYTES_STEREO); + computeOutputData(); } void AudioScope::addLastFrameRepeatedWithFadeToScope(int samplesPerChannel) { @@ -302,4 +309,5 @@ void AudioScope::addInputToScope(const QByteArray& inputSamples) { _scopeInputOffset = addBufferToScope(_scopeInput, _scopeInputOffset, reinterpret_cast(inputSamples.data()), inputSamples.size() / sizeof(int16_t), INPUT_AUDIO_CHANNEL, NUM_INPUT_CHANNELS); + computeInputData(); } diff --git a/interface/src/audio/AudioScope.h b/interface/src/audio/AudioScope.h index e0c8840bb2..e99b8378e3 100644 --- a/interface/src/audio/AudioScope.h +++ b/interface/src/audio/AudioScope.h @@ -24,27 +24,60 @@ class AudioScope : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY + + Q_PROPERTY(QVector scopeInput READ getScopeInput) + Q_PROPERTY(QVector scopeOutputLeft READ getScopeOutputLeft) + Q_PROPERTY(QVector scopeOutputRight READ getScopeOutputRight) + + Q_PROPERTY(QVector triggerInput READ getTriggerInput) + Q_PROPERTY(QVector triggerOutputLeft READ getTriggerOutputLeft) + Q_PROPERTY(QVector triggerOutputRight READ getTriggerOutputRight) + public: // Audio scope methods for allocation/deallocation void allocateScope(); void freeScope(); void reallocateScope(int frames); - void render(RenderArgs* renderArgs, int width, int height); - public slots: void toggle() { setVisible(!_isEnabled); } void setVisible(bool visible); bool getVisible() const { return _isEnabled; } - void togglePause() { _isPaused = !_isPaused; } - void setPause(bool paused) { _isPaused = paused; } + void togglePause() { setPause(!_isPaused); } + void setPause(bool paused) { _isPaused = paused; emit pauseChanged(); } bool getPause() { return _isPaused; } + void toggleTrigger() { _autoTrigger = !_autoTrigger; } + bool getAutoTrigger() { return _autoTrigger; } + void setAutoTrigger(bool autoTrigger) { _isTriggered = false; _autoTrigger = autoTrigger; } + + void setTriggerValues(int x, int y) { _triggerValues.x = x; _triggerValues.y = y; } + void setTriggered(bool triggered) { _isTriggered = triggered; } + bool getTriggered() { return _isTriggered; } + + float getFramesPerSecond(); + int getFramesPerScope() { return _framesPerScope; } + void selectAudioScopeFiveFrames(); void selectAudioScopeTwentyFrames(); void selectAudioScopeFiftyFrames(); - + + QVector getScopeInput() { return _scopeInputData; }; + QVector getScopeOutputLeft() { return _scopeOutputLeftData; }; + QVector getScopeOutputRight() { return _scopeOutputRightData; }; + + QVector getTriggerInput() { return _triggerInputData; }; + QVector getTriggerOutputLeft() { return _triggerOutputLeftData; }; + QVector getTriggerOutputRight() { return _triggerOutputRightData; }; + + void setLocalEcho(bool serverEcho); + void setServerEcho(bool serverEcho); + +signals: + void pauseChanged(); + void triggered(); + protected: AudioScope(); @@ -55,24 +88,44 @@ private slots: void addInputToScope(const QByteArray& inputSamples); private: - // Audio scope methods for rendering - void renderLineStrip(gpu::Batch& batch, int id, const glm::vec4& color, int x, int y, int n, int offset, const QByteArray* byteArray); // Audio scope methods for data acquisition int addBufferToScope(QByteArray* byteArray, int frameOffset, const int16_t* source, int sourceSamples, unsigned int sourceChannel, unsigned int sourceNumberOfChannels, float fade = 1.0f); int addSilenceToScope(QByteArray* byteArray, int frameOffset, int silentSamples); + QVector getScopeVector(const QByteArray* scope, int offset); + + bool shouldTrigger(const QVector& scope); + void computeInputData(); + void computeOutputData(); + + void storeTriggerValues(); + bool _isEnabled; bool _isPaused; + bool _isTriggered; + bool _autoTrigger; int _scopeInputOffset; int _scopeOutputOffset; int _framesPerScope; int _samplesPerScope; + QByteArray* _scopeInput; QByteArray* _scopeOutputLeft; QByteArray* _scopeOutputRight; QByteArray _scopeLastFrame; + + QVector _scopeInputData; + QVector _scopeOutputLeftData; + QVector _scopeOutputRightData; + + QVector _triggerInputData; + QVector _triggerOutputLeftData; + QVector _triggerOutputRightData; + + + glm::ivec2 _triggerValues; int _audioScopeBackground; int _audioScopeGrid; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 9ffe74d470..8a294182bd 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -142,32 +143,39 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { PerformanceTimer perfTimer("otherAvatars"); - auto avatarMap = getHashCopy(); - QList avatarList = avatarMap.values(); + class SortableAvatar: public PrioritySortUtil::Sortable { + public: + SortableAvatar() = delete; + SortableAvatar(const AvatarSharedPointer& avatar) : _avatar(avatar) {} + glm::vec3 getPosition() const override { return _avatar->getWorldPosition(); } + float getRadius() const override { return std::static_pointer_cast(_avatar)->getBoundingRadius(); } + uint64_t getTimestamp() const override { return std::static_pointer_cast(_avatar)->getLastRenderUpdateTime(); } + const AvatarSharedPointer& getAvatar() const { return _avatar; } + private: + AvatarSharedPointer _avatar; + }; + ViewFrustum cameraView; qApp->copyDisplayViewFrustum(cameraView); + PrioritySortUtil::PriorityQueue sortedAvatars(cameraView, + AvatarData::_avatarSortCoefficientSize, + AvatarData::_avatarSortCoefficientCenter, + AvatarData::_avatarSortCoefficientAge); - std::priority_queue sortedAvatars; - AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars, - - [](AvatarSharedPointer avatar)->uint64_t{ - return std::static_pointer_cast(avatar)->getLastRenderUpdateTime(); - }, - - [](AvatarSharedPointer avatar)->float{ - return std::static_pointer_cast(avatar)->getBoundingRadius(); - }, - - [this](AvatarSharedPointer avatar)->bool{ - const auto& castedAvatar = std::static_pointer_cast(avatar); - if (castedAvatar == _myAvatar || !castedAvatar->isInitialized()) { - // DO NOT update _myAvatar! Its update has already been done earlier in the main loop. - // DO NOT update or fade out uninitialized Avatars - return true; // ignore it - } - return false; - }); + // sort + auto avatarMap = getHashCopy(); + AvatarHash::iterator itr = avatarMap.begin(); + while (itr != avatarMap.end()) { + const auto& avatar = std::static_pointer_cast(*itr); + // DO NOT update _myAvatar! Its update has already been done earlier in the main loop. + // DO NOT update or fade out uninitialized Avatars + if (avatar != _myAvatar && avatar->isInitialized()) { + sortedAvatars.push(SortableAvatar(avatar)); + } + ++itr; + } + // process in sorted order uint64_t startTime = usecTimestampNow(); const uint64_t UPDATE_BUDGET = 2000; // usec uint64_t updateExpiry = startTime + UPDATE_BUDGET; @@ -176,8 +184,8 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { render::Transaction transaction; while (!sortedAvatars.empty()) { - const AvatarPriority& sortData = sortedAvatars.top(); - const auto& avatar = std::static_pointer_cast(sortData.avatar); + const SortableAvatar& sortData = sortedAvatars.top(); + const auto& avatar = std::static_pointer_cast(sortData.getAvatar()); bool ignoring = DependencyManager::get()->isPersonalMutingNode(avatar->getID()); if (ignoring) { @@ -207,7 +215,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { uint64_t now = usecTimestampNow(); if (now < updateExpiry) { // we're within budget - bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD; + bool inView = sortData.getPriority() > OUT_OF_VIEW_THRESHOLD; if (inView && avatar->hasNewJointData()) { numAvatarsUpdated++; } @@ -221,7 +229,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { // --> some avatar velocity measurements may be a little off // no time simulate, but we take the time to count how many were tragically missed - bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD; + bool inView = sortData.getPriority() > OUT_OF_VIEW_THRESHOLD; if (!inView) { break; } @@ -230,9 +238,9 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { } sortedAvatars.pop(); while (inView && !sortedAvatars.empty()) { - const AvatarPriority& newSortData = sortedAvatars.top(); - const auto& newAvatar = std::static_pointer_cast(newSortData.avatar); - inView = newSortData.priority > OUT_OF_VIEW_THRESHOLD; + const SortableAvatar& newSortData = sortedAvatars.top(); + const auto& newAvatar = std::static_pointer_cast(newSortData.getAvatar()); + inView = newSortData.getPriority() > OUT_OF_VIEW_THRESHOLD; if (inView && newAvatar->hasNewJointData()) { numAVatarsNotUpdated++; } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index df2089223b..c874205611 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -114,7 +114,8 @@ MyAvatar::MyAvatar(QThread* thread) : _skeletonModel = std::make_shared(this, nullptr); connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished); - + connect(_skeletonModel.get(), &Model::rigReady, this, &Avatar::rigReady); + connect(_skeletonModel.get(), &Model::rigReset, this, &Avatar::rigReset); using namespace recording; _skeletonModel->flagAsCauterized(); @@ -1516,9 +1517,19 @@ void MyAvatar::updateMotors() { _characterController.clearMotors(); glm::quat motorRotation; if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) { + + const float FLYING_MOTOR_TIMESCALE = 0.05f; + const float WALKING_MOTOR_TIMESCALE = 0.2f; + const float INVALID_MOTOR_TIMESCALE = 1.0e6f; + + float horizontalMotorTimescale; + float verticalMotorTimescale; + if (_characterController.getState() == CharacterController::State::Hover || _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { motorRotation = getMyHead()->getHeadOrientation(); + horizontalMotorTimescale = FLYING_MOTOR_TIMESCALE; + verticalMotorTimescale = FLYING_MOTOR_TIMESCALE; } else { // non-hovering = walking: follow camera twist about vertical but not lift // we decompose camera's rotation and store the twist part in motorRotation @@ -1529,11 +1540,12 @@ void MyAvatar::updateMotors() { glm::quat liftRotation; swingTwistDecomposition(headOrientation, Vectors::UNIT_Y, liftRotation, motorRotation); motorRotation = orientation * motorRotation; + horizontalMotorTimescale = WALKING_MOTOR_TIMESCALE; + verticalMotorTimescale = INVALID_MOTOR_TIMESCALE; } - const float DEFAULT_MOTOR_TIMESCALE = 0.2f; - const float INVALID_MOTOR_TIMESCALE = 1.0e6f; + if (_isPushing || _isBraking || !_isBeingPushed) { - _characterController.addMotor(_actionMotorVelocity, motorRotation, DEFAULT_MOTOR_TIMESCALE, INVALID_MOTOR_TIMESCALE); + _characterController.addMotor(_actionMotorVelocity, motorRotation, horizontalMotorTimescale, verticalMotorTimescale); } else { // _isBeingPushed must be true --> disable action motor by giving it a long timescale, // otherwise it's attempt to "stand in in place" could defeat scripted motor/thrusts @@ -1799,6 +1811,7 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { _skeletonModel->setCauterizeBoneSet(_headBoneSet); _fstAnimGraphOverrideUrl = _skeletonModel->getGeometry()->getAnimGraphOverrideUrl(); initAnimGraph(); + _isAnimatingScale = true; } if (_enableDebugDrawDefaultPose || _enableDebugDrawAnimPose) { @@ -1956,27 +1969,33 @@ void MyAvatar::updateOrientation(float deltaTime) { // Use head/HMD roll to turn while flying, but not when standing still. if (qApp->isHMDMode() && getCharacterController()->getState() == CharacterController::State::Hover && _hmdRollControlEnabled && hasDriveInput()) { + // Turn with head roll. - const float MIN_CONTROL_SPEED = 0.01f; - float speed = glm::length(getWorldVelocity()); - if (speed >= MIN_CONTROL_SPEED) { - // Feather turn when stopping moving. - float speedFactor; - if (getDriveKey(TRANSLATE_Z) != 0.0f || _lastDrivenSpeed == 0.0f) { - _lastDrivenSpeed = speed; - speedFactor = 1.0f; - } else { - speedFactor = glm::min(speed / _lastDrivenSpeed, 1.0f); - } + const float MIN_CONTROL_SPEED = 2.0f * getSensorToWorldScale(); // meters / sec + const glm::vec3 characterForward = getWorldOrientation() * Vectors::UNIT_NEG_Z; + float forwardSpeed = glm::dot(characterForward, getWorldVelocity()); - float direction = glm::dot(getWorldVelocity(), getWorldOrientation() * Vectors::UNIT_NEG_Z) > 0.0f ? 1.0f : -1.0f; + // only enable roll-turns if we are moving forward or backward at greater then MIN_CONTROL_SPEED + if (fabsf(forwardSpeed) >= MIN_CONTROL_SPEED) { + float direction = forwardSpeed > 0.0f ? 1.0f : -1.0f; float rollAngle = glm::degrees(asinf(glm::dot(IDENTITY_UP, _hmdSensorOrientation * IDENTITY_RIGHT))); float rollSign = rollAngle < 0.0f ? -1.0f : 1.0f; rollAngle = fabsf(rollAngle); - rollAngle = rollAngle > _hmdRollControlDeadZone ? rollSign * (rollAngle - _hmdRollControlDeadZone) : 0.0f; - totalBodyYaw += speedFactor * direction * rollAngle * deltaTime * _hmdRollControlRate; + const float MIN_ROLL_ANGLE = _hmdRollControlDeadZone; + const float MAX_ROLL_ANGLE = 90.0f; // degrees + + if (rollAngle > MIN_ROLL_ANGLE) { + // rate of turning is linearly proportional to rollAngle + rollAngle = glm::clamp(rollAngle, MIN_ROLL_ANGLE, MAX_ROLL_ANGLE); + + // scale rollAngle into a value from zero to one. + float rollFactor = (rollAngle - MIN_ROLL_ANGLE) / (MAX_ROLL_ANGLE - MIN_ROLL_ANGLE); + + float angularSpeed = rollSign * rollFactor * _hmdRollControlRate; + totalBodyYaw += direction * angularSpeed * deltaTime; + } } } @@ -2022,12 +2041,13 @@ void MyAvatar::updateActionMotor(float deltaTime) { _isBraking = _wasPushing || (_isBraking && speed > MIN_ACTION_BRAKE_SPEED); } + CharacterController::State state = _characterController.getState(); + // compute action input glm::vec3 forward = (getDriveKey(TRANSLATE_Z)) * IDENTITY_FORWARD; glm::vec3 right = (getDriveKey(TRANSLATE_X)) * IDENTITY_RIGHT; glm::vec3 direction = forward + right; - CharacterController::State state = _characterController.getState(); if (state == CharacterController::State::Hover || _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { // we can fly --> support vertical motion @@ -2161,41 +2181,6 @@ bool findAvatarAvatarPenetration(const glm::vec3 positionA, float radiusA, float // target scale to match the new scale they have chosen. When they leave the domain they will not return to the scale they were // before they entered the limiting domain. -void MyAvatar::clampTargetScaleToDomainLimits() { - // when we're about to change the target scale because the user has asked to increase or decrease their scale, - // we first make sure that we're starting from a target scale that is allowed by the current domain - - auto clampedTargetScale = glm::clamp(_targetScale, _domainMinimumScale, _domainMaximumScale); - - if (clampedTargetScale != _targetScale) { - qCDebug(interfaceapp, "Clamped scale to %f since original target scale %f was not allowed by domain", - (double)clampedTargetScale, (double)_targetScale); - - setTargetScale(clampedTargetScale); - } -} - -void MyAvatar::clampScaleChangeToDomainLimits(float desiredScale) { - auto clampedTargetScale = glm::clamp(desiredScale, _domainMinimumScale, _domainMaximumScale); - - if (clampedTargetScale != desiredScale) { - qCDebug(interfaceapp, "Forcing scale to %f since %f is not allowed by domain", - clampedTargetScale, desiredScale); - } - - setTargetScale(clampedTargetScale); - qCDebug(interfaceapp, "Changed scale to %f", (double)_targetScale); - emit(scaleChanged()); -} - -float MyAvatar::getDomainMinScale() { - return _domainMinimumScale; -} - -float MyAvatar::getDomainMaxScale() { - return _domainMaximumScale; -} - void MyAvatar::setGravity(float gravity) { _characterController.setGravity(gravity); } @@ -2205,70 +2190,58 @@ float MyAvatar::getGravity() { } void MyAvatar::increaseSize() { - // make sure we're starting from an allowable scale - clampTargetScaleToDomainLimits(); + float minScale = getDomainMinScale(); + float maxScale = getDomainMaxScale(); - // calculate what our new scale should be - float updatedTargetScale = _targetScale * (1.0f + SCALING_RATIO); + float clampedTargetScale = glm::clamp(_targetScale, minScale, maxScale); + float newTargetScale = glm::clamp(clampedTargetScale * (1.0f + SCALING_RATIO), minScale, maxScale); - // attempt to change to desired scale (clamped to the domain limits) - clampScaleChangeToDomainLimits(updatedTargetScale); + setTargetScale(newTargetScale); } void MyAvatar::decreaseSize() { - // make sure we're starting from an allowable scale - clampTargetScaleToDomainLimits(); + float minScale = getDomainMinScale(); + float maxScale = getDomainMaxScale(); - // calculate what our new scale should be - float updatedTargetScale = _targetScale * (1.0f - SCALING_RATIO); + float clampedTargetScale = glm::clamp(_targetScale, minScale, maxScale); + float newTargetScale = glm::clamp(clampedTargetScale * (1.0f - SCALING_RATIO), minScale, maxScale); - // attempt to change to desired scale (clamped to the domain limits) - clampScaleChangeToDomainLimits(updatedTargetScale); + setTargetScale(newTargetScale); } void MyAvatar::resetSize() { // attempt to reset avatar size to the default (clamped to domain limits) const float DEFAULT_AVATAR_SCALE = 1.0f; - - clampScaleChangeToDomainLimits(DEFAULT_AVATAR_SCALE); + setTargetScale(DEFAULT_AVATAR_SCALE); } void MyAvatar::restrictScaleFromDomainSettings(const QJsonObject& domainSettingsObject) { - // pull out the minimum and maximum scale and set them to restrict our scale + // pull out the minimum and maximum height and set them to restrict our scale static const QString AVATAR_SETTINGS_KEY = "avatars"; auto avatarsObject = domainSettingsObject[AVATAR_SETTINGS_KEY].toObject(); - static const QString MIN_SCALE_OPTION = "min_avatar_scale"; - float settingMinScale = avatarsObject[MIN_SCALE_OPTION].toDouble(MIN_AVATAR_SCALE); - setDomainMinimumScale(settingMinScale); + static const QString MIN_HEIGHT_OPTION = "min_avatar_height"; + float settingMinHeight = avatarsObject[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT); + setDomainMinimumHeight(settingMinHeight); - static const QString MAX_SCALE_OPTION = "max_avatar_scale"; - float settingMaxScale = avatarsObject[MAX_SCALE_OPTION].toDouble(MAX_AVATAR_SCALE); - setDomainMaximumScale(settingMaxScale); + static const QString MAX_HEIGHT_OPTION = "max_avatar_height"; + float settingMaxHeight = avatarsObject[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT); + setDomainMaximumHeight(settingMaxHeight); // make sure that the domain owner didn't flip min and max - if (_domainMinimumScale > _domainMaximumScale) { - std::swap(_domainMinimumScale, _domainMaximumScale); + if (_domainMinimumHeight > _domainMaximumHeight) { + std::swap(_domainMinimumHeight, _domainMaximumHeight); } // Set avatar current scale Settings settings; settings.beginGroup("Avatar"); _targetScale = loadSetting(settings, "scale", 1.0f); - qCDebug(interfaceapp) << "This domain requires a minimum avatar scale of " << _domainMinimumScale - << " and a maximum avatar scale of " << _domainMaximumScale - << ". Current avatar scale is " << _targetScale; + qCDebug(interfaceapp) << "This domain requires a minimum avatar scale of " << _domainMinimumHeight + << " and a maximum avatar scale of " << _domainMaximumHeight; - // debug to log if this avatar's scale in this domain will be clamped - float clampedScale = glm::clamp(_targetScale, _domainMinimumScale, _domainMaximumScale); - - if (_targetScale != clampedScale) { - qCDebug(interfaceapp) << "Current avatar scale is clamped to " << clampedScale - << " because " << _targetScale << " is not allowed by current domain"; - // The current scale of avatar should not be more than domain's max_avatar_scale and not less than domain's min_avatar_scale . - _targetScale = clampedScale; - } + _isAnimatingScale = true; setModelScale(_targetScale); rebuildCollisionShape(); @@ -2288,8 +2261,8 @@ void MyAvatar::saveAvatarScale() { } void MyAvatar::clearScaleRestriction() { - _domainMinimumScale = MIN_AVATAR_SCALE; - _domainMaximumScale = MAX_AVATAR_SCALE; + _domainMinimumHeight = MIN_AVATAR_HEIGHT; + _domainMaximumHeight = MAX_AVATAR_HEIGHT; } void MyAvatar::goToLocation(const QVariant& propertiesVar) { @@ -3248,6 +3221,7 @@ void MyAvatar::setModelScale(float scale) { if (changed) { float sensorToWorldScale = getEyeHeight() / getUserEyeHeight(); emit sensorToWorldScaleChanged(sensorToWorldScale); + emit scaleChanged(); } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 563fb7fccd..7c9513cb3e 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -110,6 +110,10 @@ class MyAvatar : public Avatar { * @property userEyeHeight {number} Estimated height of the users eyes in sensor space. (meters) * @property SELF_ID {string} READ-ONLY. UUID representing "my avatar". Only use for local-only entities and overlays in situations where MyAvatar.sessionUUID is not available (e.g., if not connected to a domain). * Note: Likely to be deprecated. + * @property hmdRollControlEnabled {bool} When enabled the roll angle of your HMD will turn your avatar while flying. + * @property hmdRollControlDeadZone {number} If hmdRollControlEnabled is true, this value can be used to tune what roll angle is required to begin turning. + * This angle is specified in degrees. + * @property hmdRollControlRate {number} If hmdRollControlEnabled is true, this value determines the maximum turn rate of your avatar when rolling your HMD in degrees per second. */ // FIXME: `glm::vec3 position` is not accessible from QML, so this exposes position in a QML-native type @@ -158,7 +162,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(float userEyeHeight READ getUserEyeHeight) Q_PROPERTY(QUuid SELF_ID READ getSelfID CONSTANT) - + const QString DOMINANT_LEFT_HAND = "left"; const QString DOMINANT_RIGHT_HAND = "right"; @@ -558,8 +562,6 @@ public slots: void increaseSize(); void decreaseSize(); void resetSize(); - float getDomainMinScale(); - float getDomainMaxScale(); void setGravity(float gravity); float getGravity(); @@ -737,12 +739,12 @@ private: bool _clearOverlayWhenMoving { true }; QString _dominantHand { DOMINANT_RIGHT_HAND }; - const float ROLL_CONTROL_DEAD_ZONE_DEFAULT = 8.0f; // deg - const float ROLL_CONTROL_RATE_DEFAULT = 2.5f; // deg/sec/deg + const float ROLL_CONTROL_DEAD_ZONE_DEFAULT = 8.0f; // degrees + const float ROLL_CONTROL_RATE_DEFAULT = 114.0f; // degrees / sec + bool _hmdRollControlEnabled { true }; float _hmdRollControlDeadZone { ROLL_CONTROL_DEAD_ZONE_DEFAULT }; float _hmdRollControlRate { ROLL_CONTROL_RATE_DEFAULT }; - float _lastDrivenSpeed { 0.0f }; // working copy -- see AvatarData for thread-safe _sensorToWorldMatrixCache, used for outward facing access glm::mat4 _sensorToWorldMatrix { glm::mat4() }; diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 435d15d161..69089df9c2 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -83,19 +83,28 @@ void QmlCommerce::buy(const QString& assetId, int cost, const bool controlledFai void QmlCommerce::balance() { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); - ledger->balance(wallet->listPublicKeys()); + QStringList cachedPublicKeys = wallet->listPublicKeys(); + if (!cachedPublicKeys.isEmpty()) { + ledger->balance(cachedPublicKeys); + } } void QmlCommerce::inventory() { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); - ledger->inventory(wallet->listPublicKeys()); + QStringList cachedPublicKeys = wallet->listPublicKeys(); + if (!cachedPublicKeys.isEmpty()) { + ledger->inventory(cachedPublicKeys); + } } void QmlCommerce::history() { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); - ledger->history(wallet->listPublicKeys()); + QStringList cachedPublicKeys = wallet->listPublicKeys(); + if (!cachedPublicKeys.isEmpty()) { + ledger->history(cachedPublicKeys); + } } void QmlCommerce::changePassphrase(const QString& oldPassphrase, const QString& newPassphrase) { diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index d4611d3e9a..69914e97a4 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -321,6 +321,16 @@ Wallet::Wallet() { auto accountManager = DependencyManager::get(); connect(accountManager.data(), &AccountManager::usernameChanged, this, [&]() { getWalletStatus(); + _publicKeys.clear(); + + if (_securityImage) { + delete _securityImage; + } + _securityImage = nullptr; + + // tell the provider we got nothing + updateImageProvider(); + _passphrase->clear(); }); } diff --git a/interface/src/raypick/RayPick.cpp b/interface/src/raypick/RayPick.cpp index 2f6e69bc7e..501e8d1e42 100644 --- a/interface/src/raypick/RayPick.cpp +++ b/interface/src/raypick/RayPick.cpp @@ -84,18 +84,7 @@ glm::vec2 RayPick::projectOntoXYPlane(const glm::vec3& worldPos, const glm::vec3 glm::vec2 RayPick::projectOntoOverlayXYPlane(const QUuid& overlayID, const glm::vec3& worldPos, bool unNormalized) { glm::vec3 position = vec3FromVariant(qApp->getOverlays().getProperty(overlayID, "position").value); glm::quat rotation = quatFromVariant(qApp->getOverlays().getProperty(overlayID, "rotation").value); - glm::vec3 dimensions; - - float dpi = qApp->getOverlays().getProperty(overlayID, "dpi").value.toFloat(); - if (dpi > 0) { - // Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property is used as a scale. - glm::vec3 resolution = glm::vec3(vec2FromVariant(qApp->getOverlays().getProperty(overlayID, "resolution").value), 1); - glm::vec3 scale = glm::vec3(vec2FromVariant(qApp->getOverlays().getProperty(overlayID, "dimensions").value), 0.01f); - const float INCHES_TO_METERS = 1.0f / 39.3701f; - dimensions = (resolution * INCHES_TO_METERS / dpi) * scale; - } else { - dimensions = glm::vec3(vec2FromVariant(qApp->getOverlays().getProperty(overlayID, "dimensions").value), 0.01); - } + glm::vec3 dimensions = glm::vec3(vec2FromVariant(qApp->getOverlays().getProperty(overlayID, "dimensions").value), 0.01f); return projectOntoXYPlane(worldPos, position, rotation, dimensions, ENTITY_ITEM_DEFAULT_REGISTRATION_POINT, unNormalized); } diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index f9c1a95fb5..be9b4280f7 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -58,6 +58,21 @@ Audio::Audio() : _devices(_contextIsHMD) { enableNoiseReduction(enableNoiseReductionSetting.get()); } +bool Audio::startRecording(const QString& filepath) { + auto client = DependencyManager::get().data(); + return client->startRecording(filepath); +} + +bool Audio::getRecording() { + auto client = DependencyManager::get().data(); + return client->getRecording(); +} + +void Audio::stopRecording() { + auto client = DependencyManager::get().data(); + client->stopRecording(); +} + void Audio::setMuted(bool isMuted) { if (_isMuted != isMuted) { auto client = DependencyManager::get().data(); diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index abd2312cf0..0f0043510c 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -16,6 +16,7 @@ #include "AudioDevices.h" #include "AudioEffectOptions.h" #include "SettingHandle.h" +#include "AudioFileWav.h" namespace scripting { @@ -55,6 +56,10 @@ public: Q_INVOKABLE void setReverb(bool enable); Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); + Q_INVOKABLE bool startRecording(const QString& filename); + Q_INVOKABLE void stopRecording(); + Q_INVOKABLE bool getRecording(); + signals: void nop(); void mutedChanged(bool isMuted); @@ -83,7 +88,6 @@ private: bool _isMuted { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _contextIsHMD { false }; - AudioDevices* getDevices() { return &_devices; } AudioDevices _devices; }; diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index c99e190d12..4b355653b6 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -176,6 +176,10 @@ bool WindowScriptingInterface::isPointOnDesktopWindow(QVariant point) { return offscreenUi->isPointOnDesktopWindow(point); } +glm::vec2 WindowScriptingInterface::getDeviceSize() const { + return qApp->getDeviceSize(); +} + /// Makes sure that the reticle is visible, use this in blocking forms that require a reticle and /// might be in same thread as a script that sets the reticle to invisible void WindowScriptingInterface::ensureReticleVisible() const { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 61aaec7bea..d223f95af4 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -12,6 +12,8 @@ #ifndef hifi_WindowScriptingInterface_h #define hifi_WindowScriptingInterface_h +#include + #include #include #include @@ -73,6 +75,7 @@ public slots: bool isPhysicsEnabled(); bool setDisplayTexture(const QString& name); bool isPointOnDesktopWindow(QVariant point); + glm::vec2 getDeviceSize() const; int openMessageBox(QString title, QString text, int buttons, int defaultButton); void updateMessageBox(int id, QString title, QString text, int buttons, int defaultButton); diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index a99fe002ee..52b53a3298 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -82,7 +82,6 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) { // Now render the overlay components together into a single texture renderDomainConnectionStatusBorder(renderArgs); // renders the connected domain line - renderAudioScope(renderArgs); // audio scope in the very back - NOTE: this is the debug audio scope, not the VU meter renderOverlays(renderArgs); // renders Scripts Overlay and AudioScope renderQmlUi(renderArgs); // renders a unit quad with the QML UI texture, and the text overlays from scripts }); @@ -118,25 +117,6 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { geometryCache->renderUnitQuad(batch, glm::vec4(1), _qmlGeometryId); } -void ApplicationOverlay::renderAudioScope(RenderArgs* renderArgs) { - PROFILE_RANGE(app, __FUNCTION__); - - gpu::Batch& batch = *renderArgs->_batch; - auto geometryCache = DependencyManager::get(); - geometryCache->useSimpleDrawPipeline(batch); - auto textureCache = DependencyManager::get(); - batch.setResourceTexture(0, textureCache->getWhiteTexture()); - int width = renderArgs->_viewport.z; - int height = renderArgs->_viewport.w; - mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); - batch.setProjectionTransform(legacyProjection); - batch.setModelTransform(Transform()); - batch.resetViewTransform(); - - // Render the audio scope - DependencyManager::get()->render(renderArgs, width, height); -} - void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) { PROFILE_RANGE(app, __FUNCTION__); diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index af4d8779d4..0d30123c61 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -32,7 +32,6 @@ private: void renderStatsAndLogs(RenderArgs* renderArgs); void renderDomainConnectionStatusBorder(RenderArgs* renderArgs); void renderQmlUi(RenderArgs* renderArgs); - void renderAudioScope(RenderArgs* renderArgs); void renderOverlays(RenderArgs* renderArgs); void buildFramebufferObject(); diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 09d9ba574a..f27e26280a 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -35,7 +35,8 @@ ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) : _modelTextures(QVariantMap()), _url(modelOverlay->_url), _updateModel(false), - _loadPriority(modelOverlay->getLoadPriority()) + _scaleToFit(modelOverlay->_scaleToFit), + _loadPriority(modelOverlay->_loadPriority) { _model->init(); _model->setLoadingPriority(_loadPriority); diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index ea0eff170c..c4506d9621 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -75,8 +75,8 @@ private: QVariantMap _modelTextures; QUrl _url; - bool _updateModel = { false }; - bool _scaleToFit = { false }; + bool _updateModel { false }; + bool _scaleToFit { false }; float _loadPriority { 0.0f }; AnimationPointer _animation; @@ -87,7 +87,7 @@ private: bool _animationRunning { false }; bool _animationLoop { false }; float _animationFirstFrame { 0.0f }; - float _animationLastFrame = { 0.0f }; + float _animationLastFrame { 0.0f }; bool _animationHold { false }; bool _animationAllowTranslation { false }; uint64_t _lastAnimated { 0 }; diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index a8f504bbc5..e1996e6bfc 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -305,13 +305,6 @@ public slots: OverlayID getKeyboardFocusOverlay(); void setKeyboardFocusOverlay(const OverlayID& id); - void mousePressPointerEvent(const OverlayID& overlayID, const PointerEvent& event); - void mouseMovePointerEvent(const OverlayID& overlayID, const PointerEvent& event); - void mouseReleasePointerEvent(const OverlayID& overlayID, const PointerEvent& event); - void hoverEnterPointerEvent(const OverlayID& overlayID, const PointerEvent& event); - void hoverOverPointerEvent(const OverlayID& overlayID, const PointerEvent& event); - void hoverLeavePointerEvent(const OverlayID& overlayID, const PointerEvent& event); - signals: /**jsdoc * Emitted when an overlay is deleted @@ -358,6 +351,14 @@ private: OverlayID _currentHoverOverOverlayID { UNKNOWN_OVERLAY_ID }; RayToOverlayIntersectionResult findRayIntersectionForMouseEvent(PickRay ray); + +private slots: + void mousePressPointerEvent(const OverlayID& overlayID, const PointerEvent& event); + void mouseMovePointerEvent(const OverlayID& overlayID, const PointerEvent& event); + void mouseReleasePointerEvent(const OverlayID& overlayID, const PointerEvent& event); + void hoverEnterPointerEvent(const OverlayID& overlayID, const PointerEvent& event); + void hoverOverPointerEvent(const OverlayID& overlayID, const PointerEvent& event); + void hoverLeavePointerEvent(const OverlayID& overlayID, const PointerEvent& event); }; #endif // hifi_Overlays_h diff --git a/interface/src/ui/overlays/Shape3DOverlay.cpp b/interface/src/ui/overlays/Shape3DOverlay.cpp index ca6446d215..07949d5349 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.cpp +++ b/interface/src/ui/overlays/Shape3DOverlay.cpp @@ -18,8 +18,9 @@ QString const Shape3DOverlay::TYPE = "shape"; -Shape3DOverlay::Shape3DOverlay(const Shape3DOverlay* Shape3DOverlay) : - Volume3DOverlay(Shape3DOverlay) +Shape3DOverlay::Shape3DOverlay(const Shape3DOverlay* shape3DOverlay) : + Volume3DOverlay(shape3DOverlay), + _shape(shape3DOverlay->_shape) { } diff --git a/interface/src/ui/overlays/Shape3DOverlay.h b/interface/src/ui/overlays/Shape3DOverlay.h index e9e26e3c94..7fc95ec981 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.h +++ b/interface/src/ui/overlays/Shape3DOverlay.h @@ -23,7 +23,7 @@ public: virtual QString getType() const override { return TYPE; } Shape3DOverlay() {} - Shape3DOverlay(const Shape3DOverlay* Shape3DOverlay); + Shape3DOverlay(const Shape3DOverlay* shape3DOverlay); virtual void render(RenderArgs* args) override; virtual const render::ShapeKey getShapeKey() override; diff --git a/interface/src/ui/overlays/Volume3DOverlay.cpp b/interface/src/ui/overlays/Volume3DOverlay.cpp index b580545288..49c76e6108 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.cpp +++ b/interface/src/ui/overlays/Volume3DOverlay.cpp @@ -14,7 +14,8 @@ #include Volume3DOverlay::Volume3DOverlay(const Volume3DOverlay* volume3DOverlay) : - Base3DOverlay(volume3DOverlay) + Base3DOverlay(volume3DOverlay), + _localBoundingBox(volume3DOverlay->_localBoundingBox) { } diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 34b8c6c865..7d5fefcb31 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -55,17 +55,15 @@ #include #include "ui/Snapshot.h" #include "SoundCache.h" - #include "raypick/PointerScriptingInterface.h" -static const float DPI = 30.47f; -static const float INCHES_TO_METERS = 1.0f / 39.3701f; +static int MAX_WINDOW_SIZE = 4096; static const float METERS_TO_INCHES = 39.3701f; static const float OPAQUE_ALPHA_THRESHOLD = 0.99f; const QString Web3DOverlay::TYPE = "web3d"; const QString Web3DOverlay::QML = "Web3DOverlay.qml"; -Web3DOverlay::Web3DOverlay() : _dpi(DPI) { +Web3DOverlay::Web3DOverlay() { _touchDevice.setCapabilities(QTouchDevice::Position); _touchDevice.setType(QTouchDevice::TouchScreen); _touchDevice.setName("Web3DOverlayTouchDevice"); @@ -82,7 +80,6 @@ Web3DOverlay::Web3DOverlay(const Web3DOverlay* Web3DOverlay) : _url(Web3DOverlay->_url), _scriptURL(Web3DOverlay->_scriptURL), _dpi(Web3DOverlay->_dpi), - _resolution(Web3DOverlay->_resolution), _showKeyboardFocusHighlight(Web3DOverlay->_showKeyboardFocusHighlight) { _geometryId = DependencyManager::get()->allocateID(); @@ -154,7 +151,7 @@ void Web3DOverlay::buildWebSurface() { setupQmlSurface(); } _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(getWorldPosition())); - _webSurface->resize(QSize(_resolution.x, _resolution.y)); + onResizeWebSurface(); _webSurface->resume(); }); @@ -244,8 +241,16 @@ void Web3DOverlay::setMaxFPS(uint8_t maxFPS) { } void Web3DOverlay::onResizeWebSurface() { - _mayNeedResize = false; - _webSurface->resize(QSize(_resolution.x, _resolution.y)); + glm::vec2 dims = glm::vec2(getDimensions()); + dims *= METERS_TO_INCHES * _dpi; + + // ensure no side is never larger then MAX_WINDOW_SIZE + float max = (dims.x > dims.y) ? dims.x : dims.y; + if (max > MAX_WINDOW_SIZE) { + dims *= MAX_WINDOW_SIZE / max; + } + + _webSurface->resize(QSize(dims.x, dims.y)); } unsigned int Web3DOverlay::deviceIdByTouchPoint(qreal x, qreal y) { @@ -266,14 +271,14 @@ void Web3DOverlay::render(RenderArgs* args) { return; } - if (_currentMaxFPS != _desiredMaxFPS) { - setMaxFPS(_desiredMaxFPS); - } - if (_mayNeedResize) { emit resizeWebSurface(); } + if (_currentMaxFPS != _desiredMaxFPS) { + setMaxFPS(_desiredMaxFPS); + } + vec4 color(toGlm(getColor()), getAlpha()); if (!_texture) { @@ -310,7 +315,7 @@ void Web3DOverlay::render(RenderArgs* args) { Transform Web3DOverlay::evalRenderTransform() { Transform transform = Parent::evalRenderTransform(); transform.setScale(1.0f); - transform.postScale(glm::vec3(getSize(), 1.0f)); + transform.postScale(glm::vec3(getDimensions(), 1.0f)); return transform; } @@ -434,18 +439,10 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) { } } - auto resolution = properties["resolution"]; - if (resolution.isValid()) { - bool valid; - auto res = vec2FromVariant(resolution, valid); - if (valid) { - _resolution = res; - } - } - auto dpi = properties["dpi"]; if (dpi.isValid()) { _dpi = dpi.toFloat(); + _mayNeedResize = true; } auto maxFPS = properties["maxFPS"]; @@ -467,8 +464,6 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) { _inputMode = Touch; } } - - _mayNeedResize = true; } QVariant Web3DOverlay::getProperty(const QString& property) { @@ -478,9 +473,6 @@ QVariant Web3DOverlay::getProperty(const QString& property) { if (property == "scriptURL") { return _scriptURL; } - if (property == "resolution") { - return vec2toVariant(_resolution); - } if (property == "dpi") { return _dpi; } @@ -536,17 +528,18 @@ void Web3DOverlay::setScriptURL(const QString& scriptURL) { } } -glm::vec2 Web3DOverlay::getSize() const { - return _resolution / _dpi * INCHES_TO_METERS * getDimensions(); -}; - bool Web3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) { - // FIXME - face and surfaceNormal not being returned + glm::vec2 dimensions = getDimensions(); + glm::quat rotation = getWorldOrientation(); + glm::vec3 position = getWorldPosition(); - // Don't call applyTransformTo() or setTransform() here because this code runs too frequently. - - // Produce the dimensions of the overlay based on the image's aspect ratio and the overlay's scale. - return findRayRectangleIntersection(origin, direction, getWorldOrientation(), getWorldPosition(), getSize(), distance); + if (findRayRectangleIntersection(origin, direction, rotation, position, dimensions, distance)) { + surfaceNormal = rotation * Vectors::UNIT_Z; + face = glm::dot(surfaceNormal, direction) > 0 ? MIN_Z_FACE : MAX_Z_FACE; + return true; + } else { + return false; + } } Web3DOverlay* Web3DOverlay::createClone() const { @@ -555,4 +548,4 @@ Web3DOverlay* Web3DOverlay::createClone() const { void Web3DOverlay::emitScriptEvent(const QVariant& message) { QMetaObject::invokeMethod(this, "scriptEventReceived", Q_ARG(QVariant, message)); -} +} \ No newline at end of file diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 563c1c0c4e..4098e98488 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -52,8 +52,6 @@ public: void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; - glm::vec2 getSize() const override; - virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) override; @@ -93,10 +91,9 @@ private: gpu::TexturePointer _texture; QString _url; QString _scriptURL; - float _dpi; - vec2 _resolution{ 640, 480 }; + float _dpi { 30.0f }; int _geometryId { 0 }; - bool _showKeyboardFocusHighlight{ true }; + bool _showKeyboardFocusHighlight { true }; QTouchDevice _touchDevice; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index e9cc444bd4..e738ad1c19 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -231,6 +231,9 @@ public: const glm::mat4& getGeometryToRigTransform() const { return _geometryToRigTransform; } + const AnimPose& getModelOffsetPose() const { return _modelOffset; } + const AnimPose& getGeometryOffsetPose() const { return _geometryOffset; } + void setEnableDebugDrawIKTargets(bool enableDebugDrawIKTargets) { _enableDebugDrawIKTargets = enableDebugDrawIKTargets; } void setEnableDebugDrawIKConstraints(bool enableDebugDrawIKConstraints) { _enableDebugDrawIKConstraints = enableDebugDrawIKConstraints; } void setEnableDebugDrawIKChains(bool enableDebugDrawIKChains) { _enableDebugDrawIKChains = enableDebugDrawIKChains; } diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 78475f5b68..af86499101 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -79,6 +79,7 @@ Setting::Handle staticJitterBufferFrames("staticJitterBufferFrames", using Mutex = std::mutex; using Lock = std::unique_lock; Mutex _deviceMutex; +Mutex _recordMutex; // thread-safe QList getAvailableDevices(QAudio::Mode mode) { @@ -222,8 +223,7 @@ AudioClient::AudioClient() : // initialize wasapi; if getAvailableDevices is called from the CheckDevicesThread before this, it will crash getAvailableDevices(QAudio::AudioInput); getAvailableDevices(QAudio::AudioOutput); - - + // start a thread to detect any device changes _checkDevicesTimer = new QTimer(this); connect(_checkDevicesTimer, &QTimer::timeout, [this] { @@ -1845,11 +1845,9 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, _receivedAudioStream.getSamplesAvailable(), samplesRequested); AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); - for (int i = 0; i < networkSamplesPopped; i++) { mixBuffer[i] = convertToFloat(scratchBuffer[i]); } - samplesRequested = networkSamplesPopped; } @@ -1911,6 +1909,13 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { bytesWritten = maxSize; } + // send output buffer for recording + if (_audio->_isRecording) { + Lock lock(_recordMutex); + _audio->_audioFileWav.addRawAudioChunk(reinterpret_cast(scratchBuffer), bytesWritten); + } + + int bytesAudioOutputUnplayed = _audio->_audioOutput->bufferSize() - _audio->_audioOutput->bytesFree(); float msecsAudioOutputUnplayed = bytesAudioOutputUnplayed / (float)_audio->_outputFormat.bytesForDuration(USECS_PER_MSEC); _audio->_stats.updateOutputMsUnplayed(msecsAudioOutputUnplayed); @@ -1922,6 +1927,22 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { return bytesWritten; } +bool AudioClient::startRecording(const QString& filepath) { + if (!_audioFileWav.create(_outputFormat, filepath)) { + qDebug() << "Error creating audio file: " + filepath; + return false; + } + _isRecording = true; + return true; +} + +void AudioClient::stopRecording() { + if (_isRecording) { + _isRecording = false; + _audioFileWav.close(); + } +} + void AudioClient::loadSettings() { _receivedAudioStream.setDynamicJitterBufferEnabled(dynamicJitterBufferEnabled.get()); _receivedAudioStream.setStaticJitterBufferFrames(staticJitterBufferFrames.get()); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 01a487455c..0ceb9c4dc3 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -47,11 +47,13 @@ #include #include + #include #include #include "AudioIOStats.h" +#include "AudioFileWav.h" #ifdef _WIN32 #pragma warning( push ) @@ -67,7 +69,6 @@ class QAudioInput; class QAudioOutput; class QIODevice; - class Transform; class NLPacket; @@ -118,6 +119,8 @@ public: const MixedProcessedAudioStream& getReceivedAudioStream() const { return _receivedAudioStream; } MixedProcessedAudioStream& getReceivedAudioStream() { return _receivedAudioStream; } + const QAudioFormat& getOutputFormat() const { return _outputFormat; } + float getLastInputLoudness() const { return _lastInputLoudness; } // TODO: relative to noise floor? float getTimeSinceLastClip() const { return _timeSinceLastClip; } @@ -142,7 +145,7 @@ public: void setIsPlayingBackRecording(bool isPlayingBackRecording) { _isPlayingBackRecording = isPlayingBackRecording; } Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale); - + bool outputLocalInjector(const AudioInjectorPointer& injector) override; QAudioDeviceInfo getActiveAudioDevice(QAudio::Mode mode) const; @@ -155,6 +158,13 @@ public: bool getNamedAudioDeviceForModeExists(QAudio::Mode mode, const QString& deviceName); + void setRecording(bool isRecording) { _isRecording = isRecording; }; + bool getRecording() { return _isRecording; }; + + bool startRecording(const QString& filename); + void stopRecording(); + + #ifdef Q_OS_WIN static QString getWinDeviceName(wchar_t* guid); #endif @@ -184,13 +194,17 @@ public slots: void toggleMute(); bool isMuted() { return _muted; } - virtual void setIsStereoInput(bool stereo) override; void setNoiseReduction(bool isNoiseGateEnabled); bool isNoiseReductionEnabled() const { return _isNoiseGateEnabled; } + bool getLocalEcho() { return _shouldEchoLocally; } + void setLocalEcho(bool localEcho) { _shouldEchoLocally = localEcho; } void toggleLocalEcho() { _shouldEchoLocally = !_shouldEchoLocally; } + + bool getServerEcho() { return _shouldEchoToServer; } + void setServerEcho(bool serverEcho) { _shouldEchoToServer = serverEcho; } void toggleServerEcho() { _shouldEchoToServer = !_shouldEchoToServer; } void processReceivedSamples(const QByteArray& inputBuffer, QByteArray& outputBuffer); @@ -239,6 +253,8 @@ signals: void muteEnvironmentRequested(glm::vec3 position, float radius); + void outputBufferReceived(const QByteArray _outputBuffer); + protected: AudioClient(); ~AudioClient(); @@ -354,9 +370,8 @@ private: int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; float* _localOutputMixBuffer { NULL }; Mutex _localAudioMutex; - AudioLimiter _audioLimiter; - + // Adds Reverb void configureReverb(); void updateReverbOptions(); @@ -391,6 +406,8 @@ private: QList _inputDevices; QList _outputDevices; + AudioFileWav _audioFileWav; + bool _hasReceivedFirstPacket { false }; QVector _activeLocalAudioInjectors; @@ -412,6 +429,8 @@ private: QTimer* _checkDevicesTimer { nullptr }; QTimer* _checkPeakValuesTimer { nullptr }; + + bool _isRecording { false }; }; diff --git a/libraries/audio-client/src/AudioFileWav.cpp b/libraries/audio-client/src/AudioFileWav.cpp new file mode 100644 index 0000000000..613628883c --- /dev/null +++ b/libraries/audio-client/src/AudioFileWav.cpp @@ -0,0 +1,69 @@ +// +// AudioWavFile.h +// libraries/audio-client/src +// +// Created by Luis Cuenca on 12/1/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AudioFileWav.h" + +bool AudioFileWav::create(const QAudioFormat& audioFormat, const QString& filepath) { + if (_file.isOpen()) { + _file.close(); + } + _file.setFileName(filepath); + if (!_file.open(QIODevice::WriteOnly)) { + return false; + } + addHeader(audioFormat); + return true; +} + +bool AudioFileWav::addRawAudioChunk(char* chunk, int size) { + if (_file.isOpen()) { + QDataStream stream(&_file); + stream.writeRawData(chunk, size); + return true; + } + return false; +} + +void AudioFileWav::close() { + QDataStream stream(&_file); + stream.setByteOrder(QDataStream::LittleEndian); + + // fill RIFF and size data on header + _file.seek(4); + stream << quint32(_file.size() - 8); + _file.seek(40); + stream << quint32(_file.size() - 44); + _file.close(); +} + +void AudioFileWav::addHeader(const QAudioFormat& audioFormat) { + QDataStream stream(&_file); + + stream.setByteOrder(QDataStream::LittleEndian); + + // RIFF + stream.writeRawData("RIFF", 4); + stream << quint32(0); + stream.writeRawData("WAVE", 4); + + // Format description PCM = 16 + stream.writeRawData("fmt ", 4); + stream << quint32(16); + stream << quint16(1); + stream << quint16(audioFormat.channelCount()); + stream << quint32(audioFormat.sampleRate()); + stream << quint32(audioFormat.sampleRate() * audioFormat.channelCount() * audioFormat.sampleSize() / 8); // bytes per second + stream << quint16(audioFormat.channelCount() * audioFormat.sampleSize() / 8); // block align + stream << quint16(audioFormat.sampleSize()); // bits Per Sample + // Init data chunck + stream.writeRawData("data", 4); + stream << quint32(0); +} diff --git a/libraries/audio-client/src/AudioFileWav.h b/libraries/audio-client/src/AudioFileWav.h new file mode 100644 index 0000000000..7e9c83a23b --- /dev/null +++ b/libraries/audio-client/src/AudioFileWav.h @@ -0,0 +1,34 @@ +// +// AudioWavFile.h +// libraries/audio-client/src +// +// Created by Luis Cuenca on 12/1/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AudioFileWav_h +#define hifi_AudioFileWav_h + +#include +#include +#include +#include +#include + +class AudioFileWav : public QObject { + Q_OBJECT +public: + AudioFileWav() {} + bool create(const QAudioFormat& audioFormat, const QString& filepath); + bool addRawAudioChunk(char* chunk, int size); + void close(); + +private: + void addHeader(const QAudioFormat& audioFormat); + QFile _file; +}; + +#endif // hifi_AudioFileWav_h \ No newline at end of file diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 055a3cf24a..bb7f141cd9 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -162,6 +162,7 @@ AABox Avatar::getBounds() const { } void Avatar::animateScaleChanges(float deltaTime) { + if (_isAnimatingScale) { float currentScale = getModelScale(); float desiredScale = getDomainLimitedScale(); @@ -172,7 +173,7 @@ void Avatar::animateScaleChanges(float deltaTime) { float animatedScale = (1.0f - blendFactor) * currentScale + blendFactor * desiredScale; // snap to the end when we get close enough - const float MIN_RELATIVE_ERROR = 0.03f; + const float MIN_RELATIVE_ERROR = 0.001f; float relativeError = fabsf(desiredScale - currentScale) / desiredScale; if (relativeError < MIN_RELATIVE_ERROR) { animatedScale = desiredScale; @@ -698,6 +699,7 @@ void Avatar::fixupModelsInScene(const render::ScenePointer& scene) { _skeletonModel->removeFromScene(scene, transaction); _skeletonModel->addToScene(scene, transaction); canTryFade = true; + _isAnimatingScale = true; } for (auto attachmentModel : _attachmentModels) { if (attachmentModel->isRenderable() && attachmentModel->needsFixupInScene()) { @@ -1195,6 +1197,8 @@ void Avatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { void Avatar::setModelURLFinished(bool success) { invalidateJointIndicesCache(); + _isAnimatingScale = true; + if (!success && _skeletonModelURL != AvatarData::defaultFullAvatarModelUrl()) { const int MAX_SKELETON_DOWNLOAD_ATTEMPTS = 4; // NOTE: we don't want to be as generous as ResourceCache is, we only want 4 attempts if (_skeletonModel->getResourceDownloadAttemptsRemaining() <= 0 || @@ -1213,6 +1217,15 @@ void Avatar::setModelURLFinished(bool success) { } } +// rig is ready +void Avatar::rigReady() { + buildUnscaledEyeHeightCache(); +} + +// rig has been reset. +void Avatar::rigReset() { + clearUnscaledEyeHeightCache(); +} // create new model, can return an instance of a SoftAttachmentModel rather then Model static std::shared_ptr allocateAttachmentModel(bool isSoft, const Rig& rigOverride, bool isCauterized) { @@ -1580,53 +1593,91 @@ void Avatar::ensureInScene(AvatarSharedPointer self, const render::ScenePointer& } } +// thread-safe float Avatar::getEyeHeight() const { + return getModelScale() * getUnscaledEyeHeight(); +} - if (QThread::currentThread() != thread()) { - float result = DEFAULT_AVATAR_EYE_HEIGHT; - BLOCKING_INVOKE_METHOD(const_cast(this), "getEyeHeight", Q_RETURN_ARG(float, result)); - return result; +// thread-safe +float Avatar::getUnscaledEyeHeight() const { + return _unscaledEyeHeightCache.get(); +} + +void Avatar::buildUnscaledEyeHeightCache() { + float skeletonHeight = getUnscaledEyeHeightFromSkeleton(); + + // Sanity check by looking at the model extents. + Extents meshExtents = _skeletonModel->getUnscaledMeshExtents(); + float meshHeight = meshExtents.size().y; + + // if we determine the mesh is much larger then the skeleton, then we use the mesh height instead. + // This helps prevent absurdly large avatars from exceeding the domain height limit. + const float MESH_SLOP_RATIO = 1.5f; + if (meshHeight > skeletonHeight * MESH_SLOP_RATIO) { + _unscaledEyeHeightCache.set(meshHeight); + } else { + _unscaledEyeHeightCache.set(skeletonHeight); } +} + +void Avatar::clearUnscaledEyeHeightCache() { + _unscaledEyeHeightCache.set(DEFAULT_AVATAR_EYE_HEIGHT); +} + +float Avatar::getUnscaledEyeHeightFromSkeleton() const { // TODO: if performance becomes a concern we can cache this value rather then computing it everytime. - // Makes assumption that the y = 0 plane in geometry is the ground plane. - // We also make that assumption in Rig::computeAvatarBoundingCapsule() - float avatarScale = getModelScale(); + if (_skeletonModel) { auto& rig = _skeletonModel->getRig(); + + // Normally the model offset transform will contain the avatar scale factor, we explicitly remove it here. + AnimPose modelOffsetWithoutAvatarScale(glm::vec3(1.0f), rig.getModelOffsetPose().rot(), rig.getModelOffsetPose().trans()); + AnimPose geomToRigWithoutAvatarScale = modelOffsetWithoutAvatarScale * rig.getGeometryOffsetPose(); + + // This factor can be used to scale distances in the geometry frame into the unscaled rig frame. + // Typically it will be the unit conversion from cm to m. + float scaleFactor = geomToRigWithoutAvatarScale.scale().x; // in practice this always a uniform scale factor. + int headTopJoint = rig.indexOfJoint("HeadTop_End"); int headJoint = rig.indexOfJoint("Head"); int eyeJoint = rig.indexOfJoint("LeftEye") != -1 ? rig.indexOfJoint("LeftEye") : rig.indexOfJoint("RightEye"); int toeJoint = rig.indexOfJoint("LeftToeBase") != -1 ? rig.indexOfJoint("LeftToeBase") : rig.indexOfJoint("RightToeBase"); + + // Makes assumption that the y = 0 plane in geometry is the ground plane. + // We also make that assumption in Rig::computeAvatarBoundingCapsule() + const float GROUND_Y = 0.0f; + + // Values from the skeleton are in the geometry coordinate frame. + auto skeleton = rig.getAnimSkeleton(); if (eyeJoint >= 0 && toeJoint >= 0) { - // measure from eyes to toes. - float eyeHeight = rig.getAbsoluteDefaultPose(eyeJoint).trans().y - rig.getAbsoluteDefaultPose(toeJoint).trans().y; - return eyeHeight; + // Measure from eyes to toes. + float eyeHeight = skeleton->getAbsoluteDefaultPose(eyeJoint).trans().y - skeleton->getAbsoluteDefaultPose(toeJoint).trans().y; + return scaleFactor * eyeHeight; } else if (eyeJoint >= 0) { - // measure eyes to y = 0 plane. - float groundHeight = transformPoint(rig.getGeometryToRigTransform(), glm::vec3(0.0f)).y; - float eyeHeight = rig.getAbsoluteDefaultPose(eyeJoint).trans().y - groundHeight; - return eyeHeight; + // Measure Eye joint to y = 0 plane. + float eyeHeight = skeleton->getAbsoluteDefaultPose(eyeJoint).trans().y - GROUND_Y; + return scaleFactor * eyeHeight; } else if (headTopJoint >= 0 && toeJoint >= 0) { - // measure toe to top of head. Note: default poses already include avatar scale factor + // Measure from ToeBase joint to HeadTop_End joint, then remove forehead distance. const float ratio = DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD / DEFAULT_AVATAR_HEIGHT; - float height = rig.getAbsoluteDefaultPose(headTopJoint).trans().y - rig.getAbsoluteDefaultPose(toeJoint).trans().y; - return height - height * ratio; + float height = skeleton->getAbsoluteDefaultPose(headTopJoint).trans().y - skeleton->getAbsoluteDefaultPose(toeJoint).trans().y; + return scaleFactor * (height - height * ratio); } else if (headTopJoint >= 0) { + // Measure from HeadTop_End joint to the ground, then remove forehead distance. const float ratio = DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD / DEFAULT_AVATAR_HEIGHT; - float groundHeight = transformPoint(rig.getGeometryToRigTransform(), glm::vec3(0.0f)).y; - float headHeight = rig.getAbsoluteDefaultPose(headTopJoint).trans().y - groundHeight; - return headHeight - headHeight * ratio; + float headHeight = skeleton->getAbsoluteDefaultPose(headTopJoint).trans().y - GROUND_Y; + return scaleFactor * (headHeight - headHeight * ratio); } else if (headJoint >= 0) { - float groundHeight = transformPoint(rig.getGeometryToRigTransform(), glm::vec3(0.0f)).y; + // Measure Head joint to the ground, then add in distance from neck to eye. const float DEFAULT_AVATAR_NECK_TO_EYE = DEFAULT_AVATAR_NECK_TO_TOP_OF_HEAD - DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD; const float ratio = DEFAULT_AVATAR_NECK_TO_EYE / DEFAULT_AVATAR_NECK_HEIGHT; - float neckHeight = rig.getAbsoluteDefaultPose(headJoint).trans().y - groundHeight; - return neckHeight + neckHeight * ratio; + float neckHeight = skeleton->getAbsoluteDefaultPose(headJoint).trans().y - GROUND_Y; + return scaleFactor * (neckHeight + neckHeight * ratio); } else { - return avatarScale * DEFAULT_AVATAR_EYE_HEIGHT; + return DEFAULT_AVATAR_EYE_HEIGHT; } } else { - return avatarScale * DEFAULT_AVATAR_EYE_HEIGHT; + return DEFAULT_AVATAR_EYE_HEIGHT; } } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index a723a8e1b0..c75b54fdc4 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -255,12 +255,16 @@ public: bool isFading() const { return _isFading; } void updateFadingStatus(render::ScenePointer scene); - /**jsdoc - * Provides read only access to the current eye height of the avatar. - * @function Avatar.getEyeHeight - * @returns {number} eye height of avatar in meters - */ - Q_INVOKABLE float getEyeHeight() const; + Q_INVOKABLE virtual float getEyeHeight() const override; + + // returns eye height of avatar in meters, ignoring avatar scale. + // if _targetScale is 1 then this will be identical to getEyeHeight. + virtual float getUnscaledEyeHeight() const override; + + // returns true, if an acurate eye height estimage can be obtained by inspecting the avatar model skeleton and geometry, + // not all subclasses of AvatarData have access to this data. + virtual bool canMeasureEyeHeight() const override { return true; } + virtual float getModelScale() const { return _modelScale; } virtual void setModelScale(float scale) { _modelScale = scale; } @@ -276,9 +280,17 @@ public slots: glm::vec3 getRightPalmPosition() const; glm::quat getRightPalmRotation() const; + // hooked up to Model::setURLFinished signal void setModelURLFinished(bool success); + // hooked up to Model::rigReady & rigReset signals + void rigReady(); + void rigReset(); + protected: + float getUnscaledEyeHeightFromSkeleton() const; + void buildUnscaledEyeHeightCache(); + void clearUnscaledEyeHeightCache(); virtual const QString& getSessionDisplayNameForTransport() const override { return _empty; } // Save a tiny bit of bandwidth. Mixer won't look at what we send. QString _empty{}; virtual void maybeUpdateSessionDisplayNameFromTransport(const QString& sessionDisplayName) override { _sessionDisplayName = sessionDisplayName; } // don't use no-op setter! @@ -349,7 +361,7 @@ protected: RateCounter<> _skeletonModelSimulationRate; RateCounter<> _jointDataSimulationRate; -private: +protected: class AvatarEntityDataHash { public: AvatarEntityDataHash(uint32_t h) : hash(h) {}; @@ -379,6 +391,8 @@ private: float _displayNameTargetAlpha { 1.0f }; float _displayNameAlpha { 1.0f }; + + ThreadSafeValueCache _unscaledEyeHeightCache { DEFAULT_AVATAR_EYE_HEIGHT }; }; #endif // hifi_Avatar_h diff --git a/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.cpp index e870e2de12..4382216575 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.cpp @@ -13,4 +13,6 @@ OtherAvatar::OtherAvatar(QThread* thread) : Avatar(thread) { _headData = new Head(this); _skeletonModel = std::make_shared(this, nullptr); connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished); + connect(_skeletonModel.get(), &Model::rigReady, this, &Avatar::rigReady); + connect(_skeletonModel.get(), &Model::rigReset, this, &Avatar::rigReset); } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index cb43aeee5d..f2053e29d7 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -117,6 +117,55 @@ void AvatarData::setTargetScale(float targetScale) { } } +float AvatarData::getDomainLimitedScale() const { + if (canMeasureEyeHeight()) { + const float minScale = getDomainMinScale(); + const float maxScale = getDomainMaxScale(); + return glm::clamp(_targetScale, minScale, maxScale); + } else { + // We can't make a good estimate. + return _targetScale; + } +} + +void AvatarData::setDomainMinimumHeight(float domainMinimumHeight) { + _domainMinimumHeight = glm::clamp(domainMinimumHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); +} + +void AvatarData::setDomainMaximumHeight(float domainMaximumHeight) { + _domainMaximumHeight = glm::clamp(domainMaximumHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); +} + +float AvatarData::getDomainMinScale() const { + float unscaledHeight = getUnscaledHeight(); + const float EPSILON = 1.0e-4f; + if (unscaledHeight <= EPSILON) { + unscaledHeight = DEFAULT_AVATAR_HEIGHT; + } + return _domainMinimumHeight / unscaledHeight; +} + +float AvatarData::getDomainMaxScale() const { + float unscaledHeight = getUnscaledHeight(); + const float EPSILON = 1.0e-4f; + if (unscaledHeight <= EPSILON) { + unscaledHeight = DEFAULT_AVATAR_HEIGHT; + } + return _domainMaximumHeight / unscaledHeight; +} + +float AvatarData::getUnscaledHeight() const { + const float eyeHeight = getUnscaledEyeHeight(); + const float ratio = eyeHeight / DEFAULT_AVATAR_HEIGHT; + return eyeHeight + ratio * DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD; +} + +float AvatarData::getHeight() const { + const float eyeHeight = getEyeHeight(); + const float ratio = eyeHeight / DEFAULT_AVATAR_HEIGHT; + return eyeHeight + ratio * DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD; +} + glm::vec3 AvatarData::getHandPosition() const { return getWorldOrientation() * _handPosition + getWorldPosition(); } @@ -2387,63 +2436,10 @@ void RayToAvatarIntersectionResultFromScriptValue(const QScriptValue& object, Ra const float AvatarData::OUT_OF_VIEW_PENALTY = -10.0f; -float AvatarData::_avatarSortCoefficientSize { 0.5f }; +float AvatarData::_avatarSortCoefficientSize { 1.0f }; float AvatarData::_avatarSortCoefficientCenter { 0.25 }; float AvatarData::_avatarSortCoefficientAge { 1.0f }; -void AvatarData::sortAvatars( - QList avatarList, - const ViewFrustum& cameraView, - std::priority_queue& sortedAvatarsOut, - std::function getLastUpdated, - std::function getBoundingRadius, - std::function shouldIgnore) { - - PROFILE_RANGE(simulation, "sort"); - uint64_t now = usecTimestampNow(); - - glm::vec3 frustumCenter = cameraView.getPosition(); - const glm::vec3& forward = cameraView.getDirection(); - for (int32_t i = 0; i < avatarList.size(); ++i) { - const auto& avatar = avatarList.at(i); - - if (shouldIgnore(avatar)) { - continue; - } - - // priority = weighted linear combination of: - // (a) apparentSize - // (b) proximity to center of view - // (c) time since last update - glm::vec3 avatarPosition = avatar->getWorldPosition(); - glm::vec3 offset = avatarPosition - frustumCenter; - float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero - - // FIXME - AvatarData has something equivolent to this - float radius = getBoundingRadius(avatar); - - float apparentSize = 2.0f * radius / distance; - float cosineAngle = glm::dot(offset, forward) / distance; - float age = (float)(now - getLastUpdated(avatar)) / (float)(USECS_PER_SECOND); - - // NOTE: we are adding values of different units to get a single measure of "priority". - // Thus we multiply each component by a conversion "weight" that scales its units relative to the others. - // These weights are pure magic tuning and should be hard coded in the relation below, - // but are currently exposed for anyone who would like to explore fine tuning: - float priority = _avatarSortCoefficientSize * apparentSize - + _avatarSortCoefficientCenter * cosineAngle - + _avatarSortCoefficientAge * age; - - // decrement priority of avatars outside keyhole - if (distance > cameraView.getCenterRadius()) { - if (!cameraView.sphereIntersectsFrustum(avatarPosition, radius)) { - priority += OUT_OF_VIEW_PENALTY; - } - } - sortedAvatarsOut.push(AvatarPriority(avatar, priority)); - } -} - QScriptValue AvatarEntityMapToScriptValue(QScriptEngine* engine, const AvatarEntityMap& value) { QScriptValue obj = engine->newObject(); for (auto entityID : value.keys()) { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 2f3154ad08..d7dd2837cb 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -35,6 +35,7 @@ #include #include +#include #include #include #include @@ -257,9 +258,6 @@ namespace AvatarDataPacket { size_t maxJointDataSize(size_t numJoints); } -static const float MAX_AVATAR_SCALE = 1000.0f; -static const float MIN_AVATAR_SCALE = .005f; - const float MAX_AUDIO_LOUDNESS = 1000.0f; // close enough for mouth animation const int AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS = 1000; @@ -484,12 +482,52 @@ public: // Scale virtual void setTargetScale(float targetScale); - float getDomainLimitedScale() const { return glm::clamp(_targetScale, _domainMinimumScale, _domainMaximumScale); } + float getDomainLimitedScale() const; - void setDomainMinimumScale(float domainMinimumScale) - { _domainMinimumScale = glm::clamp(domainMinimumScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE); _scaleChanged = usecTimestampNow(); } - void setDomainMaximumScale(float domainMaximumScale) - { _domainMaximumScale = glm::clamp(domainMaximumScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE); _scaleChanged = usecTimestampNow(); } + /**jsdoc + * returns the minimum scale allowed for this avatar in the current domain. + * This value can change as the user changes avatars or when changing domains. + * @function AvatarData.getDomainMinScale + * @returns {number} minimum scale allowed for this avatar in the current domain. + */ + Q_INVOKABLE float getDomainMinScale() const; + + /**jsdoc + * returns the maximum scale allowed for this avatar in the current domain. + * This value can change as the user changes avatars or when changing domains. + * @function AvatarData.getDomainMaxScale + * @returns {number} maximum scale allowed for this avatar in the current domain. + */ + Q_INVOKABLE float getDomainMaxScale() const; + + // returns eye height of avatar in meters, ignoreing avatar scale. + // if _targetScale is 1 then this will be identical to getEyeHeight; + virtual float getUnscaledEyeHeight() const { return DEFAULT_AVATAR_EYE_HEIGHT; } + + // returns true, if an acurate eye height estimage can be obtained by inspecting the avatar model skeleton and geometry, + // not all subclasses of AvatarData have access to this data. + virtual bool canMeasureEyeHeight() const { return false; } + + /**jsdoc + * Provides read only access to the current eye height of the avatar. + * This height is only an estimate and might be incorrect for avatars that are missing standard joints. + * @function AvatarData.getEyeHeight + * @returns {number} eye height of avatar in meters + */ + Q_INVOKABLE virtual float getEyeHeight() const { return _targetScale * getUnscaledEyeHeight(); } + + /**jsdoc + * Provides read only access to the current height of the avatar. + * This height is only an estimate and might be incorrect for avatars that are missing standard joints. + * @function AvatarData.getHeight + * @returns {number} height of avatar in meters + */ + Q_INVOKABLE virtual float getHeight() const; + + float getUnscaledHeight() const; + + void setDomainMinimumHeight(float domainMinimumHeight); + void setDomainMaximumHeight(float domainMaximumHeight); // Hand State Q_INVOKABLE void setHandState(char s) { _handState = s; } @@ -629,14 +667,6 @@ public: static const float OUT_OF_VIEW_PENALTY; - static void sortAvatars( - QList avatarList, - const ViewFrustum& cameraView, - std::priority_queue& sortedAvatarsOut, - std::function getLastUpdated, - std::function getBoundingRadius, - std::function shouldIgnore); - // TODO: remove this HACK once we settle on optimal sort coefficients // These coefficients exposed for fine tuning the sort priority for transfering new _jointData to the render pipeline. static float _avatarSortCoefficientSize; @@ -706,8 +736,8 @@ protected: // Body scale float _targetScale; - float _domainMinimumScale { MIN_AVATAR_SCALE }; - float _domainMaximumScale { MAX_AVATAR_SCALE }; + float _domainMinimumHeight { MIN_AVATAR_HEIGHT }; + float _domainMaximumHeight { MAX_AVATAR_HEIGHT }; // Hand state (are we grabbing something or not) char _handState; diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index ee24d6a4c2..a765e66bbc 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -607,7 +607,7 @@ void FBXBaker::rewriteAndBakeSceneTextures() { return; } - if (!TextureBaker::getSupportedFormats().contains(textureFileInfo.suffix())) { + if (!image::getSupportedFormats().contains(textureFileInfo.suffix())) { // this is a texture format we don't bake, skip it handleWarning(fbxTextureFileName + " is not a bakeable texture format"); continue; diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index 1a320efabc..ff006e7185 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -61,14 +61,6 @@ void TextureBaker::abort() { _abortProcessing.store(true); } -const QStringList TextureBaker::getSupportedFormats() { - auto formats = QImageReader::supportedImageFormats(); - QStringList stringFormats; - std::transform(formats.begin(), formats.end(), std::back_inserter(stringFormats), - [](QByteArray& format) -> QString { return format; }); - return stringFormats; -} - void TextureBaker::loadTexture() { // check if the texture is local or first needs to be downloaded if (_textureURL.isLocalFile()) { diff --git a/libraries/baking/src/TextureBaker.h b/libraries/baking/src/TextureBaker.h index b2e86b2b5b..90ecfe52f7 100644 --- a/libraries/baking/src/TextureBaker.h +++ b/libraries/baking/src/TextureBaker.h @@ -31,8 +31,6 @@ public: const QDir& outputDirectory, const QString& bakedFilename = QString(), const QByteArray& textureContent = QByteArray()); - static const QStringList getSupportedFormats(); - const QByteArray& getOriginalTexture() const { return _originalTexture; } QUrl getTextureURL() const { return _textureURL; } diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index 7b639e8308..f3f81c0b2e 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -437,9 +437,11 @@ glm::mat4 CompositorHelper::getReticleTransform(const glm::mat4& eyePose, const } else { d = glm::normalize(overlaySurfacePoint); } - reticlePosition = headPosition + (d * getReticleDepth()); + // Our sensor to world matrix always has uniform scale + float sensorSpaceReticleDepth = getReticleDepth() / extractScale(_sensorToWorldMatrix).x; + reticlePosition = headPosition + (d * sensorSpaceReticleDepth); quat reticleOrientation = cancelOutRoll(glm::quat_cast(_currentDisplayPlugin->getHeadPose())); - vec3 reticleScale = vec3(Cursor::Manager::instance().getScale() * reticleSize * getReticleDepth()); + vec3 reticleScale = vec3(Cursor::Manager::instance().getScale() * reticleSize * sensorSpaceReticleDepth); return glm::inverse(eyePose) * createMatFromScaleQuatAndPos(reticleScale, reticleOrientation, reticlePosition); } else { static const float CURSOR_PIXEL_SIZE = 32.0f; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 06b81ff428..c992eb5dc4 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1149,7 +1149,7 @@ bool ModelEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPoin if (model && model->isLoaded()) { if (!entity->_dimensionsInitialized || entity->_needsInitialSimulation) { return true; - } + } // Check to see if we need to update the model bounds if (entity->needsUpdateModelBounds()) { diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index c8e7ce6c11..48370b02fd 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1605,6 +1605,48 @@ void EntityItem::setParentID(const QUuid& value) { if (tree && !oldParentID.isNull()) { tree->removeFromChildrenOfAvatars(getThisPointer()); } + + uint32_t oldParentNoBootstrapping = 0; + uint32_t newParentNoBootstrapping = 0; + if (!value.isNull() && tree) { + EntityItemPointer entity = tree->findEntityByEntityItemID(value); + if (entity) { + newParentNoBootstrapping = entity->getDirtyFlags() & Simulation::NO_BOOTSTRAPPING; + } + } + + if (!oldParentID.isNull() && tree) { + EntityItemPointer entity = tree->findEntityByEntityItemID(oldParentID); + if (entity) { + oldParentNoBootstrapping = entity->getDirtyFlags() & Simulation::NO_BOOTSTRAPPING; + } + } + + if (!value.isNull() && (value == Physics::getSessionUUID() || value == AVATAR_SELF_ID)) { + newParentNoBootstrapping |= Simulation::NO_BOOTSTRAPPING; + } + + if ((bool)(oldParentNoBootstrapping ^ newParentNoBootstrapping)) { + if ((bool)(newParentNoBootstrapping & Simulation::NO_BOOTSTRAPPING)) { + markDirtyFlags(Simulation::NO_BOOTSTRAPPING); + forEachDescendant([&](SpatiallyNestablePointer object) { + if (object->getNestableType() == NestableType::Entity) { + EntityItemPointer entity = std::static_pointer_cast(object); + entity->markDirtyFlags(Simulation::DIRTY_COLLISION_GROUP | Simulation::NO_BOOTSTRAPPING); + } + }); + } else { + clearDirtyFlags(Simulation::NO_BOOTSTRAPPING); + forEachDescendant([&](SpatiallyNestablePointer object) { + if (object->getNestableType() == NestableType::Entity) { + EntityItemPointer entity = std::static_pointer_cast(object); + entity->markDirtyFlags(Simulation::DIRTY_COLLISION_GROUP); + entity->clearDirtyFlags(Simulation::NO_BOOTSTRAPPING); + } + }); + } + } + SpatiallyNestable::setParentID(value); // children are forced to be kinematic // may need to not collide with own avatar @@ -1834,39 +1876,8 @@ void EntityItem::computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask } } - if (userMask & USER_COLLISION_GROUP_MY_AVATAR) { - bool iAmHoldingThis = false; - // if this entity is a descendant of MyAvatar, don't collide with MyAvatar. This avoids the - // "bootstrapping" problem where you can shoot yourself across the room by grabbing something - // and holding it against your own avatar. - if (isChildOfMyAvatar()) { - iAmHoldingThis = true; - } - // also, don't bootstrap our own avatar with a hold action - QList holdActions = getActionsOfType(DYNAMIC_TYPE_HOLD); - QList::const_iterator i = holdActions.begin(); - while (i != holdActions.end()) { - EntityDynamicPointer action = *i; - if (action->isMine()) { - iAmHoldingThis = true; - break; - } - i++; - } - QList farGrabActions = getActionsOfType(DYNAMIC_TYPE_FAR_GRAB); - i = farGrabActions.begin(); - while (i != farGrabActions.end()) { - EntityDynamicPointer action = *i; - if (action->isMine()) { - iAmHoldingThis = true; - break; - } - i++; - } - - if (iAmHoldingThis) { - userMask &= ~USER_COLLISION_GROUP_MY_AVATAR; - } + if ((bool)(_dirtyFlags & Simulation::NO_BOOTSTRAPPING)) { + userMask &= ~USER_COLLISION_GROUP_MY_AVATAR; } mask = Physics::getDefaultCollisionMask(group) & (int16_t)(userMask); } @@ -1961,7 +1972,20 @@ bool EntityItem::addActionInternal(EntitySimulationPointer simulation, EntityDyn if (success) { _allActionsDataCache = newDataCache; _dirtyFlags |= Simulation::DIRTY_PHYSICS_ACTIVATION; - _dirtyFlags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar + + auto actionType = action->getType(); + if (actionType == DYNAMIC_TYPE_HOLD || actionType == DYNAMIC_TYPE_FAR_GRAB) { + if (!(bool)(_dirtyFlags & Simulation::NO_BOOTSTRAPPING)) { + _dirtyFlags |= Simulation::NO_BOOTSTRAPPING; + _dirtyFlags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar + forEachDescendant([&](SpatiallyNestablePointer child) { + if (child->getNestableType() == NestableType::Entity) { + EntityItemPointer entity = std::static_pointer_cast(child); + entity->markDirtyFlags(Simulation::NO_BOOTSTRAPPING | Simulation::DIRTY_COLLISION_GROUP); + } + }); + } + } } else { qCDebug(entities) << "EntityItem::addActionInternal -- serializeActions failed"; } @@ -2002,6 +2026,29 @@ bool EntityItem::removeAction(EntitySimulationPointer simulation, const QUuid& a return success; } +bool EntityItem::stillHasGrabActions() const { + QList holdActions = getActionsOfType(DYNAMIC_TYPE_HOLD); + QList::const_iterator i = holdActions.begin(); + while (i != holdActions.end()) { + EntityDynamicPointer action = *i; + if (action->isMine()) { + return true; + } + i++; + } + QList farGrabActions = getActionsOfType(DYNAMIC_TYPE_FAR_GRAB); + i = farGrabActions.begin(); + while (i != farGrabActions.end()) { + EntityDynamicPointer action = *i; + if (action->isMine()) { + return true; + } + i++; + } + + return false; +} + bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPointer simulation) { _previouslyDeletedActions.insert(actionID, usecTimestampNow()); if (_objectActions.contains(actionID)) { @@ -2015,7 +2062,6 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPoi action->setOwnerEntity(nullptr); action->setIsMine(false); - _objectActions.remove(actionID); if (simulation) { action->removeFromSimulation(simulation); @@ -2024,7 +2070,23 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPoi bool success = true; serializeActions(success, _allActionsDataCache); _dirtyFlags |= Simulation::DIRTY_PHYSICS_ACTIVATION; - _dirtyFlags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar + auto removedActionType = action->getType(); + if ((removedActionType == DYNAMIC_TYPE_HOLD || removedActionType == DYNAMIC_TYPE_FAR_GRAB) && !stillHasGrabActions()) { + _dirtyFlags &= ~Simulation::NO_BOOTSTRAPPING; + _dirtyFlags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar + forEachDescendant([&](SpatiallyNestablePointer child) { + if (child->getNestableType() == NestableType::Entity) { + EntityItemPointer entity = std::static_pointer_cast(child); + entity->markDirtyFlags(Simulation::DIRTY_COLLISION_GROUP); + entity->clearDirtyFlags(Simulation::NO_BOOTSTRAPPING); + } + }); + } else { + // NO-OP: we assume NO_BOOTSTRAPPING bits and collision group are correct + // because they should have been set correctly when the action was added + // and/or when children were linked + } + _objectActions.remove(actionID); setDynamicDataNeedsTransmit(true); return success; } diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 882b8e6812..4c7f37bd6a 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -470,6 +470,7 @@ protected: void setSimulated(bool simulated) { _simulated = simulated; } const QByteArray getDynamicDataInternal() const; + bool stillHasGrabActions() const; void setDynamicDataInternal(QByteArray dynamicData); virtual void dimensionsChanged() override; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 5ab4bdee01..9f7ba1cc80 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -2486,7 +2486,7 @@ QByteArray EntityItemProperties::getStaticCertificateJSON() const { ADD_STRING_PROPERTY(collisionSoundURL, CollisionSoundURL); ADD_STRING_PROPERTY(compoundShapeURL, CompoundShapeURL); ADD_INT_PROPERTY(editionNumber, EditionNumber); - ADD_INT_PROPERTY(instanceNumber, EntityInstanceNumber); + ADD_INT_PROPERTY(entityInstanceNumber, EntityInstanceNumber); ADD_STRING_PROPERTY(itemArtist, ItemArtist); ADD_STRING_PROPERTY(itemCategories, ItemCategories); ADD_STRING_PROPERTY(itemDescription, ItemDescription); diff --git a/libraries/entities/src/SimulationFlags.h b/libraries/entities/src/SimulationFlags.h index e2b2224b4a..aaa92000e7 100644 --- a/libraries/entities/src/SimulationFlags.h +++ b/libraries/entities/src/SimulationFlags.h @@ -27,6 +27,7 @@ namespace Simulation { const uint32_t DIRTY_PHYSICS_ACTIVATION = 0x0800; // should activate object in physics engine const uint32_t DIRTY_SIMULATOR_ID = 0x1000; // the simulatorID has changed const uint32_t DIRTY_SIMULATION_OWNERSHIP_PRIORITY = 0x2000; // our own bid priority has changed + const uint32_t NO_BOOTSTRAPPING = 0x4000; const uint32_t DIRTY_TRANSFORM = DIRTY_POSITION | DIRTY_ROTATION; const uint32_t DIRTY_VELOCITIES = DIRTY_LINEAR_VELOCITY | DIRTY_ANGULAR_VELOCITY; diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index 9f584c844f..de91901739 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -75,6 +75,14 @@ glm::uvec2 rectifyToSparseSize(const glm::uvec2& size) { namespace image { +const QStringList getSupportedFormats() { + auto formats = QImageReader::supportedImageFormats(); + QStringList stringFormats; + std::transform(formats.begin(), formats.end(), std::back_inserter(stringFormats), + [](QByteArray& format) -> QString { return format; }); + return stringFormats; +} + QImage::Format QIMAGE_HDR_FORMAT = QImage::Format_RGB30; TextureUsage::TextureLoader TextureUsage::getTextureLoaderForType(Type type, const QVariantMap& options) { diff --git a/libraries/image/src/image/Image.h b/libraries/image/src/image/Image.h index 856dc009cf..ef175935d0 100644 --- a/libraries/image/src/image/Image.h +++ b/libraries/image/src/image/Image.h @@ -80,6 +80,8 @@ gpu::TexturePointer processCubeTextureColorFromImage(const QImage& srcImage, con } // namespace TextureUsage +const QStringList getSupportedFormats(); + bool isColorTexturesCompressionEnabled(); bool isNormalTexturesCompressionEnabled(); bool isGrayscaleTexturesCompressionEnabled(); diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 4184351c2d..d25c5225d6 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -1006,14 +1006,11 @@ NetworkTexturePointer TextureCache::getResourceTexture(QUrl resourceTextureUrl) if (!_spectatorCameraNetworkTexture) { _spectatorCameraNetworkTexture.reset(new NetworkTexture(resourceTextureUrl)); } - if (_spectatorCameraFramebuffer) { - texture = _spectatorCameraFramebuffer->getRenderBuffer(0); - if (texture) { - texture->setSource(SPECTATOR_CAMERA_FRAME_URL.toString().toStdString()); - _spectatorCameraNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); - return _spectatorCameraNetworkTexture; - } + if (!_spectatorCameraFramebuffer) { + getSpectatorCameraFramebuffer(); // initialize frame buffer } + updateSpectatorCameraNetworkTexture(); + return _spectatorCameraNetworkTexture; } // FIXME: Generalize this, DRY up this code if (resourceTextureUrl == HMD_PREVIEW_FRAME_URL) { @@ -1052,7 +1049,18 @@ const gpu::FramebufferPointer& TextureCache::getSpectatorCameraFramebuffer(int w // If we aren't taking a screenshot, we might need to resize or create the camera buffer if (!_spectatorCameraFramebuffer || _spectatorCameraFramebuffer->getWidth() != width || _spectatorCameraFramebuffer->getHeight() != height) { _spectatorCameraFramebuffer.reset(gpu::Framebuffer::create("spectatorCamera", gpu::Element::COLOR_SRGBA_32, width, height)); + updateSpectatorCameraNetworkTexture(); emit spectatorCameraFramebufferReset(); } return _spectatorCameraFramebuffer; } + +void TextureCache::updateSpectatorCameraNetworkTexture() { + if (_spectatorCameraFramebuffer && _spectatorCameraNetworkTexture) { + gpu::TexturePointer texture = _spectatorCameraFramebuffer->getRenderBuffer(0); + if (texture) { + texture->setSource(SPECTATOR_CAMERA_FRAME_URL.toString().toStdString()); + _spectatorCameraNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); + } + } +} diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 1102694f86..5a96fcf5e6 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -171,6 +171,7 @@ public: const gpu::FramebufferPointer& getHmdPreviewFramebuffer(int width, int height); const gpu::FramebufferPointer& getSpectatorCameraFramebuffer(); const gpu::FramebufferPointer& getSpectatorCameraFramebuffer(int width, int height); + void updateSpectatorCameraNetworkTexture(); static const int DEFAULT_SPECTATOR_CAM_WIDTH { 2048 }; static const int DEFAULT_SPECTATOR_CAM_HEIGHT { 1024 }; diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index 61f2071c5f..0965c9834f 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -88,3 +88,56 @@ void UserActivityLoggerScriptingInterface::doLogAction(QString action, QJsonObje Q_ARG(QString, action), Q_ARG(QJsonObject, details)); } + +void UserActivityLoggerScriptingInterface::commercePurchaseSuccess(QString marketplaceID, int cost, bool firstPurchaseOfThisItem) { + QJsonObject payload; + payload["marketplaceID"] = marketplaceID; + payload["cost"] = cost; + payload["firstPurchaseOfThisItem"] = firstPurchaseOfThisItem; + doLogAction("commercePurchaseSuccess", payload); +} + +void UserActivityLoggerScriptingInterface::commercePurchaseFailure(QString marketplaceID, int cost, bool firstPurchaseOfThisItem, QString errorDetails) { + QJsonObject payload; + payload["marketplaceID"] = marketplaceID; + payload["cost"] = cost; + payload["firstPurchaseOfThisItem"] = firstPurchaseOfThisItem; + payload["errorDetails"] = errorDetails; + doLogAction("commercePurchaseFailure", payload); +} + +void UserActivityLoggerScriptingInterface::commerceEntityRezzed(QString marketplaceID, QString source, QString type) { + QJsonObject payload; + payload["marketplaceID"] = marketplaceID; + payload["source"] = source; + payload["type"] = type; + doLogAction("commerceEntityRezzed", payload); +} + +void UserActivityLoggerScriptingInterface::commerceWalletSetupStarted(int timestamp, QString setupAttemptID, int setupFlowVersion, QString referrer, QString currentDomain) { + QJsonObject payload; + payload["timestamp"] = timestamp; + payload["setupAttemptID"] = setupAttemptID; + payload["setupFlowVersion"] = setupFlowVersion; + payload["referrer"] = referrer; + payload["currentDomain"] = currentDomain; + doLogAction("commerceWalletSetupStarted", payload); +} + +void UserActivityLoggerScriptingInterface::commerceWalletSetupProgress(int timestamp, QString setupAttemptID, int secondsElapsed, int currentStepNumber, QString currentStepName) { + QJsonObject payload; + payload["timestamp"] = timestamp; + payload["setupAttemptID"] = setupAttemptID; + payload["secondsElapsed"] = secondsElapsed; + payload["currentStepNumber"] = currentStepNumber; + payload["currentStepName"] = currentStepName; + doLogAction("commerceWalletSetupProgress", payload); +} + +void UserActivityLoggerScriptingInterface::commerceWalletSetupFinished(int timestamp, QString setupAttemptID, int secondsToComplete) { + QJsonObject payload; + payload["timestamp"] = timestamp; + payload["setupAttemptID"] = setupAttemptID; + payload["secondsToComplete"] = secondsToComplete; + doLogAction("commerceWalletSetupFinished", payload); +} diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index 885f637a62..e71723f03c 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -33,6 +33,12 @@ public: Q_INVOKABLE void bubbleToggled(bool newValue); Q_INVOKABLE void bubbleActivated(); Q_INVOKABLE void logAction(QString action, QVariantMap details = QVariantMap{}); + Q_INVOKABLE void commercePurchaseSuccess(QString marketplaceID, int cost, bool firstPurchaseOfThisItem); + Q_INVOKABLE void commercePurchaseFailure(QString marketplaceID, int cost, bool firstPurchaseOfThisItem, QString errorDetails); + Q_INVOKABLE void commerceEntityRezzed(QString marketplaceID, QString source, QString type); + Q_INVOKABLE void commerceWalletSetupStarted(int timestamp, QString setupAttemptID, int setupFlowVersion, QString referrer, QString currentDomain); + Q_INVOKABLE void commerceWalletSetupProgress(int timestamp, QString setupAttemptID, int secondsElapsed, int currentStepNumber, QString currentStepName); + Q_INVOKABLE void commerceWalletSetupFinished(int timestamp, QString setupAttemptID, int secondsToComplete); private: void doLogAction(QString action, QJsonObject details = {}); }; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index fa5028e1bb..207ddf6bbb 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -74,6 +74,8 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(AudioVersion::HighDynamicRangeVolume); case PacketType::ICEPing: return static_cast(IcePingVersion::SendICEPeerID); + case PacketType::DomainSettings: + return 18; // replace min_avatar_scale and max_avatar_scale with min_avatar_height and max_avatar_height default: return 17; } diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 8ebce9f811..7e8b431ceb 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -700,7 +700,7 @@ uint32_t EntityMotionState::getIncomingDirtyFlags() { void EntityMotionState::clearIncomingDirtyFlags() { assert(entityTreeIsLocked()); if (_body && _entity) { - _entity->clearDirtyFlags(); + _entity->clearDirtyFlags(DIRTY_PHYSICS_FLAGS); } } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 428fcc7a54..c4bc435691 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -163,7 +163,7 @@ void Model::setScale(const glm::vec3& scale) { _scaledToFit = false; } -const float SCALE_CHANGE_EPSILON = 0.01f; +const float SCALE_CHANGE_EPSILON = 0.001f; void Model::setScaleInternal(const glm::vec3& scale) { if (glm::distance(_scale, scale) > SCALE_CHANGE_EPSILON) { @@ -286,6 +286,7 @@ void Model::reset() { if (isLoaded()) { const FBXGeometry& geometry = getFBXGeometry(); _rig.reset(geometry); + emit rigReset(); } } @@ -322,6 +323,7 @@ bool Model::updateGeometry() { _blendedVertexBuffers.push_back(buffer); } needFullUpdate = true; + emit rigReady(); } return needFullUpdate; } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index c537a928b3..7568a17342 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -204,6 +204,9 @@ public: /// Returns the extents of the model's mesh Extents getMeshExtents() const; + /// Returns the unscaled extents of the model's mesh + Extents getUnscaledMeshExtents() const; + void setTranslation(const glm::vec3& translation); void setRotation(const glm::quat& rotation); void setTransformNoUpdateRenderItems(const Transform& transform); // temporary HACK @@ -270,15 +273,14 @@ signals: void setURLFinished(bool success); void setCollisionModelURLFinished(bool success); void requestRenderUpdate(); + void rigReady(); + void rigReset(); protected: void setBlendshapeCoefficients(const QVector& coefficients) { _blendshapeCoefficients = coefficients; } const QVector& getBlendshapeCoefficients() const { return _blendshapeCoefficients; } - /// Returns the unscaled extents of the model's mesh - Extents getUnscaledMeshExtents() const; - /// Clear the joint states void clearJointState(int index); diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index 88e25b6d27..4b337a1046 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -32,23 +32,23 @@ void Transaction::removeItem(ItemID id) { } void Transaction::addTransitionToItem(ItemID id, Transition::Type transition, ItemID boundId) { - _addedTransitions.emplace_back(TransitionAdd{ id, transition, boundId }); + _addedTransitions.emplace_back(id, transition, boundId); } void Transaction::removeTransitionFromItem(ItemID id) { - _addedTransitions.emplace_back(TransitionAdd{ id, Transition::NONE, render::Item::INVALID_ITEM_ID }); + _addedTransitions.emplace_back(id, Transition::NONE, render::Item::INVALID_ITEM_ID); } void Transaction::reApplyTransitionToItem(ItemID id) { - _reAppliedTransitions.emplace_back(TransitionReApply{ id }); + _reAppliedTransitions.emplace_back(id); } void Transaction::queryTransitionOnItem(ItemID id, TransitionQueryFunc func) { - _queriedTransitions.emplace_back(TransitionQuery{ id, func }); + _queriedTransitions.emplace_back(id, func); } void Transaction::updateItem(ItemID id, const UpdateFunctorPointer& functor) { - _updatedItems.emplace_back(Update{ id, functor }); + _updatedItems.emplace_back(id, functor); } void Transaction::resetSelection(const Selection& selection) { @@ -56,28 +56,122 @@ void Transaction::resetSelection(const Selection& selection) { } void Transaction::resetSelectionHighlight(const std::string& selectionName, const HighlightStyle& style) { - _highlightResets.emplace_back(HighlightReset{ selectionName, style }); + _highlightResets.emplace_back(selectionName, style ); } void Transaction::removeHighlightFromSelection(const std::string& selectionName) { _highlightRemoves.emplace_back(selectionName); } -void Transaction::querySelectionHighlight(const std::string& selectionName, SelectionHighlightQueryFunc func) { - _highlightQueries.emplace_back(HighlightQuery{ selectionName, func }); +void Transaction::querySelectionHighlight(const std::string& selectionName, const SelectionHighlightQueryFunc& func) { + _highlightQueries.emplace_back(selectionName, func); +} + +void Transaction::reserve(const std::vector& transactionContainer) { + size_t resetItemsCount = 0; + size_t removedItemsCount = 0; + size_t updatedItemsCount = 0; + size_t resetSelectionsCount = 0; + size_t addedTransitionsCount = 0; + size_t queriedTransitionsCount = 0; + size_t reAppliedTransitionsCount = 0; + size_t highlightResetsCount = 0; + size_t highlightRemovesCount = 0; + size_t highlightQueriesCount = 0; + + for (const auto& transaction : transactionContainer) { + resetItemsCount += transaction._resetItems.size(); + removedItemsCount += transaction._removedItems.size(); + updatedItemsCount += transaction._updatedItems.size(); + resetSelectionsCount += transaction._resetSelections.size(); + addedTransitionsCount += transaction._addedTransitions.size(); + queriedTransitionsCount += transaction._queriedTransitions.size(); + reAppliedTransitionsCount += transaction._reAppliedTransitions.size(); + highlightResetsCount += transaction._highlightResets.size(); + highlightRemovesCount += transaction._highlightRemoves.size(); + highlightQueriesCount += transaction._highlightQueries.size(); + } + + _resetItems.reserve(resetItemsCount); + _removedItems.reserve(removedItemsCount); + _updatedItems.reserve(updatedItemsCount); + _resetSelections.reserve(resetSelectionsCount); + _addedTransitions.reserve(addedTransitionsCount); + _queriedTransitions.reserve(queriedTransitionsCount); + _reAppliedTransitions.reserve(reAppliedTransitionsCount); + _highlightResets.reserve(highlightResetsCount); + _highlightRemoves.reserve(highlightRemovesCount); + _highlightQueries.reserve(highlightQueriesCount); +} + +void Transaction::merge(const std::vector& transactionContainer) { + reserve(transactionContainer); + for (const auto& transaction : transactionContainer) { + merge(transaction); + } +} + + +void Transaction::merge(std::vector&& transactionContainer) { + reserve(transactionContainer); + auto begin = std::make_move_iterator(transactionContainer.begin()); + auto end = std::make_move_iterator(transactionContainer.end()); + for (auto itr = begin; itr != end; ++itr) { + merge(*itr); + } + transactionContainer.clear(); +} + + +template +void moveElements(T& target, T& source) { + target.insert(target.end(), std::make_move_iterator(source.begin()), std::make_move_iterator(source.end())); + source.clear(); +} + +template +void copyElements(T& target, const T& source) { + target.insert(target.end(), source.begin(), source.end()); +} + + +void Transaction::merge(Transaction&& transaction) { + moveElements(_resetItems, transaction._resetItems); + moveElements(_removedItems, transaction._removedItems); + moveElements(_updatedItems, transaction._updatedItems); + moveElements(_resetSelections, transaction._resetSelections); + moveElements(_addedTransitions, transaction._addedTransitions); + moveElements(_queriedTransitions, transaction._queriedTransitions); + moveElements(_reAppliedTransitions, transaction._reAppliedTransitions); + moveElements(_highlightResets, transaction._highlightResets); + moveElements(_highlightRemoves, transaction._highlightRemoves); + moveElements(_highlightQueries, transaction._highlightQueries); } void Transaction::merge(const Transaction& transaction) { - _resetItems.insert(_resetItems.end(), transaction._resetItems.begin(), transaction._resetItems.end()); - _removedItems.insert(_removedItems.end(), transaction._removedItems.begin(), transaction._removedItems.end()); - _updatedItems.insert(_updatedItems.end(), transaction._updatedItems.begin(), transaction._updatedItems.end()); - _resetSelections.insert(_resetSelections.end(), transaction._resetSelections.begin(), transaction._resetSelections.end()); - _addedTransitions.insert(_addedTransitions.end(), transaction._addedTransitions.begin(), transaction._addedTransitions.end()); - _queriedTransitions.insert(_queriedTransitions.end(), transaction._queriedTransitions.begin(), transaction._queriedTransitions.end()); - _reAppliedTransitions.insert(_reAppliedTransitions.end(), transaction._reAppliedTransitions.begin(), transaction._reAppliedTransitions.end()); - _highlightResets.insert(_highlightResets.end(), transaction._highlightResets.begin(), transaction._highlightResets.end()); - _highlightRemoves.insert(_highlightRemoves.end(), transaction._highlightRemoves.begin(), transaction._highlightRemoves.end()); - _highlightQueries.insert(_highlightQueries.end(), transaction._highlightQueries.begin(), transaction._highlightQueries.end()); + copyElements(_resetItems, transaction._resetItems); + copyElements(_removedItems, transaction._removedItems); + copyElements(_updatedItems, transaction._updatedItems); + copyElements(_resetSelections, transaction._resetSelections); + copyElements(_addedTransitions, transaction._addedTransitions); + copyElements(_queriedTransitions, transaction._queriedTransitions); + copyElements(_reAppliedTransitions, transaction._reAppliedTransitions); + copyElements(_highlightResets, transaction._highlightResets); + copyElements(_highlightRemoves, transaction._highlightRemoves); + copyElements(_highlightQueries, transaction._highlightQueries); +} + +void Transaction::clear() { + _resetItems.clear(); + _removedItems.clear(); + _updatedItems.clear(); + _resetSelections.clear(); + _addedTransitions.clear(); + _queriedTransitions.clear(); + _reAppliedTransitions.clear(); + _highlightResets.clear(); + _highlightRemoves.clear(); + _highlightQueries.clear(); } @@ -102,54 +196,50 @@ bool Scene::isAllocatedID(const ItemID& id) const { /// Enqueue change batch to the scene void Scene::enqueueTransaction(const Transaction& transaction) { - _transactionQueueMutex.lock(); - _transactionQueue.push(transaction); - _transactionQueueMutex.unlock(); + std::unique_lock lock(_transactionQueueMutex); + _transactionQueue.emplace_back(transaction); } -void consolidateTransaction(TransactionQueue& queue, Transaction& singleBatch) { - while (!queue.empty()) { - const auto& transaction = queue.front(); - singleBatch.merge(transaction); - queue.pop(); - }; +void Scene::enqueueTransaction(Transaction&& transaction) { + std::unique_lock lock(_transactionQueueMutex); + _transactionQueue.emplace_back(std::move(transaction)); } uint32_t Scene::enqueueFrame() { PROFILE_RANGE(render, __FUNCTION__); - Transaction consolidatedTransaction; + TransactionQueue localTransactionQueue; { std::unique_lock lock(_transactionQueueMutex); - consolidateTransaction(_transactionQueue, consolidatedTransaction); + localTransactionQueue.swap(_transactionQueue); } - uint32_t frameNumber = 0; + Transaction consolidatedTransaction; + consolidatedTransaction.merge(std::move(localTransactionQueue)); { std::unique_lock lock(_transactionFramesMutex); _transactionFrames.push_back(consolidatedTransaction); - _transactionFrameNumber++; - frameNumber = _transactionFrameNumber; } - return frameNumber; + return ++_transactionFrameNumber; } void Scene::processTransactionQueue() { PROFILE_RANGE(render, __FUNCTION__); - TransactionFrames queuedFrames; + static TransactionFrames queuedFrames; { // capture the queued frames and clear the queue std::unique_lock lock(_transactionFramesMutex); - queuedFrames = _transactionFrames; - _transactionFrames.clear(); + queuedFrames.swap(_transactionFrames); } // go through the queue of frames and process them for (auto& frame : queuedFrames) { processTransactionFrame(frame); } + + queuedFrames.clear(); } void Scene::processTransactionFrame(const Transaction& transaction) { diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index af6204acb4..2d8bc7f4dd 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -65,9 +65,14 @@ public: void resetSelectionHighlight(const std::string& selectionName, const HighlightStyle& style = HighlightStyle()); void removeHighlightFromSelection(const std::string& selectionName); - void querySelectionHighlight(const std::string& selectionName, SelectionHighlightQueryFunc func); + void querySelectionHighlight(const std::string& selectionName, const SelectionHighlightQueryFunc& func); + void reserve(const std::vector& transactionContainer); + void merge(const std::vector& transactionContainer); + void merge(std::vector&& transactionContainer); void merge(const Transaction& transaction); + void merge(Transaction&& transaction); + void clear(); // Checkers if there is work to do when processing the transaction bool touchTransactions() const { return !_resetSelections.empty(); } @@ -107,7 +112,7 @@ protected: HighlightRemoves _highlightRemoves; HighlightQueries _highlightQueries; }; -typedef std::queue TransactionQueue; +typedef std::vector TransactionQueue; // Scene is a container for Items @@ -133,6 +138,9 @@ public: // Enqueue transaction to the scene void enqueueTransaction(const Transaction& transaction); + // Enqueue transaction to the scene + void enqueueTransaction(Transaction&& transaction); + // Enqueue end of frame transactions boundary uint32_t enqueueFrame(); @@ -187,7 +195,7 @@ protected: std::mutex _transactionFramesMutex; - using TransactionFrames = std::list; + using TransactionFrames = std::vector; TransactionFrames _transactionFrames; uint32_t _transactionFrameNumber{ 0 }; diff --git a/libraries/shared/src/AvatarConstants.h b/libraries/shared/src/AvatarConstants.h index a7a80471be..4942c63e27 100644 --- a/libraries/shared/src/AvatarConstants.h +++ b/libraries/shared/src/AvatarConstants.h @@ -12,6 +12,8 @@ #ifndef hifi_AvatarConstants_h #define hifi_AvatarConstants_h +#include "GLMHelpers.h" + // 50th Percentile Man const float DEFAULT_AVATAR_HEIGHT = 1.755f; // meters const float DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD = 0.11f; // meters @@ -52,5 +54,10 @@ const float DEFAULT_AVATAR_JUMP_HEIGHT = (DEFAULT_AVATAR_JUMP_SPEED * DEFAULT_AV const float DEFAULT_AVATAR_FALL_HEIGHT = 20.0f; // meters const float DEFAULT_AVATAR_MIN_HOVER_HEIGHT = 2.5f; // meters +static const float MAX_AVATAR_SCALE = 1000.0f; +static const float MIN_AVATAR_SCALE = 0.005f; + +static const float MAX_AVATAR_HEIGHT = 1000.0f * DEFAULT_AVATAR_HEIGHT; // meters +static const float MIN_AVATAR_HEIGHT = 0.005f * DEFAULT_AVATAR_HEIGHT; // meters #endif // hifi_AvatarConstants_h diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 46613f1283..6f2e7d8b19 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -19,8 +19,14 @@ #include #include #include +#include #include // std::once #include "shared/GlobalAppProperties.h" +#include "SharedUtil.h" + +// Format: AppName-PID-Timestamp +// Example: ... +QString TEMP_DIR_FORMAT { "%1-%2-%3" }; const QString& PathUtils::resourcesPath() { #ifdef Q_OS_MAC @@ -60,7 +66,8 @@ QString PathUtils::generateTemporaryDir() { QString appName = qApp->applicationName(); for (auto i = 0; i < 64; ++i) { auto now = std::chrono::system_clock::now().time_since_epoch().count(); - QDir tempDir = rootTempDir.filePath(appName + "-" + QString::number(now)); + auto dirName = TEMP_DIR_FORMAT.arg(appName).arg(qApp->applicationPid()).arg(now); + QDir tempDir = rootTempDir.filePath(dirName); if (tempDir.mkpath(".")) { return tempDir.absolutePath(); } @@ -68,6 +75,39 @@ QString PathUtils::generateTemporaryDir() { return ""; } +// Delete all temporary directories for an application +int PathUtils::removeTemporaryApplicationDirs(QString appName) { + if (appName.isNull()) { + appName = qApp->applicationName(); + } + + auto dirName = TEMP_DIR_FORMAT.arg(appName).arg("*").arg("*"); + + QDir rootTempDir = QDir::tempPath(); + auto dirs = rootTempDir.entryInfoList({ dirName }, QDir::Dirs); + int removed = 0; + for (auto& dir : dirs) { + auto dirName = dir.fileName(); + auto absoluteDirPath = QDir(dir.absoluteFilePath()); + QRegularExpression re { "^" + QRegularExpression::escape(appName) + "\\-(?\\d+)\\-(?\\d+)$" }; + + auto match = re.match(dirName); + if (match.hasMatch()) { + auto pid = match.capturedRef("pid").toLongLong(); + auto timestamp = match.capturedRef("timestamp"); + if (!processIsRunning(pid)) { + qDebug() << " Removing old temporary directory: " << dir.absoluteFilePath(); + absoluteDirPath.removeRecursively(); + removed++; + } else { + qDebug() << " Not removing (process is running): " << dir.absoluteFilePath(); + } + } + } + + return removed; +} + QString fileNameWithoutExtension(const QString& fileName, const QVector possibleExtensions) { QString fileNameLowered = fileName.toLower(); foreach (const QString possibleExtension, possibleExtensions) { diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index 8c4bcf2394..f3ba5910c4 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -39,6 +39,8 @@ public: static QString generateTemporaryDir(); + static int removeTemporaryApplicationDirs(QString appName = QString::null); + static Qt::CaseSensitivity getFSCaseSensitivity(); static QString stripFilename(const QUrl& url); // note: this is FS-case-sensitive version of parentURL.isParentOf(childURL) diff --git a/libraries/shared/src/PrioritySortUtil.h b/libraries/shared/src/PrioritySortUtil.h index 1d11a04265..dc6a877bb9 100644 --- a/libraries/shared/src/PrioritySortUtil.h +++ b/libraries/shared/src/PrioritySortUtil.h @@ -12,6 +12,9 @@ #define hifi_PrioritySortUtil_h #include +#include + +#include "NumericalConstants.h" #include "ViewFrustum.h" /* PrioritySortUtil is a helper for sorting 3D things relative to a ViewFrustum. To use: @@ -32,11 +35,10 @@ (2) Make a PrioritySortUtil::PriorityQueue and add them to the queue: - PrioritySortUtil::Prioritizer prioritizer(viewFrustum); + PrioritySortUtil::PriorityQueue sortedThings(viewFrustum); std::priority_queue< PrioritySortUtil::Sortable > sortedThings; for (thing in things) { - float priority = prioritizer.computePriority(PrioritySortUtil::PrioritizableThing(thing)); - sortedThings.push(PrioritySortUtil::Sortable entry(thing, priority)); + sortedThings.push(SortableWrapper(thing)); } (3) Loop over your priority queue and do timeboxed work: @@ -65,6 +67,7 @@ namespace PrioritySortUtil { virtual uint64_t getTimestamp() const = 0; void setPriority(float priority) { _priority = priority; } + float getPriority() const { return _priority; } bool operator<(const Sortable& other) const { return _priority < other._priority; } private: float _priority { 0.0f }; @@ -109,11 +112,19 @@ namespace PrioritySortUtil { glm::vec3 position = thing.getPosition(); glm::vec3 offset = position - _view.getPosition(); float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero - float radius = thing.getRadius(); + const float MIN_RADIUS = 0.1f; // WORKAROUND for zero size objects (we still want them to sort by distance) + float radius = glm::min(thing.getRadius(), MIN_RADIUS); + float cosineAngle = (glm::dot(offset, _view.getDirection()) / distance); + float age = (float)(usecTimestampNow() - thing.getTimestamp()); - float priority = _angularWeight * (radius / distance) - + _centerWeight * (glm::dot(offset, _view.getDirection()) / distance) - + _ageWeight * (float)(usecTimestampNow() - thing.getTimestamp()); + // we modulatate "age" drift rate by the cosineAngle term to make periphrial objects sort forward + // at a reduced rate but we don't want the "age" term to go zero or negative so we clamp it + const float MIN_COSINE_ANGLE_FACTOR = 0.1f; + float cosineAngleFactor = glm::max(cosineAngle, MIN_COSINE_ANGLE_FACTOR); + + float priority = _angularWeight * glm::max(radius, MIN_RADIUS) / distance + + _centerWeight * cosineAngle + + _ageWeight * cosineAngleFactor * age; // decrement priority of things outside keyhole if (distance - radius > _view.getCenterRadius()) { diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index 38a7a3165f..2d2ec7c28f 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -44,6 +44,12 @@ extern "C" FILE * __cdecl __iob_func(void) { #include #endif + +#if defined(Q_OS_LINUX) || defined(Q_OS_MAC) +#include +#include +#endif + #include #include #include @@ -1078,6 +1084,24 @@ void setMaxCores(uint8_t maxCores) { #endif } +bool processIsRunning(int64_t pid) { +#ifdef Q_OS_WIN + HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (process) { + DWORD exitCode; + if (GetExitCodeProcess(process, &exitCode) != 0) { + return exitCode == STILL_ACTIVE; + } + } + return false; +#else + if (kill(pid, 0) == -1) { + return errno != ESRCH; + } + return true; +#endif +} + void quitWithParentProcess() { if (qApp) { qDebug() << "Parent process died, quitting"; diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index 25051d45ac..6cf5a4755d 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -238,6 +238,8 @@ void setMaxCores(uint8_t maxCores); const QString PARENT_PID_OPTION = "parent-pid"; void watchParentProcess(int parentPID); +bool processIsRunning(int64_t pid); + #ifdef Q_OS_WIN void* createProcessGroup(); diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 7bc88d73f6..942c9f71a5 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -121,63 +121,58 @@ uint64_t uvec2ToUint64(const uvec2& v) { class AudioHandler : public QObject, QRunnable { Q_OBJECT public: - AudioHandler(QObject* container, const QString& deviceName, int runDelayMs = 0, QObject* parent = nullptr) : QObject(parent) { - _container = container; + AudioHandler(QSharedPointer surface, const QString& deviceName, QObject* parent = nullptr) : QObject(parent) { _newTargetDevice = deviceName; - _runDelayMs = runDelayMs; + _surface = surface; setAutoDelete(true); - QThreadPool::globalInstance()->start(this); + if (deviceName.size() > 0) { + QThreadPool::globalInstance()->start(this); + } } virtual ~AudioHandler() { qDebug() << "Audio Handler Destroyed"; } void run() override { - if (_newTargetDevice.isEmpty()) { - return; - } - if (_runDelayMs > 0) { - QThread::msleep(_runDelayMs); - } - auto audioIO = DependencyManager::get(); - QString deviceName = audioIO->getActiveAudioDevice(QAudio::AudioOutput).deviceName(); - for (auto player : _container->findChildren()) { - auto mediaState = player->state(); - QMediaService *svc = player->service(); - if (nullptr == svc) { - return; - } - QAudioOutputSelectorControl *out = qobject_cast - (svc->requestControl(QAudioOutputSelectorControl_iid)); - if (nullptr == out) { - return; - } - QString deviceOuput; - auto outputs = out->availableOutputs(); - for (int i = 0; i < outputs.size(); i++) { - QString output = outputs[i]; - QString description = out->outputDescription(output); - if (description == deviceName) { - deviceOuput = output; - break; + if (!_surface.isNull() && _surface->getRootItem() && !_surface->getCleaned()) { + for (auto player : _surface->getRootItem()->findChildren()) { + auto mediaState = player->state(); + QMediaService *svc = player->service(); + if (nullptr == svc) { + return; + } + QAudioOutputSelectorControl *out = qobject_cast + (svc->requestControl(QAudioOutputSelectorControl_iid)); + if (nullptr == out) { + return; + } + QString deviceOuput; + auto outputs = out->availableOutputs(); + for (int i = 0; i < outputs.size(); i++) { + QString output = outputs[i]; + QString description = out->outputDescription(output); + if (description == _newTargetDevice) { + deviceOuput = output; + break; + } + } + out->setActiveOutput(deviceOuput); + svc->releaseControl(out); + // if multimedia was paused, it will start playing automatically after changing audio device + // this will reset it back to a paused state + if (mediaState == QMediaPlayer::State::PausedState) { + player->pause(); + } + else if (mediaState == QMediaPlayer::State::StoppedState) { + player->stop(); } } - out->setActiveOutput(deviceOuput); - svc->releaseControl(out); - // if multimedia was paused, it will start playing automatically after changing audio device - // this will reset it back to a paused state - if (mediaState == QMediaPlayer::State::PausedState) { - player->pause(); - } else if (mediaState == QMediaPlayer::State::StoppedState) { - player->stop(); - } } - qDebug() << "QML Audio changed to " << deviceName; + qDebug() << "QML Audio changed to " << _newTargetDevice; } private: QString _newTargetDevice; - QObject* _container; - int _runDelayMs; + QSharedPointer _surface; }; class OffscreenTextures { @@ -502,6 +497,7 @@ QOpenGLContext* OffscreenQmlSurface::getSharedContext() { } void OffscreenQmlSurface::cleanup() { + _isCleaned = true; _canvas->makeCurrent(); _renderControl->invalidate(); @@ -600,6 +596,7 @@ OffscreenQmlSurface::OffscreenQmlSurface() { OffscreenQmlSurface::~OffscreenQmlSurface() { QObject::disconnect(&_updateTimer); + disconnectAudioOutputTimer(); QObject::disconnect(qApp); cleanup(); @@ -613,6 +610,15 @@ OffscreenQmlSurface::~OffscreenQmlSurface() { void OffscreenQmlSurface::onAboutToQuit() { _paused = true; QObject::disconnect(&_updateTimer); + disconnectAudioOutputTimer(); + +} + +void OffscreenQmlSurface::disconnectAudioOutputTimer() { + if (_audioOutputUpdateTimer.isActive()) { + _audioOutputUpdateTimer.stop(); + } + QObject::disconnect(&_audioOutputUpdateTimer); } void OffscreenQmlSurface::create() { @@ -671,6 +677,14 @@ void OffscreenQmlSurface::create() { } }); + // Setup the update of the QML media components with the current audio output device + QObject::connect(&_audioOutputUpdateTimer, &QTimer::timeout, this, [this]() { + new AudioHandler(sharedFromThis(), _currentAudioOutputDevice); + }); + int waitForAudioQmlMs = 200; + _audioOutputUpdateTimer.setInterval(waitForAudioQmlMs); + _audioOutputUpdateTimer.setSingleShot(true); + // When Quick says there is a need to render, we will not render immediately. Instead, // a timer with a small interval is used to get better performance. QObject::connect(&_updateTimer, &QTimer::timeout, this, &OffscreenQmlSurface::updateQuick); @@ -699,10 +713,11 @@ void OffscreenQmlSurface::forceQmlAudioOutputDeviceUpdate() { QMetaObject::invokeMethod(this, "forceQmlAudioOutputDeviceUpdate", Qt::QueuedConnection); } else { auto audioIO = DependencyManager::get(); - QString deviceName = audioIO->getActiveAudioDevice(QAudio::AudioOutput).deviceName(); - int waitForAudioQmlMs = 500; - // The audio device need to be change using oth - new AudioHandler(_rootItem, deviceName, waitForAudioQmlMs); + _currentAudioOutputDevice = audioIO->getActiveAudioDevice(QAudio::AudioOutput).deviceName(); + if (_audioOutputUpdateTimer.isActive()) { + _audioOutputUpdateTimer.stop(); + } + _audioOutputUpdateTimer.start(); } } @@ -1134,6 +1149,7 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QT touchEvent.setTarget(_rootItem); touchEvent.setTouchPoints(touchPoints); touchEvent.setTouchPointStates(touchPointStates); + touchEvent.setTimestamp((ulong)QDateTime::currentMSecsSinceEpoch()); touchEvent.ignore(); } diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.h b/libraries/ui/src/ui/OffscreenQmlSurface.h index 44c6c3c77b..4c23c62c12 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.h +++ b/libraries/ui/src/ui/OffscreenQmlSurface.h @@ -40,7 +40,7 @@ class QQuickItem; using QmlContextCallback = std::function; -class OffscreenQmlSurface : public QObject { +class OffscreenQmlSurface : public QObject, public QEnableSharedFromThis { Q_OBJECT Q_PROPERTY(bool focusText READ isFocusText NOTIFY focusTextChanged) public: @@ -75,6 +75,7 @@ public: void pause(); void resume(); bool isPaused() const; + bool getCleaned() { return _isCleaned; } void setBaseUrl(const QUrl& baseUrl); QQuickItem* getRootItem(); @@ -116,6 +117,7 @@ public slots: void changeAudioOutputDevice(const QString& deviceName, bool isHtmlUpdate = false); void forceHtmlAudioOutputDeviceUpdate(); void forceQmlAudioOutputDeviceUpdate(); + signals: void audioOutputDeviceChanged(const QString& deviceName); @@ -147,6 +149,7 @@ private: void render(); void cleanup(); QJsonObject getGLContextData(); + void disconnectAudioOutputTimer(); private slots: void updateQuick(); @@ -170,6 +173,9 @@ private: uint64_t _lastRenderTime { 0 }; uvec2 _size; + QTimer _audioOutputUpdateTimer; + QString _currentAudioOutputDevice; + // Texture management TextureAndFence _latestTextureAndFence { 0, 0 }; @@ -177,6 +183,7 @@ private: bool _polish { true }; bool _paused { true }; bool _focusText { false }; + bool _isCleaned{ false }; uint8_t _maxFps { 60 }; MouseTranslator _mouseTranslator { [](const QPointF& p) { return p.toPoint(); } }; QWindow* _proxyWindow { nullptr }; diff --git a/scripts/developer/utilities/audio/audioScope.js b/scripts/developer/utilities/audio/audioScope.js new file mode 100644 index 0000000000..00c9e4b725 --- /dev/null +++ b/scripts/developer/utilities/audio/audioScope.js @@ -0,0 +1,17 @@ +var qml = Script.resourcesPath() + '/qml/AudioScope.qml'; +var window = new OverlayWindow({ + title: 'Audio Scope', + source: qml, + width: 1200, + height: 500 +}); +window.closed.connect(function () { + if (Audio.getRecording()) { + Audio.stopRecording(); + } + AudioScope.setVisible(false); + AudioScope.setLocalEcho(false); + AudioScope.setServerEcho(false); + AudioScope.selectAudioScopeFiveFrames(); + Script.stop(); +}); \ No newline at end of file diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index bad6793995..57f3b4fd8b 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -376,6 +376,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); if (PROFILE) { Script.endProfileRange("dispatch.run"); } + Script.setTimeout(_this.update, BASIC_TIMER_INTERVAL_MS); }; this.setBlacklist = function() { @@ -470,7 +471,6 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); }; this.cleanup = function () { - Script.update.disconnect(_this.update); Controller.disableMapping(MAPPING_NAME); _this.pointerManager.removePointers(); Pointers.removePointer(this.mouseRayPick); @@ -501,5 +501,5 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.messageReceived.connect(controllerDispatcher.handleHandMessage); Script.scriptEnding.connect(controllerDispatcher.cleanup); - Script.update.connect(controllerDispatcher.update); + Script.setTimeout(controllerDispatcher.update, BASIC_TIMER_INTERVAL_MS); }()); diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index b8c20d5bd6..a250f77b2e 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -10,7 +10,7 @@ getControllerJointIndex, enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, entityIsCloneable, - cloneEntity, DISPATCHER_PROPERTIES + cloneEntity, DISPATCHER_PROPERTIES, TEAR_AWAY_DISTANCE */ Script.include("/~/system/libraries/Xform.js"); @@ -138,9 +138,9 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var dimensions; if (overlayInfoSet.type === "sphere") { - dimensions = overlayInfoSet.hotspot.radius * 2 * overlayInfoSet.currentSize * EQUIP_SPHERE_SCALE_FACTOR; + dimensions = (overlayInfoSet.hotspot.radius / 2) * overlayInfoSet.currentSize * EQUIP_SPHERE_SCALE_FACTOR; } else { - dimensions = overlayInfoSet.hotspot.radius * 2 * overlayInfoSet.currentSize; + dimensions = (overlayInfoSet.hotspot.radius / 2) * overlayInfoSet.currentSize; } overlayInfoSet.overlays.forEach(function(overlay) { @@ -162,7 +162,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; - var EQUIP_RADIUS = 0.2; // radius used for palm vs equip-hotspot for equipping. + var EQUIP_RADIUS = 1.0; // radius used for palm vs equip-hotspot for equipping. var HAPTIC_PULSE_STRENGTH = 1.0; var HAPTIC_PULSE_DURATION = 13.0; @@ -322,7 +322,9 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa } } else { var wearableProps = getWearableData(props); + var sensorToScaleFactor = MyAvatar.sensorToWorldScale; if (wearableProps && wearableProps.joints) { + result.push({ key: entityID.toString() + "0", entityID: entityID, @@ -332,7 +334,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa z: 0 }, worldPosition: entityXform.pos, - radius: EQUIP_RADIUS, + radius: EQUIP_RADIUS * sensorToScaleFactor, joints: wearableProps.joints, modelURL: null, modelScale: null diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js index 38ee29ae3b..a1faacbdd6 100644 --- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js +++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js @@ -62,7 +62,7 @@ this.pointingAtTablet = function(controllerData) { var rayPick = controllerData.rayPicks[this.hand]; - return (rayPick.objectID === HMD.tabletScreenID || rayPick.objectID === HMD.homeButtonID); + return (HMD.tabletScreenID && HMD.homeButtonID && (rayPick.objectID === HMD.tabletScreenID || rayPick.objectID === HMD.homeButtonID)); }; this.getOtherModule = function() { diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js index 26bebc9247..01c8424e0c 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -81,7 +81,8 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); }; this.otherHandIsParent = function(props) { - return this.getOtherModule().thisHandIsParent(props); + var otherModule = this.getOtherModule(); + return (otherModule.thisHandIsParent(props) && otherModule.grabbing); }; this.startNearParentingGrabEntity = function (controllerData, targetProps) { diff --git a/scripts/system/controllers/controllerModules/scaleAvatar.js b/scripts/system/controllers/controllerModules/scaleAvatar.js index e4ae3654e1..1868b0228a 100644 --- a/scripts/system/controllers/controllerModules/scaleAvatar.js +++ b/scripts/system/controllers/controllerModules/scaleAvatar.js @@ -12,6 +12,10 @@ (function () { var dispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js"); + function clamp(val, min, max) { + return Math.max(min, Math.min(max, val)); + } + function ScaleAvatar(hand) { this.hand = hand; this.scalingStartAvatarScale = 0; @@ -61,7 +65,7 @@ controllerData.controllerLocations[this.otherHand()].position)); var newAvatarScale = (scalingCurrentDistance / this.scalingStartDistance) * this.scalingStartAvatarScale; - MyAvatar.scale = newAvatarScale; + MyAvatar.scale = clamp(newAvatarScale, MyAvatar.getDomainMinScale(), MyAvatar.getDomainMaxScale()); MyAvatar.scaleChanged(); } return dispatcherUtils.makeRunningValues(true, [], []); diff --git a/scripts/system/directory.js b/scripts/system/directory.js index 8b9ec17f05..f84429ab95 100644 --- a/scripts/system/directory.js +++ b/scripts/system/directory.js @@ -63,7 +63,7 @@ var toolBar = (function() { y: -TOOLBAR_MARGIN_Y - toolHeight }); browseDirectoryButton = toolBar.addTool({ - imageURL: toolIconUrl + "directory-01.svg", + imageURL: toolIconUrl + "directory.svg", subImage: { x: 0, y: Tool.IMAGE_WIDTH, diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 6aacaa5333..736d42d593 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1415,6 +1415,9 @@ input#reset-to-natural-dimensions { } /* ----- Order of Menu items for Primitive ----- */ +/* Entity Menu classes are specified by selected entity + within entityProperties.js +*/ #properties-list.ShapeMenu #general, #properties-list.BoxMenu #general, #properties-list.SphereMenu #general { @@ -1469,6 +1472,34 @@ input#reset-to-natural-dimensions { display: none; } +/* ----- ParticleEffectMenu ----- */ +#properties-list.ParticleEffectMenu #general { + order: 1; +} +#properties-list.ParticleEffectMenu #collision-info { + order: 2; +} +#properties-list.ParticleEffectMenu #physical { + order: 3; +} +#properties-list.ParticleEffectMenu #spatial { + order: 4; +} +#properties-list.ParticleEffectMenu #behavior { + order: 5; +} + +/* items to hide */ +#properties-list.ParticleEffectMenu #base-color-section, +#properties-list.ParticleEffectMenu #hyperlink, +#properties-list.ParticleEffectMenu #light, +#properties-list.ParticleEffectMenu #model, +#properties-list.ParticleEffectMenu #shape-list, +#properties-list.ParticleEffectMenu #text, +#properties-list.ParticleEffectMenu #web, +#properties-list.ParticleEffectMenu #zone { + display: none; +} /* ----- Order of Menu items for Light ----- */ #properties-list.LightMenu #general { @@ -1500,8 +1531,8 @@ input#reset-to-natural-dimensions { display: none; } /* items to hide */ -#properties-list.LightMenu .shape-group.shape-section.property.dropdown, -#properties-list.LightMenu color-section.color-control1 { +#properties-list.LightMenu #shape-list, +#properties-list.LightMenu #base-color-section { display: none } @@ -1536,8 +1567,8 @@ input#reset-to-natural-dimensions { display: none; } /* items to hide */ -#properties-list.ModelMenu .shape-group.shape-section.property.dropdown, -#properties-list.ModelMenu .color-section.color-control1 { +#properties-list.ModelMenu #shape-list, +#properties-list.ModelMenu #base-color-section { display: none } @@ -1572,8 +1603,8 @@ input#reset-to-natural-dimensions { display: none; } /* items to hide */ -#properties-list.ZoneMenu .shape-group.shape-section.property.dropdown, -#properties-list.ZoneMenu .color-section.color-control1 { +#properties-list.ZoneMenu #shape-list, +#properties-list.ZoneMenu #base-color-section { display: none } @@ -1608,8 +1639,8 @@ input#reset-to-natural-dimensions { display: none; } /* items to hide */ -#properties-list.WebMenu .shape-group.shape-section.property.dropdown, -#properties-list.WebMenu .color-section.color-control1 { +#properties-list.WebMenu #shape-list, +#properties-list.WebMenu #base-color-section { display: none; } @@ -1645,8 +1676,8 @@ input#reset-to-natural-dimensions { display: none; } /* items to hide */ -#properties-list.TextMenu .shape-group.shape-section.property.dropdown, -#properties-list.TextMenu .color-section.color-control1 { +#properties-list.TextMenu #shape-list, +#properties-list.TextMenu #base-color-section { display: none } diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 8b2a088d83..2ccad2c169 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -44,7 +44,7 @@
-
-
+ +
+ CollisionM
@@ -216,17 +223,6 @@
-
-
-
- -
-
-
-
-
-
-
diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index f54394a353..0ab9f7a9cb 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -6,6 +6,9 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +/* global alert, augmentSpinButtons, clearTimeout, console, document, Element, EventBridge, + HifiEntityUI, JSONEditor, openEventBridge, setUpKeyboardControl, setTimeout, window, _ $ */ + var PI = 3.14159265358979; var DEGREES_TO_RADIANS = PI / 180.0; var RADIANS_TO_DEGREES = 180.0 / PI; @@ -22,33 +25,33 @@ var ICON_FOR_TYPE = { PolyVox: "", Multiple: "", PolyLine: "" -} +}; var EDITOR_TIMEOUT_DURATION = 1500; -const KEY_P = 80; //Key code for letter p used for Parenting hotkey. +var KEY_P = 80; // Key code for letter p used for Parenting hotkey. var colorPickers = []; var lastEntityID = null; -debugPrint = function(message) { +function debugPrint(message) { EventBridge.emitWebEvent( JSON.stringify({ type: "print", message: message }) ); -}; +} function enableChildren(el, selector) { - els = el.querySelectorAll(selector); - for (var i = 0; i < els.length; i++) { - els[i].removeAttribute('disabled'); + var elSelectors = el.querySelectorAll(selector); + for (var selectorIndex = 0; selectorIndex < elSelectors.length; ++selectorIndex) { + elSelectors[selectorIndex].removeAttribute('disabled'); } } function disableChildren(el, selector) { - els = el.querySelectorAll(selector); - for (var i = 0; i < els.length; i++) { - els[i].setAttribute('disabled', 'disabled'); + var elSelectors = el.querySelectorAll(selector); + for (var selectorIndex = 0; selectorIndex < elSelectors.length; ++selectorIndex) { + elSelectors[selectorIndex].setAttribute('disabled', 'disabled'); } } @@ -103,16 +106,6 @@ function createEmitCheckedPropertyUpdateFunction(propertyName) { }; } -function createEmitCheckedToStringPropertyUpdateFunction(checkboxElement, name, propertyName) { - var newString = ""; - if (checkboxElement.checked) { - newString += name + ""; - } else { - - } - -} - function createEmitGroupCheckedPropertyUpdateFunction(group, propertyName) { return function() { var properties = {}; @@ -123,7 +116,7 @@ function createEmitGroupCheckedPropertyUpdateFunction(group, propertyName) { } function createEmitNumberPropertyUpdateFunction(propertyName, decimals) { - decimals = decimals == undefined ? 4 : decimals; + decimals = ((decimals === undefined) ? 4 : decimals); return function() { var value = parseFloat(this.value).toFixed(decimals); updateProperty(propertyName, value); @@ -146,7 +139,9 @@ function createEmitTextPropertyUpdateFunction(propertyName) { }; } -function createZoneComponentModeChangedFunction(zoneComponent, zoneComponentModeInherit, zoneComponentModeDisabled, zoneComponentModeEnabled) { +function createZoneComponentModeChangedFunction(zoneComponent, zoneComponentModeInherit, + zoneComponentModeDisabled, zoneComponentModeEnabled) { + return function() { var zoneComponentMode; @@ -159,7 +154,7 @@ function createZoneComponentModeChangedFunction(zoneComponent, zoneComponentMode } updateProperty(zoneComponent, zoneComponentMode); - } + }; } function createEmitGroupTextPropertyUpdateFunction(group, propertyName) { @@ -177,11 +172,11 @@ function createEmitVec3PropertyUpdateFunction(property, elX, elY, elZ) { properties[property] = { x: elX.value, y: elY.value, - z: elZ.value, + z: elZ.value }; updateProperties(properties); - } -}; + }; +} function createEmitGroupVec3PropertyUpdateFunction(group, property, elX, elY, elZ) { return function() { @@ -190,11 +185,11 @@ function createEmitGroupVec3PropertyUpdateFunction(group, property, elX, elY, el properties[group][property] = { x: elX.value, y: elY.value, - z: elZ ? elZ.value : 0, + z: elZ ? elZ.value : 0 }; updateProperties(properties); - } -}; + }; +} function createEmitVec3PropertyUpdateFunctionWithMultiplier(property, elX, elY, elZ, multiplier) { return function() { @@ -202,17 +197,17 @@ function createEmitVec3PropertyUpdateFunctionWithMultiplier(property, elX, elY, properties[property] = { x: elX.value * multiplier, y: elY.value * multiplier, - z: elZ.value * multiplier, + z: elZ.value * multiplier }; updateProperties(properties); - } -}; + }; +} function createEmitColorPropertyUpdateFunction(property, elRed, elGreen, elBlue) { return function() { emitColorPropertyUpdate(property, elRed.value, elGreen.value, elBlue.value); - } -}; + }; +} function emitColorPropertyUpdate(property, red, green, blue, group) { var properties = {}; @@ -221,17 +216,17 @@ function emitColorPropertyUpdate(property, red, green, blue, group) { properties[group][property] = { red: red, green: green, - blue: blue, + blue: blue }; } else { properties[property] = { red: red, green: green, - blue: blue, + blue: blue }; } updateProperties(properties); -}; +} function createEmitGroupColorPropertyUpdateFunction(group, property, elRed, elGreen, elBlue) { @@ -241,11 +236,11 @@ function createEmitGroupColorPropertyUpdateFunction(group, property, elRed, elGr properties[group][property] = { red: elRed.value, green: elGreen.value, - blue: elBlue.value, + blue: elBlue.value }; updateProperties(properties); - } -}; + }; +} function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElement, subPropertyString) { if (subPropertyElement.checked) { @@ -264,12 +259,12 @@ function setUserDataFromEditor(noUpdate) { try { json = editor.get(); } catch (e) { - alert('Invalid JSON code - look for red X in your code ', +e) + alert('Invalid JSON code - look for red X in your code ', +e); } if (json === null) { return; } else { - var text = editor.getText() + var text = editor.getText(); if (noUpdate === true) { EventBridge.emitWebEvent( JSON.stringify({ @@ -277,7 +272,7 @@ function setUserDataFromEditor(noUpdate) { type: "saveUserData", properties: { userData: text - }, + } }) ); return; @@ -292,22 +287,24 @@ function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults) { var parsedData = {}; try { if ($('#userdata-editor').css('height') !== "0px") { - //if there is an expanded, we want to use its json. + // if there is an expanded, we want to use its json. parsedData = getEditorJSON(); } else { parsedData = JSON.parse(userDataElement.value); } - } catch (e) {} + } catch (e) { + // TODO: Should an alert go here? + } if (!(groupName in parsedData)) { - parsedData[groupName] = {} + parsedData[groupName] = {}; } var keys = Object.keys(updateKeyPair); keys.forEach(function (key) { delete parsedData[groupName][key]; if (updateKeyPair[key] !== null && updateKeyPair[key] !== "null") { if (updateKeyPair[key] instanceof Element) { - if(updateKeyPair[key].type === "checkbox") { + if (updateKeyPair[key].type === "checkbox") { if (updateKeyPair[key].checked !== defaults[key]) { parsedData[groupName][key] = updateKeyPair[key].checked; } @@ -322,16 +319,16 @@ function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults) { } } }); - if (Object.keys(parsedData[groupName]).length == 0) { + if (Object.keys(parsedData[groupName]).length === 0) { delete parsedData[groupName]; } if (Object.keys(parsedData).length > 0) { - properties['userData'] = JSON.stringify(parsedData); + properties.userData = JSON.stringify(parsedData); } else { - properties['userData'] = ''; + properties.userData = ''; } - userDataElement.value = properties['userData']; + userDataElement.value = properties.userData; updateProperties(properties); } @@ -340,13 +337,12 @@ function userDataChanger(groupName, keyName, values, userDataElement, defaultVal val[keyName] = values; def[keyName] = defaultValue; multiDataUpdater(groupName, val, userDataElement, def); -}; +} function setTextareaScrolling(element) { var isScrolling = element.scrollHeight > element.offsetHeight; element.setAttribute("scrolling", isScrolling ? "true" : "false"); -}; - +} var editor = null; @@ -364,7 +360,7 @@ function createJSONEditor() { $('.jsoneditor-poweredBy').remove(); }, onError: function(e) { - alert('JSON editor:' + e) + alert('JSON editor:' + e); }, onChange: function() { var currentJSONString = editor.getText(); @@ -372,22 +368,22 @@ function createJSONEditor() { if (currentJSONString === '{"":""}') { return; } - $('#userdata-save').attr('disabled', false) + $('#userdata-save').attr('disabled', false); } }; editor = new JSONEditor(container, options); -}; +} function hideNewJSONEditorButton() { $('#userdata-new-editor').hide(); -}; +} function hideClearUserDataButton() { $('#userdata-clear').hide(); -}; +} function showSaveUserDataButton() { $('#userdata-save').show(); @@ -401,65 +397,65 @@ function hideSaveUserDataButton() { function showNewJSONEditorButton() { $('#userdata-new-editor').show(); -}; +} function showClearUserDataButton() { $('#userdata-clear').show(); -}; +} function showUserDataTextArea() { $('#property-user-data').show(); -}; +} function hideUserDataTextArea() { $('#property-user-data').hide(); -}; +} function showStaticUserData() { if (editor !== null) { $('#static-userdata').show(); - $('#static-userdata').css('height', $('#userdata-editor').height()) + $('#static-userdata').css('height', $('#userdata-editor').height()); $('#static-userdata').text(editor.getText()); } -}; +} function removeStaticUserData() { $('#static-userdata').hide(); -}; +} function setEditorJSON(json) { - editor.set(json) + editor.set(json); if (editor.hasOwnProperty('expandAll')) { editor.expandAll(); } -}; +} function getEditorJSON() { return editor.get(); -}; +} function deleteJSONEditor() { if (editor !== null) { editor.destroy(); editor = null; } -}; +} var savedJSONTimer = null; function saveJSONUserData(noUpdate) { setUserDataFromEditor(noUpdate); $('#userdata-saved').show(); - $('#userdata-save').attr('disabled', true) + $('#userdata-save').attr('disabled', true); if (savedJSONTimer !== null) { clearTimeout(savedJSONTimer); } savedJSONTimer = setTimeout(function() { $('#userdata-saved').hide(); - }, 1500) + }, EDITOR_TIMEOUT_DURATION); } function bindAllNonJSONEditorElements() { @@ -468,6 +464,8 @@ function bindAllNonJSONEditorElements() { for (i = 0; i < inputs.length; i++) { var input = inputs[i]; var field = $(input); + // TODO FIXME: (JSHint) Functions declared within loops referencing + // an outer scoped variable may lead to confusing semantics. field.on('focus', function(e) { if (e.target.id === "userdata-new-editor" || e.target.id === "userdata-clear") { return; @@ -477,7 +475,7 @@ function bindAllNonJSONEditorElements() { } } - }) + }); } } @@ -492,18 +490,17 @@ function unbindAllInputs() { } function clearSelection() { - if(document.selection && document.selection.empty) { - document.selection.empty(); - } else if(window.getSelection) { - var sel = window.getSelection(); - sel.removeAllRanges(); - } + if (document.selection && document.selection.empty) { + document.selection.empty(); + } else if (window.getSelection) { + var sel = window.getSelection(); + sel.removeAllRanges(); + } } function loaded() { openEventBridge(function() { - var allSections = []; var elPropertiesList = document.getElementById("properties-list"); var elID = document.getElementById("property-id"); var elType = document.getElementById("property-type"); @@ -589,21 +586,14 @@ function loaded() { var elUserData = document.getElementById("property-user-data"); var elClearUserData = document.getElementById("userdata-clear"); var elSaveUserData = document.getElementById("userdata-save"); - var elJSONEditor = document.getElementById("userdata-editor"); var elNewJSONEditor = document.getElementById('userdata-new-editor'); - var elColorSections = document.querySelectorAll(".color-section"); - var elColorControl1 = document.getElementById("property-color-control1"); - var elColorControl2 = document.getElementById("property-color-control2"); + var elColorControlVariant2 = document.getElementById("property-color-control2"); var elColorRed = document.getElementById("property-color-red"); var elColorGreen = document.getElementById("property-color-green"); var elColorBlue = document.getElementById("property-color-blue"); - var elShapeSections = document.querySelectorAll(".shape-section"); - allSections.push(elShapeSections); var elShape = document.getElementById("property-shape"); - var elLightSections = document.querySelectorAll(".light-section"); - allSections.push(elLightSections); var elLightSpotLight = document.getElementById("property-light-spot-light"); var elLightColor = document.getElementById("property-light-color"); var elLightColorRed = document.getElementById("property-light-color-red"); @@ -615,8 +605,6 @@ function loaded() { var elLightExponent = document.getElementById("property-light-exponent"); var elLightCutoff = document.getElementById("property-light-cutoff"); - var elModelSections = document.querySelectorAll(".model-section"); - allSections.push(elModelSections); var elModelURL = document.getElementById("property-model-url"); var elShapeType = document.getElementById("property-shape-type"); var elCompoundShapeURL = document.getElementById("property-compound-shape-url"); @@ -632,8 +620,6 @@ function loaded() { var elModelTextures = document.getElementById("property-model-textures"); var elModelOriginalTextures = document.getElementById("property-model-original-textures"); - var elWebSections = document.querySelectorAll(".web-section"); - allSections.push(elWebSections); var elWebSourceURL = document.getElementById("property-web-source-url"); var elWebDPI = document.getElementById("property-web-dpi"); @@ -641,11 +627,7 @@ function loaded() { var elHyperlinkHref = document.getElementById("property-hyperlink-href"); - var elHyperlinkSections = document.querySelectorAll(".hyperlink-section"); - - var elTextSections = document.querySelectorAll(".text-section"); - allSections.push(elTextSections); var elTextText = document.getElementById("property-text-text"); var elTextLineHeight = document.getElementById("property-text-line-height"); var elTextTextColor = document.getElementById("property-text-text-color"); @@ -653,13 +635,10 @@ function loaded() { var elTextTextColorRed = document.getElementById("property-text-text-color-red"); var elTextTextColorGreen = document.getElementById("property-text-text-color-green"); var elTextTextColorBlue = document.getElementById("property-text-text-color-blue"); - var elTextBackgroundColor = document.getElementById("property-text-background-color"); var elTextBackgroundColorRed = document.getElementById("property-text-background-color-red"); var elTextBackgroundColorGreen = document.getElementById("property-text-background-color-green"); var elTextBackgroundColorBlue = document.getElementById("property-text-background-color-blue"); - var elZoneSections = document.querySelectorAll(".zone-section"); - allSections.push(elZoneSections); var elZoneStageSunModelEnabled = document.getElementById("property-zone-stage-sun-model-enabled"); var elZoneKeyLightColor = document.getElementById("property-zone-key-light-color"); @@ -670,7 +649,6 @@ function loaded() { var elZoneKeyLightAmbientIntensity = document.getElementById("property-zone-key-ambient-intensity"); var elZoneKeyLightDirectionX = document.getElementById("property-zone-key-light-direction-x"); var elZoneKeyLightDirectionY = document.getElementById("property-zone-key-light-direction-y"); - var elZoneKeyLightDirectionZ = document.getElementById("property-zone-key-light-direction-z"); var elZoneKeyLightAmbientURL = document.getElementById("property-zone-key-ambient-url"); var elZoneHazeModeInherit = document.getElementById("property-zone-haze-mode-inherit"); @@ -694,11 +672,7 @@ function loaded() { var elZoneHazeCeiling = document.getElementById("property-zone-haze-ceiling"); var elZoneHazeBackgroundBlend = document.getElementById("property-zone-haze-background-blend"); - - var elZoneHazeAttenuateKeyLight = document.getElementById("property-zone-haze-attenuate-keylight"); - var elZoneHazeKeyLightRange = document.getElementById("property-zone-haze-keylight-range"); - var elZoneHazeKeyLightAltitude = document.getElementById("property-zone-haze-keylight-altitude"); - + var elZoneStageLatitude = document.getElementById("property-zone-stage-latitude"); var elZoneStageLongitude = document.getElementById("property-zone-stage-longitude"); var elZoneStageAltitude = document.getElementById("property-zone-stage-altitude"); @@ -718,8 +692,6 @@ function loaded() { var elZoneGhostingAllowed = document.getElementById("property-zone-ghosting-allowed"); var elZoneFilterURL = document.getElementById("property-zone-filter-url"); - var elPolyVoxSections = document.querySelectorAll(".poly-vox-section"); - allSections.push(elPolyVoxSections); var elVoxelVolumeSizeX = document.getElementById("property-voxel-volume-size-x"); var elVoxelVolumeSizeY = document.getElementById("property-voxel-volume-size-y"); var elVoxelVolumeSizeZ = document.getElementById("property-voxel-volume-size-z"); @@ -732,10 +704,10 @@ function loaded() { var properties; EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); - if (data.type == "server_script_status") { + if (data.type === "server_script_status") { elServerScriptError.value = data.errorInfo; // If we just set elServerScriptError's diplay to block or none, we still end up with - //it's parent contributing 21px bottom padding even when elServerScriptError is display:none. + // it's parent contributing 21px bottom padding even when elServerScriptError is display:none. // So set it's parent to block or none elServerScriptError.parentElement.style.display = data.errorInfo ? "block" : "none"; if (data.statusRetrieved === false) { @@ -744,18 +716,18 @@ function loaded() { var ENTITY_SCRIPT_STATUS = { pending: "Pending", loading: "Loading", - error_loading_script: "Error loading script", - error_running_script: "Error running script", + error_loading_script: "Error loading script", // eslint-disable-line camelcase + error_running_script: "Error running script", // eslint-disable-line camelcase running: "Running", - unloaded: "Unloaded", + unloaded: "Unloaded" }; elServerScriptStatus.innerText = ENTITY_SCRIPT_STATUS[data.status] || data.status; } else { elServerScriptStatus.innerText = "Not running"; } - } else if (data.type == "update") { + } else if (data.type === "update") { - if (!data.selections || data.selections.length == 0) { + if (!data.selections || data.selections.length === 0) { if (editor !== null && lastEntityID !== null) { saveJSONUserData(true); deleteJSONEditor(); @@ -775,20 +747,19 @@ function loaded() { for (var i = 0; i < selections.length; i++) { ids.push(selections[i].id); - var type = selections[i].properties.type; - if (types[type] === undefined) { - types[type] = 0; + var currentSelectedType = selections[i].properties.type; + if (types[currentSelectedType] === undefined) { + types[currentSelectedType] = 0; numTypes += 1; } - types[type]++; + types[currentSelectedType]++; } - var type; + var type = "Multiple"; if (numTypes === 1) { type = selections[0].properties.type; - } else { - type = "Multiple"; } + elType.innerHTML = type + " (" + data.selections.length + ")"; elTypeIcon.innerHTML = ICON_FOR_TYPE[type]; elTypeIcon.style.display = "inline-block"; @@ -804,12 +775,14 @@ function loaded() { if (lastEntityID !== '"' + properties.id + '"' && lastEntityID !== null && editor !== null) { saveJSONUserData(true); } - //the event bridge and json parsing handle our avatar id string differently. + // the event bridge and json parsing handle our avatar id string differently. lastEntityID = '"' + properties.id + '"'; elID.value = properties.id; + // Create class name for css ruleset filtering elPropertiesList.className = properties.type + 'Menu'; + elType.innerHTML = properties.type; elTypeIcon.innerHTML = ICON_FOR_TYPE[properties.type]; elTypeIcon.style.display = "inline-block"; @@ -883,13 +856,13 @@ function loaded() { elCloneableLifetime.value = 300; var grabbablesSet = false; - var parsedUserData = {} + var parsedUserData = {}; try { parsedUserData = JSON.parse(properties.userData); if ("grabbableKey" in parsedUserData) { grabbablesSet = true; - var grabbableData = parsedUserData["grabbableKey"]; + var grabbableData = parsedUserData.grabbableKey; if ("grabbable" in grabbableData) { elGrabbable.checked = grabbableData.grabbable; } else { @@ -907,27 +880,28 @@ function loaded() { } if ("cloneable" in grabbableData) { elCloneable.checked = grabbableData.cloneable; - elCloneableGroup.style.display = elCloneable.checked ? "block": "none"; + elCloneableGroup.style.display = elCloneable.checked ? "block" : "none"; elCloneableDynamic.checked = grabbableData.cloneDynamic ? grabbableData.cloneDynamic : properties.dynamic; if (elCloneable.checked) { - if ("cloneLifetime" in grabbableData) { - elCloneableLifetime.value = - grabbableData.cloneLifetime ? grabbableData.cloneLifetime : 300; - } - if ("cloneLimit" in grabbableData) { - elCloneableLimit.value = grabbableData.cloneLimit ? grabbableData.cloneLimit : 0; - } - if ("cloneAvatarEntity" in grabbableData) { - elCloneableAvatarEntity.checked = - grabbableData.cloneAvatarEntity ? grabbableData.cloneAvatarEntity : false; - } + if ("cloneLifetime" in grabbableData) { + elCloneableLifetime.value = + grabbableData.cloneLifetime ? grabbableData.cloneLifetime : 300; + } + if ("cloneLimit" in grabbableData) { + elCloneableLimit.value = grabbableData.cloneLimit ? grabbableData.cloneLimit : 0; + } + if ("cloneAvatarEntity" in grabbableData) { + elCloneableAvatarEntity.checked = + grabbableData.cloneAvatarEntity ? grabbableData.cloneAvatarEntity : false; + } } } else { elCloneable.checked = false; } } } catch (e) { + // TODO: What should go here? } if (!grabbablesSet) { elGrabbable.checked = true; @@ -946,7 +920,7 @@ function loaded() { try { json = JSON.parse(properties.userData); } catch (e) { - //normal text + // normal text deleteJSONEditor(); elUserData.value = properties.userData; showUserDataTextArea(); @@ -968,19 +942,21 @@ function loaded() { elDescription.value = properties.description; - if (properties.type == "Shape" || properties.type == "Box" || properties.type == "Sphere") { + if (properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { elShape.value = properties.shape; setDropdownText(elShape); } - if (properties.type == "Shape" || properties.type == "Box" || properties.type == "Sphere" || properties.type == "ParticleEffect") { + if (properties.type === "Shape" || properties.type === "Box" || + properties.type === "Sphere" || properties.type === "ParticleEffect") { elColorRed.value = properties.color.red; elColorGreen.value = properties.color.green; elColorBlue.value = properties.color.blue; - elColorControl1.style.backgroundColor = elColorControl2.style.backgroundColor = "rgb(" + properties.color.red + "," + properties.color.green + "," + properties.color.blue + ")"; + elColorControlVariant2.style.backgroundColor = "rgb(" + properties.color.red + "," + + properties.color.green + "," + properties.color.blue + ")"; } - if (properties.type == "Model") { + if (properties.type === "Model") { elModelURL.value = properties.modelURL; elShapeType.value = properties.shapeType; setDropdownText(elShapeType); @@ -998,24 +974,26 @@ function loaded() { setTextareaScrolling(elModelTextures); elModelOriginalTextures.value = properties.originalTextures; setTextareaScrolling(elModelOriginalTextures); - } else if (properties.type == "Web") { + } else if (properties.type === "Web") { elWebSourceURL.value = properties.sourceUrl; elWebDPI.value = properties.dpi; - } else if (properties.type == "Text") { + } else if (properties.type === "Text") { elTextText.value = properties.text; elTextLineHeight.value = properties.lineHeight.toFixed(4); elTextFaceCamera.checked = properties.faceCamera; - elTextTextColor.style.backgroundColor = "rgb(" + properties.textColor.red + "," + properties.textColor.green + "," + properties.textColor.blue + ")"; + elTextTextColor.style.backgroundColor = "rgb(" + properties.textColor.red + "," + + properties.textColor.green + "," + properties.textColor.blue + ")"; elTextTextColorRed.value = properties.textColor.red; elTextTextColorGreen.value = properties.textColor.green; elTextTextColorBlue.value = properties.textColor.blue; elTextBackgroundColorRed.value = properties.backgroundColor.red; elTextBackgroundColorGreen.value = properties.backgroundColor.green; elTextBackgroundColorBlue.value = properties.backgroundColor.blue; - } else if (properties.type == "Light") { + } else if (properties.type === "Light") { elLightSpotLight.checked = properties.isSpotlight; - elLightColor.style.backgroundColor = "rgb(" + properties.color.red + "," + properties.color.green + "," + properties.color.blue + ")"; + elLightColor.style.backgroundColor = "rgb(" + properties.color.red + "," + + properties.color.green + "," + properties.color.blue + ")"; elLightColorRed.value = properties.color.red; elLightColorGreen.value = properties.color.green; elLightColorBlue.value = properties.color.blue; @@ -1024,9 +1002,10 @@ function loaded() { elLightFalloffRadius.value = properties.falloffRadius.toFixed(1); elLightExponent.value = properties.exponent.toFixed(2); elLightCutoff.value = properties.cutoff.toFixed(2); - } else if (properties.type == "Zone") { + } else if (properties.type === "Zone") { elZoneStageSunModelEnabled.checked = properties.stage.sunModelEnabled; - elZoneKeyLightColor.style.backgroundColor = "rgb(" + properties.keyLight.color.red + "," + properties.keyLight.color.green + "," + properties.keyLight.color.blue + ")"; + elZoneKeyLightColor.style.backgroundColor = "rgb(" + properties.keyLight.color.red + "," + + properties.keyLight.color.green + "," + properties.keyLight.color.blue + ")"; elZoneKeyLightColorRed.value = properties.keyLight.color.red; elZoneKeyLightColorGreen.value = properties.keyLight.color.green; elZoneKeyLightColorBlue.value = properties.keyLight.color.blue; @@ -1036,9 +1015,9 @@ function loaded() { elZoneKeyLightDirectionY.value = properties.keyLight.direction.y.toFixed(2); elZoneKeyLightAmbientURL.value = properties.keyLight.ambientURL; - elZoneHazeModeInherit.checked = (properties.hazeMode == 'inherit'); - elZoneHazeModeDisabled.checked = (properties.hazeMode == 'disabled'); - elZoneHazeModeEnabled.checked = (properties.hazeMode == 'enabled'); + elZoneHazeModeInherit.checked = (properties.hazeMode === 'inherit'); + elZoneHazeModeDisabled.checked = (properties.hazeMode === 'disabled'); + elZoneHazeModeEnabled.checked = (properties.hazeMode === 'enabled'); elZoneHazeRange.value = properties.haze.hazeRange.toFixed(0); elZoneHazeColor.style.backgroundColor = "rgb(" + @@ -1069,10 +1048,6 @@ function loaded() { elZoneHazeBackgroundBlend.value = properties.haze.hazeBackgroundBlend.toFixed(2); -// elZoneHazeAttenuateKeyLight.checked = properties.haze.hazeAttenuateKeyLight; -// elZoneHazeKeyLightRange.value = properties.haze.hazeKeyLightRange.toFixed(0); -// elZoneHazeKeyLightAltitude.value = properties.haze.hazeKeyLightAltitude.toFixed(0); - elZoneStageLatitude.value = properties.stage.latitude.toFixed(2); elZoneStageLongitude.value = properties.stage.longitude.toFixed(2); elZoneStageAltitude.value = properties.stage.altitude.toFixed(2); @@ -1085,7 +1060,8 @@ function loaded() { elZoneBackgroundMode.value = properties.backgroundMode; setDropdownText(elZoneBackgroundMode); - elZoneSkyboxColor.style.backgroundColor = "rgb(" + properties.skybox.color.red + "," + properties.skybox.color.green + "," + properties.skybox.color.blue + ")"; + elZoneSkyboxColor.style.backgroundColor = "rgb(" + properties.skybox.color.red + "," + + properties.skybox.color.green + "," + properties.skybox.color.blue + ")"; elZoneSkyboxColorRed.value = properties.skybox.color.red; elZoneSkyboxColorGreen.value = properties.skybox.color.green; elZoneSkyboxColorBlue.value = properties.skybox.color.blue; @@ -1095,8 +1071,9 @@ function loaded() { elZoneGhostingAllowed.checked = properties.ghostingAllowed; elZoneFilterURL.value = properties.filterURL; - showElements(document.getElementsByClassName('skybox-section'), elZoneBackgroundMode.value == 'skybox'); - } else if (properties.type == "PolyVox") { + showElements(document.getElementsByClassName('skybox-section'), + elZoneBackgroundMode.value === 'skybox'); + } else if (properties.type === "PolyVox") { elVoxelVolumeSizeX.value = properties.voxelVolumeSize.x.toFixed(2); elVoxelVolumeSizeY.value = properties.voxelVolumeSize.y.toFixed(2); elVoxelVolumeSizeZ.value = properties.voxelVolumeSize.z.toFixed(2); @@ -1121,7 +1098,7 @@ function loaded() { activeElement.select(); } } - clearSelection(); + clearSelection(); } }); } @@ -1228,29 +1205,30 @@ function loaded() { var checked = event.target.checked; if (checked) { multiDataUpdater("grabbableKey", { - cloneLifetime: elCloneableLifetime, - cloneLimit: elCloneableLimit, - cloneDynamic: elCloneableDynamic, - cloneAvatarEntity: elCloneableAvatarEntity, - cloneable: event.target, - grabbable: null - }, elUserData, {}); + cloneLifetime: elCloneableLifetime, + cloneLimit: elCloneableLimit, + cloneDynamic: elCloneableDynamic, + cloneAvatarEntity: elCloneableAvatarEntity, + cloneable: event.target, + grabbable: null + }, elUserData, {}); elCloneableGroup.style.display = "block"; updateProperty('dynamic', false); } else { multiDataUpdater("grabbableKey", { - cloneLifetime: null, - cloneLimit: null, - cloneDynamic: null, - cloneAvatarEntity: null, - cloneable: false - }, elUserData, {}); + cloneLifetime: null, + cloneLimit: null, + cloneDynamic: null, + cloneAvatarEntity: null, + cloneable: false + }, elUserData, {}); elCloneableGroup.style.display = "none"; } }); var numberListener = function (event) { - userDataChanger("grabbableKey", event.target.getAttribute("data-user-data-type"), parseInt(event.target.value), elUserData, false); + userDataChanger("grabbableKey", + event.target.getAttribute("data-user-data-type"), parseInt(event.target.value), elUserData, false); }; elCloneableLifetime.addEventListener('change', numberListener); elCloneableLimit.addEventListener('change', numberListener); @@ -1279,7 +1257,7 @@ function loaded() { showUserDataTextArea(); showNewJSONEditorButton(); hideSaveUserDataButton(); - updateProperty('userData', elUserData.value) + updateProperty('userData', elUserData.value); }); elSaveUserData.addEventListener("click", function() { @@ -1303,24 +1281,6 @@ function loaded() { elColorRed.addEventListener('change', colorChangeFunction); elColorGreen.addEventListener('change', colorChangeFunction); elColorBlue.addEventListener('change', colorChangeFunction); - colorPickers.push($('#property-color-control1').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - onShow: function(colpick) { - $('#property-color-control1').attr('active', 'true'); - }, - onHide: function(colpick) { - $('#property-color-control1').attr('active', 'false'); - }, - onSubmit: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - $(el).colpickHide(); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); - // Keep the companion control in sync - elColorControl2.style.backgroundColor = "rgb(" + rgb.r + "," + rgb.g + "," + rgb.b + ")"; - } - })); colorPickers.push($('#property-color-control2').colpick({ colorScheme: 'dark', layout: 'hex', @@ -1335,9 +1295,6 @@ function loaded() { $(el).css('background-color', '#' + hex); $(el).colpickHide(); emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); - // Keep the companion control in sync - elColorControl1.style.backgroundColor = "rgb(" + rgb.r + "," + rgb.g + "," + rgb.b + ")"; - } })); @@ -1380,14 +1337,18 @@ function loaded() { elCompoundShapeURL.addEventListener('change', createEmitTextPropertyUpdateFunction('compoundShapeURL')); elModelAnimationURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('animation', 'url')); - elModelAnimationPlaying.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'running')); + elModelAnimationPlaying.addEventListener('change',createEmitGroupCheckedPropertyUpdateFunction('animation', 'running')); elModelAnimationFPS.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'fps')); - elModelAnimationFrame.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'currentFrame')); - elModelAnimationFirstFrame.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'firstFrame')); - elModelAnimationLastFrame.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'lastFrame')); + elModelAnimationFrame.addEventListener('change', + createEmitGroupNumberPropertyUpdateFunction('animation', 'currentFrame')); + elModelAnimationFirstFrame.addEventListener('change', + createEmitGroupNumberPropertyUpdateFunction('animation', 'firstFrame')); + elModelAnimationLastFrame.addEventListener('change', + createEmitGroupNumberPropertyUpdateFunction('animation', 'lastFrame')); elModelAnimationLoop.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'loop')); elModelAnimationHold.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'hold')); - elModelAnimationAllowTranslation.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'allowTranslation')); + elModelAnimationAllowTranslation.addEventListener('change', + createEmitGroupCheckedPropertyUpdateFunction('animation', 'allowTranslation')); elModelTextures.addEventListener('change', createEmitTextPropertyUpdateFunction('textures')); @@ -1439,7 +1400,8 @@ function loaded() { } })); - elZoneStageSunModelEnabled.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('stage', 'sunModelEnabled')); + elZoneStageSunModelEnabled.addEventListener('change', + createEmitGroupCheckedPropertyUpdateFunction('stage', 'sunModelEnabled')); colorPickers.push($('#property-zone-key-light-color').colpick({ colorScheme: 'dark', layout: 'hex', @@ -1456,18 +1418,26 @@ function loaded() { emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b, 'keyLight'); } })); - var zoneKeyLightColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('keyLight', 'color', elZoneKeyLightColorRed, elZoneKeyLightColorGreen, elZoneKeyLightColorBlue); + var zoneKeyLightColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('keyLight', 'color', + elZoneKeyLightColorRed, elZoneKeyLightColorGreen, elZoneKeyLightColorBlue); elZoneKeyLightColorRed.addEventListener('change', zoneKeyLightColorChangeFunction); elZoneKeyLightColorGreen.addEventListener('change', zoneKeyLightColorChangeFunction); elZoneKeyLightColorBlue.addEventListener('change', zoneKeyLightColorChangeFunction); - elZoneKeyLightIntensity.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('keyLight', 'intensity')); - elZoneKeyLightAmbientIntensity.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('keyLight', 'ambientIntensity')); - elZoneKeyLightAmbientURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('keyLight', 'ambientURL')); - var zoneKeyLightDirectionChangeFunction = createEmitGroupVec3PropertyUpdateFunction('keyLight', 'direction', elZoneKeyLightDirectionX, elZoneKeyLightDirectionY); + elZoneKeyLightIntensity.addEventListener('change', + createEmitGroupNumberPropertyUpdateFunction('keyLight', 'intensity')); + elZoneKeyLightAmbientIntensity.addEventListener('change', + createEmitGroupNumberPropertyUpdateFunction('keyLight', 'ambientIntensity')); + elZoneKeyLightAmbientURL.addEventListener('change', + createEmitGroupTextPropertyUpdateFunction('keyLight', 'ambientURL')); + var zoneKeyLightDirectionChangeFunction = + createEmitGroupVec3PropertyUpdateFunction('keyLight', 'direction', + elZoneKeyLightDirectionX, elZoneKeyLightDirectionY); elZoneKeyLightDirectionX.addEventListener('change', zoneKeyLightDirectionChangeFunction); elZoneKeyLightDirectionY.addEventListener('change', zoneKeyLightDirectionChangeFunction); - var hazeModeChanged = createZoneComponentModeChangedFunction('hazeMode', elZoneHazeModeInherit, elZoneHazeModeDisabled, elZoneHazeModeEnabled) + var hazeModeChanged = + createZoneComponentModeChangedFunction('hazeMode', elZoneHazeModeInherit, + elZoneHazeModeDisabled, elZoneHazeModeEnabled); elZoneHazeModeInherit.addEventListener('change', hazeModeChanged); elZoneHazeModeDisabled.addEventListener('change', hazeModeChanged); elZoneHazeModeEnabled.addEventListener('change', hazeModeChanged); @@ -1524,23 +1494,23 @@ function loaded() { elZoneHazeGlareColorGreen.addEventListener('change', zoneHazeGlareColorChangeFunction); elZoneHazeGlareColorBlue.addEventListener('change', zoneHazeGlareColorChangeFunction); - elZoneHazeEnableGlare.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('haze', 'hazeEnableGlare')); + elZoneHazeEnableGlare.addEventListener('change', + createEmitGroupCheckedPropertyUpdateFunction('haze', 'hazeEnableGlare')); elZonehazeGlareAngle.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeGlareAngle')); - elZoneHazeAltitudeEffect.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('haze', 'hazeAltitudeEffect')); + elZoneHazeAltitudeEffect.addEventListener('change', + createEmitGroupCheckedPropertyUpdateFunction('haze', 'hazeAltitudeEffect')); elZoneHazeCeiling.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeCeiling')); elZoneHazeBaseRef.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeBaseRef')); - elZoneHazeBackgroundBlend.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeBackgroundBlend')); - -// elZoneHazeAttenuateKeyLight.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('haze', 'hazeAttenuateKeyLight')); -// elZoneHazeKeyLightRange.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeKeyLightRange')); -// elZoneHazeKeyLightAltitude.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeKeyLightAltitude')); + elZoneHazeBackgroundBlend.addEventListener('change', + createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeBackgroundBlend')); elZoneStageLatitude.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage', 'latitude')); elZoneStageLongitude.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage', 'longitude')); elZoneStageAltitude.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage', 'altitude')); - elZoneStageAutomaticHourDay.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('stage', 'automaticHourDay')); + elZoneStageAutomaticHourDay.addEventListener('change', + createEmitGroupCheckedPropertyUpdateFunction('stage', 'automaticHourDay')); elZoneStageDay.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage', 'day')); elZoneStageHour.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage', 'hour')); @@ -1587,26 +1557,26 @@ function loaded() { elMoveSelectionToGrid.addEventListener("click", function() { EventBridge.emitWebEvent(JSON.stringify({ type: "action", - action: "moveSelectionToGrid", + action: "moveSelectionToGrid" })); }); elMoveAllToGrid.addEventListener("click", function() { EventBridge.emitWebEvent(JSON.stringify({ type: "action", - action: "moveAllToGrid", + action: "moveAllToGrid" })); }); elResetToNaturalDimensions.addEventListener("click", function() { EventBridge.emitWebEvent(JSON.stringify({ type: "action", - action: "resetToNaturalDimensions", + action: "resetToNaturalDimensions" })); }); elRescaleDimensionsButton.addEventListener("click", function() { EventBridge.emitWebEvent(JSON.stringify({ type: "action", action: "rescaleDimensions", - percentage: parseFloat(elRescaleDimensionsPct.value), + percentage: parseFloat(elRescaleDimensionsPct.value) })); }); elReloadScriptsButton.addEventListener("click", function() { @@ -1625,20 +1595,20 @@ function loaded() { }); document.addEventListener("keydown", function (keyDown) { - if (keyDown.keyCode === KEY_P && keyDown.ctrlKey) { - if (keyDown.shiftKey) { - EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); - } else { - EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' })); - } - } + if (keyDown.keyCode === KEY_P && keyDown.ctrlKey) { + if (keyDown.shiftKey) { + EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); + } else { + EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' })); + } + } }); window.onblur = function() { // Fake a change event var ev = document.createEvent("HTMLEvents"); ev.initEvent("change", true, true); document.activeElement.dispatchEvent(ev); - } + }; // For input and textarea elements, select all of the text on focus // WebKit-based browsers, such as is used with QWebView, have a quirk @@ -1653,13 +1623,17 @@ function loaded() { for (var i = 0; i < els.length; i++) { var clicked = false; var originalText; + // TODO FIXME: (JSHint) Functions declared within loops referencing + // an outer scoped variable may lead to confusing semantics. els[i].onfocus = function(e) { originalText = this.value; this.select(); clicked = false; }; + // TODO FIXME: (JSHint) Functions declared within loops referencing + // an outer scoped variable may lead to confusing semantics. els[i].onmouseup = function(e) { - if (!clicked && originalText == this.value) { + if (!clicked && originalText === this.value) { e.preventDefault(); } clicked = true; @@ -1674,15 +1648,15 @@ function loaded() { var toggleCollapsedEvent = function(event) { var element = event.target.parentNode.parentNode; var isCollapsed = element.dataset.collapsed !== "true"; - element.dataset.collapsed = isCollapsed ? "true" : false + element.dataset.collapsed = isCollapsed ? "true" : false; element.setAttribute("collapsed", isCollapsed ? "true" : "false"); element.getElementsByTagName("span")[0].textContent = isCollapsed ? "L" : "M"; }; - for (var i = 0, length = elCollapsible.length; i < length; i++) { - var element = elCollapsible[i]; - element.addEventListener("click", toggleCollapsedEvent, true); - }; + for (var collapseIndex = 0, numCollapsibles = elCollapsible.length; collapseIndex < numCollapsibles; ++collapseIndex) { + var curCollapsibleElement = elCollapsible[collapseIndex]; + curCollapsibleElement.addEventListener("click", toggleCollapsedEvent, true); + } // Textarea scrollbars @@ -1690,17 +1664,17 @@ function loaded() { var textareaOnChangeEvent = function(event) { setTextareaScrolling(event.target); - } + }; - for (var i = 0, length = elTextareas.length; i < length; i++) { - var element = elTextareas[i]; - setTextareaScrolling(element); - element.addEventListener("input", textareaOnChangeEvent, false); - element.addEventListener("change", textareaOnChangeEvent, false); + for (var textAreaIndex = 0, numTextAreas = elTextareas.length; textAreaIndex < numTextAreas; ++textAreaIndex) { + var curTextAreaElement = elTextareas[textAreaIndex]; + setTextareaScrolling(curTextAreaElement); + curTextAreaElement.addEventListener("input", textareaOnChangeEvent, false); + curTextAreaElement.addEventListener("change", textareaOnChangeEvent, false); /* FIXME: Detect and update textarea scrolling attribute on resize. Unfortunately textarea doesn't have a resize event; mouseup is a partial stand-in but doesn't handle resizing if mouse moves outside textarea rectangle. */ - element.addEventListener("mouseup", textareaOnChangeEvent, false); - }; + curTextAreaElement.addEventListener("mouseup", textareaOnChangeEvent, false); + } // Dropdowns // For each dropdown the following replacement is created in place of the oriringal dropdown... @@ -1749,22 +1723,23 @@ function loaded() { } var elDropdowns = document.getElementsByTagName("select"); - for (var i = 0; i < elDropdowns.length; i++) { - var options = elDropdowns[i].getElementsByTagName("option"); + for (var dropDownIndex = 0; dropDownIndex < elDropdowns.length; ++dropDownIndex) { + var options = elDropdowns[dropDownIndex].getElementsByTagName("option"); var selectedOption = 0; - for (var j = 0; j < options.length; j++) { - if (options[j].getAttribute("selected") === "selected") { - selectedOption = j; + for (var optionIndex = 0; optionIndex < options.length; ++optionIndex) { + if (options[optionIndex].getAttribute("selected") === "selected") { + selectedOption = optionIndex; + // TODO: Shouldn't there be a break here? } } - var div = elDropdowns[i].parentNode; + var div = elDropdowns[dropDownIndex].parentNode; var dl = document.createElement("dl"); div.appendChild(dl); var dt = document.createElement("dt"); - dt.name = elDropdowns[i].name; - dt.id = elDropdowns[i].id; + dt.name = elDropdowns[dropDownIndex].name; + dt.id = elDropdowns[dropDownIndex].id; dt.addEventListener("click", toggleDropdown, true); dl.appendChild(dt); @@ -1773,9 +1748,9 @@ function loaded() { span.textContent = options[selectedOption].firstChild.textContent; dt.appendChild(span); - var span = document.createElement("span"); - span.textContent = "5"; // caratDn - dt.appendChild(span); + var spanCaratDown = document.createElement("span"); + spanCaratDown.textContent = "5"; // caratDn + dt.appendChild(spanCaratDown); var dd = document.createElement("dd"); dl.appendChild(dd); @@ -1783,10 +1758,10 @@ function loaded() { var ul = document.createElement("ul"); dd.appendChild(ul); - for (var j = 0; j < options.length; j++) { + for (var listOptionIndex = 0; listOptionIndex < options.length; ++listOptionIndex) { var li = document.createElement("li"); - li.setAttribute("value", options[j].value); - li.textContent = options[j].firstChild.textContent; + li.setAttribute("value", options[listOptionIndex].value); + li.textContent = options[listOptionIndex].firstChild.textContent; li.addEventListener("click", setDropdownValue); ul.appendChild(li); } diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 1346bcd750..878c3b51f1 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -327,6 +327,19 @@ }); } + // fix for 10108 - marketplace category cannot scroll + function injectAddScrollbarToCategories() { + $('#categories-dropdown').on('show.bs.dropdown', function () { + $('body > div.container').css('display', 'none') + $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': 'auto', 'height': 'calc(100vh - 110px)' }) + }); + + $('#categories-dropdown').on('hide.bs.dropdown', function () { + $('body > div.container').css('display', '') + $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': '', 'height': '' }) + }); + } + function injectHiFiCode() { if (commerceMode) { maybeAddLogInButton(); @@ -358,6 +371,7 @@ } injectUnfocusOnSearch(); + injectAddScrollbarToCategories(); } function injectHiFiItemPageCode() { diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index 57b17f3d72..4217ec503e 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -118,15 +118,16 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) { Overlays.deleteOverlay(this.webOverlayID); } - var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2) * (1 / sensorScaleFactor); - var WEB_ENTITY_Y_OFFSET = 0.004 * (1 / sensorScaleFactor); - + var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.0) / sensorScaleFactor; + var WEB_ENTITY_Y_OFFSET = 0.004; + var screenWidth = 0.82 * tabletWidth; + var screenHeight = 0.81 * tabletHeight; this.webOverlayID = Overlays.addOverlay("web3d", { name: "WebTablet Web", url: url, localPosition: { x: 0, y: WEB_ENTITY_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET }, localRotation: Quat.angleAxis(180, Y_AXIS), - resolution: this.getTabletTextureResolution(), + dimensions: {x: screenWidth, y: screenHeight, z: 0.1}, dpi: tabletDpi, color: { red: 255, green: 255, blue: 255 }, alpha: 1.0, @@ -136,12 +137,15 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) { visible: visible }); - var HOME_BUTTON_Y_OFFSET = ((tabletHeight / 2) - (tabletHeight / 20)) * (1 / sensorScaleFactor); - this.homeButtonID = Overlays.addOverlay("sphere", { + var HOME_BUTTON_Y_OFFSET = ((tabletHeight / 2) - (tabletHeight / 20)) * (1 / sensorScaleFactor) - 0.003; + // FIXME: Circle3D overlays currently at the wrong dimensions, so we need to account for that here + var homeButtonDim = 4.0 * tabletScaleFactor / 3.0; + this.homeButtonID = Overlays.addOverlay("circle3d", { name: "homeButton", - localPosition: {x: -0.001, y: -HOME_BUTTON_Y_OFFSET, z: 0.0}, - localRotation: {x: 0, y: 1, z: 0, w: 0}, - dimensions: { x: 4 * tabletScaleFactor, y: 4 * tabletScaleFactor, z: 4 * tabletScaleFactor}, + localPosition: { x: 0.0, y: -HOME_BUTTON_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET }, + localRotation: { x: 0, y: 1, z: 0, w: 0}, + dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }, + solid: true, alpha: 0.0, visible: visible, drawInFront: false, @@ -151,14 +155,14 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) { this.homeButtonHighlightID = Overlays.addOverlay("circle3d", { name: "homeButtonHighlight", - localPosition: { x: 0, y: -HOME_BUTTON_Y_OFFSET + 0.003, z: -0.0158 }, + localPosition: { x: 0, y: -HOME_BUTTON_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET }, localRotation: { x: 0, y: 1, z: 0, w: 0 }, - dimensions: { x: 4 * tabletScaleFactor, y: 4 * tabletScaleFactor, z: 4 * tabletScaleFactor }, + dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }, + color: { red: 255, green: 255, blue: 255 }, solid: true, innerRadius: 0.9, ignoreIntersection: true, alpha: 1.0, - color: { red: 255, green: 255, blue: 255 }, visible: visible, drawInFront: false, parentID: this.tabletEntityID, @@ -265,11 +269,16 @@ WebTablet.prototype.setLandscape = function(newLandscapeValue) { this.landscape = newLandscapeValue; Overlays.editOverlay(this.tabletEntityID, - { rotation: this.landscape ? Quat.multiply(Camera.orientation, ROT_LANDSCAPE) : - Quat.multiply(Camera.orientation, ROT_Y_180) }); + { rotation: Quat.multiply(Camera.orientation, this.landscape ? ROT_LANDSCAPE : ROT_Y_180) }); + + var tabletWidth = getTabletWidthFromSettings() * MyAvatar.sensorToWorldScale; + var tabletScaleFactor = tabletWidth / TABLET_NATURAL_DIMENSIONS.x; + var tabletHeight = TABLET_NATURAL_DIMENSIONS.y * tabletScaleFactor; + var screenWidth = 0.82 * tabletWidth; + var screenHeight = 0.81 * tabletHeight; Overlays.editOverlay(this.webOverlayID, { - resolution: this.getTabletTextureResolution(), - rotation: Quat.multiply(Camera.orientation, ROT_LANDSCAPE_WINDOW) + rotation: Quat.multiply(Camera.orientation, ROT_LANDSCAPE_WINDOW), + dimensions: {x: this.landscape ? screenHeight : screenWidth, y: this.landscape ? screenWidth : screenHeight, z: 0.1} }); }; @@ -505,31 +514,17 @@ WebTablet.prototype.getPosition = function () { }; WebTablet.prototype.mousePressEvent = function (event) { - var pickRay = Camera.computePickRay(event.x, event.y); - var entityPickResults; - entityPickResults = Overlays.findRayIntersection(pickRay, true, [this.tabletEntityID]); - if (entityPickResults.intersects && (entityPickResults.entityID === this.tabletEntityID || - entityPickResults.overlayID === this.tabletEntityID)) { - var overlayPickResults = Overlays.findRayIntersection(pickRay, true, [this.webOverlayID, this.homeButtonID], []); - if (overlayPickResults.intersects && overlayPickResults.overlayID === this.homeButtonID) { - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - var onHomeScreen = tablet.onHomeScreen(); - var isMessageOpen = tablet.isMessageDialogOpen(); - if (onHomeScreen) { - if (isMessageOpen === false) { - HMD.closeTablet(); - } - } else { - if (isMessageOpen === false) { - tablet.gotoHomeScreen(); - this.setHomeButtonTexture(); - } + if (!HMD.active) { + var pickRay = Camera.computePickRay(event.x, event.y); + var tabletBackPickResults = Overlays.findRayIntersection(pickRay, true, [this.tabletEntityID]); + if (tabletBackPickResults.intersects) { + var overlayPickResults = Overlays.findRayIntersection(pickRay, true, [this.webOverlayID, this.homeButtonID]); + if (!overlayPickResults.intersects) { + this.dragging = true; + var invCameraXform = new Xform(Camera.orientation, Camera.position).inv(); + this.initialLocalIntersectionPoint = invCameraXform.xformPoint(tabletBackPickResults.intersection); + this.initialLocalPosition = Overlays.getProperty(this.tabletEntityID, "localPosition"); } - } else if (!HMD.active && (!overlayPickResults.intersects || overlayPickResults.overlayID !== this.webOverlayID)) { - this.dragging = true; - var invCameraXform = new Xform(Camera.orientation, Camera.position).inv(); - this.initialLocalIntersectionPoint = invCameraXform.xformPoint(entityPickResults.intersection); - this.initialLocalPosition = Overlays.getProperty(this.tabletEntityID, "localPosition"); } } }; diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index fd0db91fec..e0971201e6 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -272,22 +272,8 @@ projectOntoEntityXYPlane = function (entityID, worldPos, props) { projectOntoOverlayXYPlane = function projectOntoOverlayXYPlane(overlayID, worldPos) { var position = Overlays.getProperty(overlayID, "position"); var rotation = Overlays.getProperty(overlayID, "rotation"); - var dimensions; - - var dpi = Overlays.getProperty(overlayID, "dpi"); - if (dpi) { - // Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property is used as a scale. - var resolution = Overlays.getProperty(overlayID, "resolution"); - resolution.z = 1; // Circumvent divide-by-zero. - var scale = Overlays.getProperty(overlayID, "dimensions"); - scale.z = 0.01; // overlay dimensions are 2D, not 3D. - dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); - } else { - dimensions = Overlays.getProperty(overlayID, "dimensions"); - if (dimensions.z) { - dimensions.z = 0.01; // overlay dimensions are 2D, not 3D. - } - } + var dimensions = Overlays.getProperty(overlayID, "dimensions"); + dimensions.z = 0.01; // we are projecting onto the XY plane of the overlay, so ignore the z dimension return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT); }; diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index d947a1d397..b8ba146757 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1031,6 +1031,71 @@ SelectionDisplay = (function() { that.updateHandles(); }; + // Function: Calculate New Bound Extremes + // uses dot product to discover new top and bottom on the new referential (max and min) + that.calculateNewBoundExtremes = function(boundPointList, referenceVector) { + + if (boundPointList.length < 2) { + return [null, null]; + } + + var refMax = boundPointList[0]; + var refMin = boundPointList[1]; + + var dotMax = Vec3.dot(boundPointList[0], referenceVector); + var dotMin = Vec3.dot(boundPointList[1], referenceVector); + + if (dotMin > dotMax) { + dotMax = dotMin; + dotMin = Vec3.dot(boundPointList[0], referenceVector); + refMax = boundPointList[1]; + refMin = boundPointList[0]; + } + + for (var i = 2; i < boundPointList.length ; i++) { + var dotAux = Vec3.dot(boundPointList[i], referenceVector); + if (dotAux > dotMax) { + dotMax = dotAux; + refMax = boundPointList[i]; + } else if (dotAux < dotMin) { + dotMin = dotAux; + refMin = boundPointList[i]; + } + } + return [refMin, refMax]; + } + + // Function: Project Bounding Box Points + // Projects all 6 bounding box points: Top, Bottom, Left, Right, Near, Far (assumes center 0,0,0) onto + // one of the basis of the new avatar referencial + // dimensions - dimensions of the AABB (axis aligned bounding box) on the standard basis + // [1, 0, 0], [0, 1, 0], [0, 0, 1] + // v - projection vector + // rotateHandleOffset - offset for the rotation handle gizmo position + that.projectBoundingBoxPoints = function(dimensions, v, rotateHandleOffset) { + var projT_v = Vec3.dot(Vec3.multiply((dimensions.y / 2) + rotateHandleOffset, Vec3.UNIT_Y), v); + projT_v = Vec3.multiply(projT_v, v); + + var projB_v = Vec3.dot(Vec3.multiply(-(dimensions.y / 2) - rotateHandleOffset, Vec3.UNIT_Y), v); + projB_v = Vec3.multiply(projB_v, v); + + var projL_v = Vec3.dot(Vec3.multiply((dimensions.x / 2) + rotateHandleOffset, Vec3.UNIT_X), v); + projL_v = Vec3.multiply(projL_v, v); + + var projR_v = Vec3.dot(Vec3.multiply(-1.0 * (dimensions.x / 2) - 1.0 * rotateHandleOffset, Vec3.UNIT_X), v); + projR_v = Vec3.multiply(projR_v, v); + + var projN_v = Vec3.dot(Vec3.multiply((dimensions.z / 2) + rotateHandleOffset, Vec3.FRONT), v); + projN_v = Vec3.multiply(projN_v, v); + + var projF_v = Vec3.dot(Vec3.multiply(-1.0 * (dimensions.z / 2) - 1.0 * rotateHandleOffset, Vec3.FRONT), v); + projF_v = Vec3.multiply(projF_v, v); + + var projList = [projT_v, projB_v, projL_v, projR_v, projN_v, projF_v]; + + return that.calculateNewBoundExtremes(projList, v); + }; + // FUNCTION: UPDATE ROTATION HANDLES that.updateRotationHandles = function() { var diagonal = (Vec3.length(SelectionManager.worldDimensions) / 2) * 1.1; @@ -1043,10 +1108,10 @@ SelectionDisplay = (function() { } else { outerAlpha = 0.5; } - + // prev 0.05 var rotateHandleOffset = 0.05; - var top, far, left, bottom, near, right, boundsCenter, objectCenter, BLN, BRN, BLF, TLN, TRN, TLF, TRF; + var boundsCenter, objectCenter; var dimensions, rotation; if (spaceMode === SPACE_LOCAL) { @@ -1058,280 +1123,66 @@ SelectionDisplay = (function() { dimensions = SelectionManager.worldDimensions; var position = objectCenter; - top = objectCenter.y + (dimensions.y / 2); - far = objectCenter.z + (dimensions.z / 2); - left = objectCenter.x + (dimensions.x / 2); - - bottom = objectCenter.y - (dimensions.y / 2); - near = objectCenter.z - (dimensions.z / 2); - right = objectCenter.x - (dimensions.x / 2); - boundsCenter = objectCenter; var yawCorner; var pitchCorner; var rollCorner; - // determine which bottom corner we are closest to - /*------------------------------ - example: - - BRF +--------+ BLF - | | - | | - BRN +--------+ BLN - - * - - ------------------------------*/ - var cameraPosition = Camera.getPosition(); - if (cameraPosition.x > objectCenter.x) { - // must be BRF or BRN - if (cameraPosition.z < objectCenter.z) { + var look = Vec3.normalize(Vec3.subtract(cameraPosition, objectCenter)); - yawHandleRotation = Quat.fromVec3Degrees({ - x: 270, - y: 90, - z: 0 - }); - pitchHandleRotation = Quat.fromVec3Degrees({ - x: 0, - y: 90, - z: 0 - }); - rollHandleRotation = Quat.fromVec3Degrees({ - x: 0, - y: 0, - z: 0 - }); + // place yaw, pitch and roll rotations on the avatar referential + + var avatarReferential = Quat.multiply(MyAvatar.orientation, Quat.fromVec3Degrees({ + x: 0, + y: 180, + z: 0 + })); + var upVector = Quat.getUp(avatarReferential); + var rightVector = Vec3.multiply(-1, Quat.getRight(avatarReferential)); + var frontVector = Quat.getFront(avatarReferential); + + // project all 6 bounding box points: Top, Bottom, Left, Right, Near, Far (assumes center 0,0,0) + // onto the new avatar referential - yawCorner = { - x: left + rotateHandleOffset, - y: bottom - rotateHandleOffset, - z: near - rotateHandleOffset - }; - - pitchCorner = { - x: right - rotateHandleOffset, - y: top + rotateHandleOffset, - z: near - rotateHandleOffset - }; - - rollCorner = { - x: left + rotateHandleOffset, - y: top + rotateHandleOffset, - z: far + rotateHandleOffset - }; - - yawCenter = { - x: boundsCenter.x, - y: bottom, - z: boundsCenter.z - }; - pitchCenter = { - x: right, - y: boundsCenter.y, - z: boundsCenter.z - }; - rollCenter = { - x: boundsCenter.x, - y: boundsCenter.y, - z: far - }; - - - Overlays.editOverlay(pitchHandle, { - url: ROTATE_ARROW_WEST_SOUTH_URL - }); - Overlays.editOverlay(rollHandle, { - url: ROTATE_ARROW_WEST_SOUTH_URL - }); - - - } else { - - yawHandleRotation = Quat.fromVec3Degrees({ - x: 270, - y: 0, - z: 0 - }); - pitchHandleRotation = Quat.fromVec3Degrees({ - x: 180, - y: 270, - z: 0 - }); - rollHandleRotation = Quat.fromVec3Degrees({ - x: 0, - y: 0, - z: 90 - }); - - yawCorner = { - x: left + rotateHandleOffset, - y: bottom - rotateHandleOffset, - z: far + rotateHandleOffset - }; - - pitchCorner = { - x: right - rotateHandleOffset, - y: top + rotateHandleOffset, - z: far + rotateHandleOffset - }; - - rollCorner = { - x: left + rotateHandleOffset, - y: top + rotateHandleOffset, - z: near - rotateHandleOffset - }; - - - yawCenter = { - x: boundsCenter.x, - y: bottom, - z: boundsCenter.z - }; - pitchCenter = { - x: right, - y: boundsCenter.y, - z: boundsCenter.z - }; - rollCenter = { - x: boundsCenter.x, - y: boundsCenter.y, - z: near - }; - - Overlays.editOverlay(pitchHandle, { - url: ROTATE_ARROW_WEST_NORTH_URL - }); - Overlays.editOverlay(rollHandle, { - url: ROTATE_ARROW_WEST_NORTH_URL - }); - } - } else { - - // must be BLF or BLN - if (cameraPosition.z < objectCenter.z) { - - yawHandleRotation = Quat.fromVec3Degrees({ - x: 270, - y: 180, - z: 0 - }); - pitchHandleRotation = Quat.fromVec3Degrees({ - x: 90, - y: 0, - z: 90 - }); - rollHandleRotation = Quat.fromVec3Degrees({ - x: 0, - y: 0, - z: 180 - }); - - yawCorner = { - x: right - rotateHandleOffset, - y: bottom - rotateHandleOffset, - z: near - rotateHandleOffset - }; - - pitchCorner = { - x: left + rotateHandleOffset, - y: top + rotateHandleOffset, - z: near - rotateHandleOffset - }; - - rollCorner = { - x: right - rotateHandleOffset, - y: top + rotateHandleOffset, - z: far + rotateHandleOffset - }; - - yawCenter = { - x: boundsCenter.x, - y: bottom, - z: boundsCenter.z - }; - pitchCenter = { - x: left, - y: boundsCenter.y, - z: boundsCenter.z - }; - rollCenter = { - x: boundsCenter.x, - y: boundsCenter.y, - z: far - }; - - Overlays.editOverlay(pitchHandle, { - url: ROTATE_ARROW_WEST_NORTH_URL - }); - Overlays.editOverlay(rollHandle, { - url: ROTATE_ARROW_WEST_NORTH_URL - }); - - } else { - - yawHandleRotation = Quat.fromVec3Degrees({ - x: 270, - y: 270, - z: 0 - }); - pitchHandleRotation = Quat.fromVec3Degrees({ - x: 180, - y: 270, - z: 0 - }); - rollHandleRotation = Quat.fromVec3Degrees({ - x: 0, - y: 0, - z: 180 - }); - - yawCorner = { - x: right - rotateHandleOffset, - y: bottom - rotateHandleOffset, - z: far + rotateHandleOffset - }; - - rollCorner = { - x: right - rotateHandleOffset, - y: top + rotateHandleOffset, - z: near - rotateHandleOffset - }; - - pitchCorner = { - x: left + rotateHandleOffset, - y: top + rotateHandleOffset, - z: far + rotateHandleOffset - }; - - yawCenter = { - x: boundsCenter.x, - y: bottom, - z: boundsCenter.z - }; - rollCenter = { - x: boundsCenter.x, - y: boundsCenter.y, - z: near - }; - pitchCenter = { - x: left, - y: boundsCenter.y, - z: boundsCenter.z - }; - - Overlays.editOverlay(pitchHandle, { - url: ROTATE_ARROW_WEST_NORTH_URL - }); - Overlays.editOverlay(rollHandle, { - url: ROTATE_ARROW_WEST_NORTH_URL - }); - - } - } + // UP + var projUP = that.projectBoundingBoxPoints(dimensions, upVector, rotateHandleOffset); + // RIGHT + var projRIGHT = that.projectBoundingBoxPoints(dimensions, rightVector, rotateHandleOffset); + // FRONT + var projFRONT = that.projectBoundingBoxPoints(dimensions, frontVector, rotateHandleOffset); + + // YAW + yawCenter = Vec3.sum(boundsCenter, projUP[0]); + yawCorner = Vec3.sum(boundsCenter, Vec3.sum(Vec3.sum(projUP[0], projRIGHT[1]), projFRONT[1])); + + yawHandleRotation = Quat.lookAt( + yawCorner, + Vec3.sum(yawCorner, upVector), + Vec3.subtract(yawCenter,yawCorner)); + yawHandleRotation = Quat.multiply(Quat.angleAxis(45, upVector), yawHandleRotation); + + // PTCH + pitchCorner = Vec3.sum(boundsCenter, Vec3.sum(Vec3.sum(projUP[1], projRIGHT[0]), projFRONT[1])); + pitchCenter = Vec3.sum(boundsCenter, projRIGHT[0]); + + pitchHandleRotation = Quat.lookAt( + pitchCorner, + Vec3.sum(pitchCorner, rightVector), + Vec3.subtract(pitchCenter,pitchCorner)); + pitchHandleRotation = Quat.multiply(Quat.angleAxis(45, rightVector), pitchHandleRotation); + + // ROLL + rollCorner = Vec3.sum(boundsCenter, Vec3.sum(Vec3.sum(projUP[1], projRIGHT[1]), projFRONT[0])); + rollCenter = Vec3.sum(boundsCenter, projFRONT[0]); + + rollHandleRotation = Quat.lookAt( + rollCorner, + Vec3.sum(rollCorner, frontVector), + Vec3.subtract(rollCenter,rollCorner)); + rollHandleRotation = Quat.multiply(Quat.angleAxis(45, frontVector), rollHandleRotation); + var rotateHandlesVisible = true; var rotationOverlaysVisible = false; @@ -1382,6 +1233,8 @@ SelectionDisplay = (function() { position: rollCorner, rotation: rollHandleRotation }); + + }; // FUNCTION: UPDATE HANDLE SIZES @@ -3422,7 +3275,7 @@ SelectionDisplay = (function() { y: innerRadius * ROTATION_DISPLAY_SIZE_Y_MULTIPLIER }, lineHeight: innerRadius * ROTATION_DISPLAY_LINE_HEIGHT_MULTIPLIER, - text: normalizeDegrees(angleFromZero) + "°" + text: normalizeDegrees(-angleFromZero) + "°" }; if (wantDebug) { print(" TranslatedPos: " + position.x + ", " + position.y + ", " + position.z); @@ -3483,6 +3336,13 @@ SelectionDisplay = (function() { initialPosition = SelectionManager.worldPosition; rotationNormal = { x: 0, y: 0, z: 0 }; rotationNormal[rotAroundAxis] = 1; + //get the correct axis according to the avatar referencial + var avatarReferential = Quat.multiply(MyAvatar.orientation, Quat.fromVec3Degrees({ + x: 0, + y: 0, + z: 0 + })); + rotationNormal = Vec3.multiplyQbyV(avatarReferential, rotationNormal); // Size the overlays to the current selection size var diagonal = (Vec3.length(SelectionManager.worldDimensions) / 2) * 1.1; @@ -3584,11 +3444,11 @@ SelectionDisplay = (function() { var snapAngle = snapToInner ? innerSnapAngle : 1.0; angleFromZero = Math.floor(angleFromZero / snapAngle) * snapAngle; - var vec3Degrees = { x: 0, y: 0, z: 0 }; - vec3Degrees[rotAroundAxis] = angleFromZero; - var rotChange = Quat.fromVec3Degrees(vec3Degrees); + + var rotChange = Quat.angleAxis(angleFromZero, rotationNormal); updateSelectionsRotation(rotChange); - + //present angle in avatar referencial + angleFromZero = -angleFromZero; updateRotationDegreesOverlay(angleFromZero, handleRotation, rotCenter); // update the rotation display accordingly... diff --git a/scripts/system/libraries/gridTool.js b/scripts/system/libraries/gridTool.js index 2c417a9dde..19d4417a12 100644 --- a/scripts/system/libraries/gridTool.js +++ b/scripts/system/libraries/gridTool.js @@ -242,7 +242,9 @@ GridTool = function(opts) { horizontalGrid.addListener(function(data) { webView.emitScriptEvent(JSON.stringify(data)); - selectionDisplay.updateHandles(); + if (selectionDisplay) { + selectionDisplay.updateHandles(); + } }); webView.webEventReceived.connect(function(data) { diff --git a/scripts/system/libraries/touchEventUtils.js b/scripts/system/libraries/touchEventUtils.js index c9fd825a09..f0f7ec46fe 100644 --- a/scripts/system/libraries/touchEventUtils.js +++ b/scripts/system/libraries/touchEventUtils.js @@ -169,32 +169,12 @@ function calculateTouchTargetFromOverlay(touchTip, overlayID) { // calclulate normalized position var invRot = Quat.inverse(overlayRotation); var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, overlayPosition)); - var dpi = Overlays.getProperty(overlayID, "dpi"); - var dimensions; - if (dpi) { - // Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property - // is used as a scale. - var resolution = Overlays.getProperty(overlayID, "resolution"); - if (resolution === undefined) { - return; - } - resolution.z = 1; // Circumvent divide-by-zero. - var scale = Overlays.getProperty(overlayID, "dimensions"); - if (scale === undefined) { - return; - } - scale.z = 0.01; // overlay dimensions are 2D, not 3D. - dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); - } else { - dimensions = Overlays.getProperty(overlayID, "dimensions"); - if (dimensions === undefined) { - return; - } - if (!dimensions.z) { - dimensions.z = 0.01; // sometimes overlay dimensions are 2D, not 3D. - } + var dimensions = Overlays.getProperty(overlayID, "dimensions"); + if (dimensions === undefined) { + return; } + dimensions.z = 0.01; // we are projecting onto the XY plane of the overlay, so ignore the z dimension var invDimensions = { x: 1 / dimensions.x, y: 1 / dimensions.y, z: 1 / dimensions.z }; var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), DEFAULT_REGISTRATION_POINT); diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index 9706073081..4a1fcdf301 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -185,7 +185,7 @@ logTrace = function(str) { // (the vector that would move the point outside the sphere) // otherwise returns false findSphereHit = function(point, sphereRadius) { - var EPSILON = 0.000001; //smallish positive number - used as margin of error for some computations + var EPSILON = 0.000001; //smallish positive number - used as margin of error for some computations var vectorLength = Vec3.length(point); if (vectorLength < EPSILON) { return true; @@ -400,25 +400,28 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride) }); // update webOverlay - var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2) * sensorScaleOffsetOverride; - var WEB_ENTITY_Y_OFFSET = 0.004 * sensorScaleOffsetOverride; + var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.0) * sensorScaleOffsetOverride; + var WEB_ENTITY_Y_OFFSET = 0.004 * sensorScaleFactor * sensorScaleOffsetOverride; + var screenWidth = 0.82 * tabletWidth; + var screenHeight = 0.81 * tabletHeight; + var landscape = Tablet.getTablet("com.highfidelity.interface.tablet.system").landscape; Overlays.editOverlay(HMD.tabletScreenID, { localPosition: { x: 0, y: WEB_ENTITY_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET }, + dimensions: {x: landscape ? screenHeight : screenWidth, y: landscape ? screenWidth : screenHeight, z: 0.1}, dpi: tabletDpi }); // update homeButton - var HOME_BUTTON_Y_OFFSET = ((tabletHeight / 2) - (tabletHeight / 20)) * sensorScaleOffsetOverride; - var homeButtonDim = 4 * tabletScaleFactor; + var HOME_BUTTON_Y_OFFSET = ((tabletHeight / 2) - (tabletHeight / 20) - 0.003 * sensorScaleFactor) * sensorScaleOffsetOverride; + // FIXME: Circle3D overlays currently at the wrong dimensions, so we need to account for that here + var homeButtonDim = 4.0 * tabletScaleFactor / 3.0; Overlays.editOverlay(HMD.homeButtonID, { - localPosition: {x: -0.001, y: -HOME_BUTTON_Y_OFFSET, z: 0.0}, - dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim} + localPosition: { x: 0, y: -HOME_BUTTON_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET }, + dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim } }); - // Circle3D overlays render at 1.5x their proper dimensions - var highlightDim = homeButtonDim / 3.0; Overlays.editOverlay(HMD.homeButtonHighlightID, { - localPosition: { x: 0, y: -HOME_BUTTON_Y_OFFSET + 0.003, z: -0.0158 }, - dimensions: { x: highlightDim, y: highlightDim, z: highlightDim } + localPosition: { x: 0, y: -HOME_BUTTON_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET }, + dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim } }); }; diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 646e5452df..004375bff7 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -11,9 +11,12 @@ /* global Tablet, Script, HMD, UserActivityLogger, Entities */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +var selectionDisplay = null; // for gridTool.js to ignore + (function () { // BEGIN LOCAL_SCOPE - Script.include("../libraries/WebTablet.js"); + Script.include("/~/system/libraries/WebTablet.js"); + Script.include("/~/system/libraries/gridTool.js"); var METAVERSE_SERVER_URL = Account.metaverseServerURL; var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace"; @@ -110,8 +113,10 @@ var filterText; // Used for updating Purchases QML function onScreenChanged(type, url) { onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; - onCommerceScreen = type === "QML" && (url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH_BASE) !== -1 || url === MARKETPLACE_PURCHASES_QML_PATH || url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); - wireEventBridge(onCommerceScreen); + onWalletScreen = url.indexOf(MARKETPLACE_WALLET_QML_PATH) !== -1; + onCommerceScreen = type === "QML" && (url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH_BASE) !== -1 || url === MARKETPLACE_PURCHASES_QML_PATH + || url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); + wireEventBridge(onMarketplaceScreen || onCommerceScreen || onWalletScreen); if (url === MARKETPLACE_PURCHASES_QML_PATH) { tablet.sendToQml({ @@ -122,7 +127,7 @@ } // for toolbar mode: change button to active when window is first openend, false otherwise. - marketplaceButton.editProperties({ isActive: onMarketplaceScreen || onCommerceScreen }); + marketplaceButton.editProperties({ isActive: (onMarketplaceScreen || onCommerceScreen) && !onWalletScreen }); if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { ContextOverlay.isInMarketplaceInspectionMode = true; } else { @@ -168,6 +173,33 @@ })); } + var grid = new Grid(); + function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { + // Adjust the position such that the bounding box (registration, dimenions, and orientation) lies behind the original + // position in the given direction. + var CORNERS = [ + { x: 0, y: 0, z: 0 }, + { x: 0, y: 0, z: 1 }, + { x: 0, y: 1, z: 0 }, + { x: 0, y: 1, z: 1 }, + { x: 1, y: 0, z: 0 }, + { x: 1, y: 0, z: 1 }, + { x: 1, y: 1, z: 0 }, + { x: 1, y: 1, z: 1 }, + ]; + + // Go through all corners and find least (most negative) distance in front of position. + var distance = 0; + for (var i = 0, length = CORNERS.length; i < length; i++) { + var cornerVector = + Vec3.multiplyQbyV(orientation, Vec3.multiplyVbyV(Vec3.subtract(CORNERS[i], registration), dimensions)); + var cornerDistance = Vec3.dot(cornerVector, direction); + distance = Math.min(cornerDistance, distance); + } + position = Vec3.sum(Vec3.multiply(distance, direction), position); + return position; + } + var HALF_TREE_SCALE = 16384; function getPositionToCreateEntity(extra) { var CREATE_DISTANCE = 2; @@ -256,10 +288,6 @@ } } } - - if (isActive) { - selectionManager.setSelections(pastedEntityIDs); - } } else { Window.notifyEditError("Can't import entities: entities would be out of bounds."); } @@ -322,6 +350,11 @@ } else if (parsedJsonMessage.type === "LOGIN") { openLoginWindow(); } else if (parsedJsonMessage.type === "WALLET_SETUP") { + wireEventBridge(true); + tablet.sendToQml({ + method: 'updateWalletReferrer', + referrer: "marketplace cta" + }); openWallet(); } else if (parsedJsonMessage.type === "MY_ITEMS") { referrerURL = MARKETPLACE_URL_INITIAL; @@ -399,6 +432,7 @@ referrer: "purchases" }); openWallet(); + break; case 'checkout_walletNotSetUp': wireEventBridge(true); tablet.sendToQml({ @@ -420,8 +454,10 @@ tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); break; case 'checkout_itemLinkClicked': - case 'checkout_continueShopping': tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'checkout_continueShopping': + tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); //tablet.popFromStack(); break; case 'purchases_itemInfoClicked': diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index 17821c737e..36a1cbcdd9 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -47,7 +47,7 @@ } return false; } - if (Overlays.getProperty(HMD.homeButtonID, "type") != "sphere" || + if (Overlays.getProperty(HMD.homeButtonID, "type") != "circle3d" || Overlays.getProperty(HMD.tabletScreenID, "type") != "web3d") { if (debugTablet) { print("TABLET is invalid due to other"); diff --git a/server-console/package-lock.json b/server-console/package-lock.json new file mode 100644 index 0000000000..51b7f2c268 --- /dev/null +++ b/server-console/package-lock.json @@ -0,0 +1,2250 @@ +{ + "name": "HighFidelitySandbox", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz", + "integrity": "sha1-W2A1su6dT7XPhZ8Iqb6BsghJGEM=", + "dev": true + }, + "always-tail": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/always-tail/-/always-tail-0.2.0.tgz", + "integrity": "sha1-M5sa9E1QJQqgeg6H7Mw6JOxET/4=", + "requires": { + "debug": "0.7.4" + } + }, + "ansi-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz", + "integrity": "sha1-xQYbbg74qBd15Q9dZhUb9r83EQc=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "ansicolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.2.1.tgz", + "integrity": "sha1-vgiVmQl7dKXJxKhKDNvNtivYeu8=" + }, + "array-find-index": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.1.tgz", + "integrity": "sha1-C8Jd2slB7IpJauJY/UrBiAA+868=", + "dev": true + }, + "asar": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/asar/-/asar-0.11.0.tgz", + "integrity": "sha1-uSbnksMV+MBIxDNx4yWwnJenZGQ=", + "dev": true, + "requires": { + "chromium-pickle-js": "0.1.0", + "commander": "2.9.0", + "cuint": "0.2.1", + "glob": "6.0.4", + "minimatch": "3.0.0", + "mkdirp": "0.5.1", + "mksnapshot": "0.3.0" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "dev": true, + "requires": { + "inflight": "1.0.4", + "inherits": "2.0.1", + "minimatch": "3.0.0", + "once": "1.3.3", + "path-is-absolute": "1.0.0" + } + } + } + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "aws4": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.3.2.tgz", + "integrity": "sha1-054L7kEs7Q6O2Uoj4xTzE6lbn9E=", + "requires": { + "lru-cache": "4.0.1" + } + }, + "balanced-match": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz", + "integrity": "sha1-qRzdHr7xqGZZ5w/03vAWJfwtZ1Y=" + }, + "base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=", + "dev": true + }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "dev": true, + "requires": { + "buffers": "0.1.1", + "chainsaw": "0.1.0" + } + }, + "bl": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", + "integrity": "sha1-/cqHGplxOqANGeO7ukHER4emU5g=", + "requires": { + "readable-stream": "2.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.1", + "isarray": "1.0.0", + "process-nextick-args": "1.0.6", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + } + } + }, + "bluebird": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.10.2.tgz", + "integrity": "sha1-AkpVFylTCIV/FPkfEQb8O1VfRGs=", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.3.tgz", + "integrity": "sha1-Rr/1ARXUf8mriYVKu4fZgHihCZE=", + "requires": { + "balanced-match": "0.3.0", + "concat-map": "0.0.1" + } + }, + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + } + }, + "cardinal": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-0.5.0.tgz", + "integrity": "sha1-ANX2YdvUqr/ffUHOSKWlm8o1opE=", + "requires": { + "ansicolors": "0.2.1", + "redeyed": "0.5.0" + } + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" + }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "dev": true, + "requires": { + "traverse": "0.3.9" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "cheerio": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.19.0.tgz", + "integrity": "sha1-dy5wFfLuKZZQltcepBdbdas1SSU=", + "requires": { + "css-select": "1.0.0", + "dom-serializer": "0.1.0", + "entities": "1.1.1", + "htmlparser2": "3.8.3", + "lodash": "3.10.1" + } + }, + "chromium-pickle-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.1.0.tgz", + "integrity": "sha1-HUixB9ghJqLz4hHC6iX4A7pVGyE=", + "dev": true + }, + "cli-table": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", + "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "requires": { + "colors": "1.0.3" + } + }, + "cli-usage": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/cli-usage/-/cli-usage-0.1.2.tgz", + "integrity": "sha1-SXwg6vEuwneTk6m/rCJcX2y5FS0=", + "requires": { + "marked": "0.3.5", + "marked-terminal": "1.6.1", + "minimist": "0.2.0" + }, + "dependencies": { + "minimist": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.2.0.tgz", + "integrity": "sha1-Tf/lJdriuGTGbC4jxicdev3s784=" + } + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "1.0.1", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.0.0" + } + }, + "code-point-at": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.0.tgz", + "integrity": "sha1-9psZLT99keOC5Lcb3bd4eGGasMY=", + "requires": { + "number-is-nan": "1.0.0" + } + }, + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "requires": { + "graceful-readlink": "1.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.0.tgz", + "integrity": "sha1-U/fUPFHF5D+ByP3QMyHGMb5o1hE=", + "dev": true, + "requires": { + "inherits": "2.0.1", + "readable-stream": "2.0.6", + "typedarray": "0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.1", + "isarray": "1.0.0", + "process-nextick-args": "1.0.6", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + } + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "requires": { + "boom": "2.10.1" + } + }, + "css-select": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.0.0.tgz", + "integrity": "sha1-sRIcpRhI3SZOIkTQWM7iVN7rRLA=", + "requires": { + "boolbase": "1.0.0", + "css-what": "1.0.0", + "domutils": "1.4.3", + "nth-check": "1.0.1" + } + }, + "css-what": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-1.0.0.tgz", + "integrity": "sha1-18wt9FGAZm+Z0rFEYmOUaeAPc2w=" + }, + "ctype": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz", + "integrity": "sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8=", + "dev": true + }, + "cuint": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.1.tgz", + "integrity": "sha1-VlBFzoEnxwxr80D1kcAEin1M/rw=", + "dev": true + }, + "dashdash": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.13.0.tgz", + "integrity": "sha1-parm/Z2OFWYk6w3ZJZ6xK6JFOFo=", + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "debug": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=" + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decompress-zip": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/decompress-zip/-/decompress-zip-0.3.0.tgz", + "integrity": "sha1-rjvLfjTGWHmt/nfhnDD4ZgK0vbA=", + "dev": true, + "requires": { + "binary": "0.3.0", + "graceful-fs": "4.1.3", + "mkpath": "0.1.0", + "nopt": "3.0.6", + "q": "1.4.1", + "readable-stream": "1.1.14", + "touch": "0.0.3" + } + }, + "deep-extend": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz", + "integrity": "sha1-7+QRPQgIX05vlod1mBD4B0aeIlM=", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "requires": { + "domelementtype": "1.1.3", + "entities": "1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" + } + } + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" + }, + "domhandler": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", + "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", + "requires": { + "domelementtype": "1.3.0" + } + }, + "domutils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.4.3.tgz", + "integrity": "sha1-CGVRN5bGswYDGFDhdVFrr4C3Km8=", + "requires": { + "domelementtype": "1.3.0" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.0" + } + }, + "electron-download": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-2.1.1.tgz", + "integrity": "sha1-AH07HyrTco0nzP5PhJayY/kTijE=", + "dev": true, + "requires": { + "debug": "2.2.0", + "home-path": "1.0.3", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "mv": "2.1.1", + "nugget": "1.6.2", + "path-exists": "1.0.0", + "rc": "1.1.6" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + } + } + }, + "electron-log": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-1.1.1.tgz", + "integrity": "sha1-DboCXtM9DkW/j0DG6b487i+YbCg=" + }, + "electron-osx-sign": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.3.0.tgz", + "integrity": "sha1-SXIB38g1OMVLNPGkBexuIaAf904=", + "dev": true, + "requires": { + "minimist": "1.2.0", + "run-series": "1.1.4" + } + }, + "electron-packager": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-6.0.2.tgz", + "integrity": "sha1-juAGaf6KNjCVAnMvz+0RfX7prCk=", + "dev": true, + "requires": { + "asar": "0.11.0", + "electron-download": "2.1.1", + "electron-osx-sign": "0.3.0", + "extract-zip": "1.5.0", + "fs-extra": "0.26.7", + "get-package-info": "0.0.2", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "mv": "2.1.1", + "ncp": "2.0.0", + "object-assign": "4.0.1", + "plist": "1.2.0", + "rcedit": "0.5.0", + "resolve": "1.1.7", + "rimraf": "2.5.2", + "run-series": "1.1.4" + } + }, + "electron-prebuilt": { + "version": "0.37.5", + "resolved": "https://registry.npmjs.org/electron-prebuilt/-/electron-prebuilt-0.37.5.tgz", + "integrity": "sha1-OkGJgod4FdOnrB+bLi9KcPQg/3A=", + "dev": true, + "requires": { + "electron-download": "2.1.1", + "extract-zip": "1.5.0" + } + }, + "end-of-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.1.0.tgz", + "integrity": "sha1-6TUyWLqpEIll78QcsO+K3i88+wc=", + "requires": { + "once": "1.3.3" + } + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" + }, + "error-ex": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.0.tgz", + "integrity": "sha1-5ntD8+gsluo6WE/+4Ln8MyXYAtk=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esprima-fb": { + "version": "12001.1.0-dev-harmony-fb", + "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-12001.1.0-dev-harmony-fb.tgz", + "integrity": "sha1-2EQAOEupXOJnjGF60kp/QICNqRU=" + }, + "extend": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", + "integrity": "sha1-WkdDU7nzNT3dgXbf03uRyDpG8dQ=" + }, + "extract-zip": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.5.0.tgz", + "integrity": "sha1-ksz22B73Cp+kwXRxFMzvbYaIpsQ=", + "dev": true, + "requires": { + "concat-stream": "1.5.0", + "debug": "0.7.4", + "mkdirp": "0.5.0", + "yauzl": "2.4.1" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, + "extsprintf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" + }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "1.2.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + }, + "dependencies": { + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + } + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "1.0.0-rc4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz", + "integrity": "sha1-BaxrwiIntD5EYfSIFhVUaZ1Pi14=", + "requires": { + "async": "1.5.2", + "combined-stream": "1.0.5", + "mime-types": "2.1.10" + } + }, + "fs-extra": { + "version": "0.26.7", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz", + "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=", + "requires": { + "graceful-fs": "4.1.3", + "jsonfile": "2.2.3", + "klaw": "1.1.3", + "path-is-absolute": "1.0.0", + "rimraf": "2.5.2" + } + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "requires": { + "is-property": "1.0.2" + } + }, + "get-package-info": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/get-package-info/-/get-package-info-0.0.2.tgz", + "integrity": "sha1-csOPvuLnZyhCSgDcFOJN0aKMI5E=", + "dev": true, + "requires": { + "bluebird": "3.3.5", + "lodash.get": "4.2.1", + "resolve": "1.1.7" + }, + "dependencies": { + "bluebird": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.3.5.tgz", + "integrity": "sha1-XudH8ce9lnZYtoOTZDCu51OVWjQ=", + "dev": true + } + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "glob": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz", + "integrity": "sha1-CqI1kxpKlqwT1g/6wvuHe9btT1g=", + "requires": { + "inflight": "1.0.4", + "inherits": "2.0.1", + "minimatch": "3.0.0", + "once": "1.3.3", + "path-is-absolute": "1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz", + "integrity": "sha1-kgM84RETxB4mKNYf36QLwQ3AFVw=" + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "requires": { + "chalk": "1.1.3", + "commander": "2.9.0", + "is-my-json-valid": "2.13.1", + "pinkie-promise": "2.0.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "2.0.0" + } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "home-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/home-path/-/home-path-1.0.3.tgz", + "integrity": "sha1-ns5Z/sPwMubRC1Q0/uJk30wt4y8=", + "dev": true + }, + "hosted-git-info": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.1.4.tgz", + "integrity": "sha1-2elTsmmIvogJbEbpJklNlgTDAPg=", + "dev": true + }, + "htmlparser2": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.3.0", + "domutils": "1.5.1", + "entities": "1.0.0", + "readable-stream": "1.1.14" + }, + "dependencies": { + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "requires": { + "dom-serializer": "0.1.0", + "domelementtype": "1.3.0" + } + }, + "entities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" + } + } + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.2.2", + "sshpk": "1.7.4" + } + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + }, + "dependencies": { + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.1" + } + } + } + }, + "inflight": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.4.tgz", + "integrity": "sha1-bLtFIevVHODsCpNr/XZX736bFyo=", + "requires": { + "once": "1.3.3", + "wrappy": "1.0.1" + } + }, + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "ini": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", + "dev": true + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "is-absolute": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.1.7.tgz", + "integrity": "sha1-hHSREZ/MtftDYhfMc39/qtUPYD8=", + "requires": { + "is-relative": "0.1.3" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-finite": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.1.tgz", + "integrity": "sha1-ZDhgPq6+J5OUj/SkJi7I2z1iWXs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.0" + } + }, + "is-my-json-valid": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz", + "integrity": "sha1-1Vd4qC/rawlj/0vhEdXRaE6JBwc=", + "requires": { + "generate-function": "2.0.0", + "generate-object-property": "1.2.0", + "jsonpointer": "2.0.0", + "xtend": "4.0.1" + } + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" + }, + "is-relative": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.1.3.tgz", + "integrity": "sha1-kF/uiuhvRbPsYUvDwVyGnfCHboI=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "isexe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-1.1.2.tgz", + "integrity": "sha1-NvPiLmB1CSD15yQaR2qMakInWtA=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jodid25519": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", + "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", + "optional": true, + "requires": { + "jsbn": "0.1.0" + } + }, + "jsbn": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz", + "integrity": "sha1-ZQmH2g3XT06/WhE3eiqi0nPpff0=", + "optional": true + }, + "json-schema": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz", + "integrity": "sha1-UDVPGfYDkXxpX3C4Wvp3w7DyNQY=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonfile": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.2.3.tgz", + "integrity": "sha1-4lK5mmr5AdPsQfMyWJyQUJp7xgU=" + }, + "jsonpointer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz", + "integrity": "sha1-OvHdIP6FRjkQ1GmjheMwF9KgMNk=" + }, + "jsprim": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz", + "integrity": "sha1-8gyQaskqvVjjt5rIvHCkiDJRLaE=", + "requires": { + "extsprintf": "1.0.2", + "json-schema": "0.2.2", + "verror": "1.3.6" + } + }, + "klaw": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.1.3.tgz", + "integrity": "sha1-faM8a0L5s9yc7ADRfxOvAX/MJyE=" + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.3", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + }, + "lodash._arraycopy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz", + "integrity": "sha1-due3wfH7klRzdIeKVi7Qaj5Q9uE=" + }, + "lodash._arrayeach": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz", + "integrity": "sha1-urFWsqkNPxu9XGU0AzSeXlkz754=" + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "requires": { + "lodash._basecopy": "3.0.1", + "lodash.keys": "3.1.2" + } + }, + "lodash._baseclone": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz", + "integrity": "sha1-MDUZv2OT/n5C802LYw73eU41Qrc=", + "requires": { + "lodash._arraycopy": "3.0.0", + "lodash._arrayeach": "3.0.0", + "lodash._baseassign": "3.2.0", + "lodash._basefor": "3.0.3", + "lodash.isarray": "3.0.4", + "lodash.keys": "3.1.2" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=" + }, + "lodash._basefor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.3.tgz", + "integrity": "sha1-dVC06SGO8J+tJDQ7YSAhx5tMIMI=" + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=" + }, + "lodash._createassigner": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", + "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", + "requires": { + "lodash._bindcallback": "3.0.1", + "lodash._isiterateecall": "3.0.9", + "lodash.restparam": "3.6.1" + } + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=" + }, + "lodash._stringtopath": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/lodash._stringtopath/-/lodash._stringtopath-4.7.1.tgz", + "integrity": "sha1-1GYKFaWZeYj6WTAaO/2gXJpt494=", + "dev": true + }, + "lodash.assign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", + "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", + "requires": { + "lodash._baseassign": "3.2.0", + "lodash._createassigner": "3.1.1", + "lodash.keys": "3.1.2" + } + }, + "lodash.clonedeep": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz", + "integrity": "sha1-oKHkDYKl6on/WxR7hETtY9koJ9s=", + "requires": { + "lodash._baseclone": "3.3.0", + "lodash._bindcallback": "3.0.1" + } + }, + "lodash.get": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.2.1.tgz", + "integrity": "sha1-bpr8h7imwCFgZnpw9sjOEHiusRs=", + "dev": true, + "requires": { + "lodash._stringtopath": "4.7.1" + } + }, + "lodash.isarguments": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.0.8.tgz", + "integrity": "sha1-W/jaiH8B8qnknAoXXNrrMYoOQ9w=" + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.0.8", + "lodash.isarray": "3.0.4" + } + }, + "lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=" + }, + "loud-rejection": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.3.0.tgz", + "integrity": "sha1-8omjkvF9K6rPGU0KZzAEOUQzsRU=", + "dev": true, + "requires": { + "array-find-index": "1.0.1", + "signal-exit": "2.1.2" + } + }, + "lru-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.1.tgz", + "integrity": "sha1-E0OVXtry432bnn7nJB4nxLn7cr4=", + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.0.0" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "marked": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.5.tgz", + "integrity": "sha1-QROhWsXXvKFYpargciRYe5+hW5Q=" + }, + "marked-terminal": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-1.6.1.tgz", + "integrity": "sha1-BM0cfIsO9I2z9oAQ1zpXqWYcbM8=", + "requires": { + "cardinal": "0.5.0", + "chalk": "1.1.3", + "cli-table": "0.3.1", + "lodash.assign": "3.2.0", + "node-emoji": "0.1.0" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.3.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.3.5", + "object-assign": "4.0.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + } + }, + "mime-db": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.22.0.tgz", + "integrity": "sha1-qyOmNy3J2G09yRIb0OvTgQWhkEo=" + }, + "mime-types": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.10.tgz", + "integrity": "sha1-uTx8tDYuFtQQcqflRTj7TUMHCDc=", + "requires": { + "mime-db": "1.22.0" + } + }, + "minimatch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz", + "integrity": "sha1-UjYVelHk8ATBd/s8Un/33Xjw74M=", + "requires": { + "brace-expansion": "1.1.3" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "mkpath": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/mkpath/-/mkpath-0.1.0.tgz", + "integrity": "sha1-dVSm+Nhxg0zJe1RisSLEwSTW3pE=", + "dev": true + }, + "mksnapshot": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mksnapshot/-/mksnapshot-0.3.0.tgz", + "integrity": "sha1-MuqYStb1MjJMaj+uZACHa4WChAc=", + "dev": true, + "requires": { + "decompress-zip": "0.3.0", + "fs-extra": "0.26.7", + "request": "2.55.0" + }, + "dependencies": { + "asn1": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz", + "integrity": "sha1-VZvhg3bQik7E2+gId9J4GGObLfc=", + "dev": true + }, + "assert-plus": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", + "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA=", + "dev": true + }, + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true + }, + "aws-sign2": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz", + "integrity": "sha1-xXED96F/wDfwLXwuZLYC6iI/fWM=", + "dev": true + }, + "bl": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz", + "integrity": "sha1-wGt5evCF6gC8Unr8jvzxHeIjIFQ=", + "dev": true, + "requires": { + "readable-stream": "1.0.34" + } + }, + "caseless": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.9.0.tgz", + "integrity": "sha1-t7Zc5r8UE4hlOc/VM/CzDv+pz4g=", + "dev": true + }, + "combined-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", + "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=", + "dev": true, + "requires": { + "delayed-stream": "0.0.5" + } + }, + "delayed-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", + "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=", + "dev": true + }, + "form-data": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.2.0.tgz", + "integrity": "sha1-Jvi8JtpkQOKZy9z7aQNcT3em5GY=", + "dev": true, + "requires": { + "async": "0.9.2", + "combined-stream": "0.0.7", + "mime-types": "2.0.14" + } + }, + "har-validator": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-1.8.0.tgz", + "integrity": "sha1-2DhCsOtMQ1lgrrEIoGejqpTA7rI=", + "dev": true, + "requires": { + "bluebird": "2.10.2", + "chalk": "1.1.3", + "commander": "2.9.0", + "is-my-json-valid": "2.13.1" + } + }, + "hawk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-2.3.1.tgz", + "integrity": "sha1-HnMc45RH+h0PbXB/e87r7A/R7B8=", + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "http-signature": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", + "integrity": "sha1-T72sEyVZqoMjEh5UB3nAoBKyfmY=", + "dev": true, + "requires": { + "asn1": "0.1.11", + "assert-plus": "0.1.5", + "ctype": "0.5.3" + } + }, + "mime-db": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", + "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc=", + "dev": true + }, + "mime-types": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", + "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=", + "dev": true, + "requires": { + "mime-db": "1.12.0" + } + }, + "oauth-sign": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.6.0.tgz", + "integrity": "sha1-fb6uRPbKRU4fFoRR1jB0ZzWBPOM=", + "dev": true + }, + "qs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-2.4.2.tgz", + "integrity": "sha1-9854jld33wtQENp/fE5zujJHD1o=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.1", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "request": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.55.0.tgz", + "integrity": "sha1-11wc32eddrsQD5v/4f5VG1wk6T0=", + "dev": true, + "requires": { + "aws-sign2": "0.5.0", + "bl": "0.9.5", + "caseless": "0.9.0", + "combined-stream": "0.0.7", + "forever-agent": "0.6.1", + "form-data": "0.2.0", + "har-validator": "1.8.0", + "hawk": "2.3.1", + "http-signature": "0.10.1", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.0.14", + "node-uuid": "1.4.7", + "oauth-sign": "0.6.0", + "qs": "2.4.2", + "stringstream": "0.0.5", + "tough-cookie": "2.2.2", + "tunnel-agent": "0.4.2" + } + } + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "dev": true, + "requires": { + "mkdirp": "0.5.1", + "ncp": "2.0.0", + "rimraf": "2.4.5" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "dev": true, + "requires": { + "inflight": "1.0.4", + "inherits": "2.0.1", + "minimatch": "3.0.0", + "once": "1.3.3", + "path-is-absolute": "1.0.0" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "dev": true, + "requires": { + "glob": "6.0.4" + } + } + } + }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "dev": true + }, + "node-emoji": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-0.1.0.tgz", + "integrity": "sha1-P0QkpVuo7VDCVKE4WLJEVPQYJgI=" + }, + "node-notifier": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-4.5.0.tgz", + "integrity": "sha1-Ap7pjXqbxOlsnLUb6dTzYTI/6ps=", + "requires": { + "cli-usage": "0.1.2", + "growly": "1.3.0", + "lodash.clonedeep": "3.0.2", + "minimist": "1.2.0", + "semver": "5.1.0", + "shellwords": "0.1.0", + "which": "1.2.4" + } + }, + "node-uuid": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz", + "integrity": "sha1-baWhdmjEs91ZYjvaEc9/pMH2Cm8=" + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1.0.7" + } + }, + "normalize-package-data": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.3.5.tgz", + "integrity": "sha1-jZJPFClg4Xd+f/4XBUNjHMfLAt8=", + "dev": true, + "requires": { + "hosted-git-info": "2.1.4", + "is-builtin-module": "1.0.0", + "semver": "5.1.0", + "validate-npm-package-license": "3.0.1" + } + }, + "nth-check": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", + "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "requires": { + "boolbase": "1.0.0" + } + }, + "nugget": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/nugget/-/nugget-1.6.2.tgz", + "integrity": "sha1-iMpuA7pXBqmRc/XaCQJZPWvK4Qc=", + "dev": true, + "requires": { + "debug": "2.2.0", + "minimist": "1.2.0", + "pretty-bytes": "1.0.4", + "progress-stream": "1.2.0", + "request": "2.71.0", + "single-line-log": "0.4.1", + "throttleit": "0.0.2" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "throttleit": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", + "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", + "dev": true + } + } + }, + "number-is-nan": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz", + "integrity": "sha1-wCD1KcUoKt/dIz2R1LGBw9aG3Es=" + }, + "oauth-sign": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.1.tgz", + "integrity": "sha1-GCQ5vbkTeL90YOdcZOpD5kSN7wY=" + }, + "object-assign": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.0.1.tgz", + "integrity": "sha1-mVBEVsNZi1ytT8WcJuipuxB/4L0=", + "dev": true + }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", + "dev": true + }, + "once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", + "requires": { + "wrappy": "1.0.1" + } + }, + "os-homedir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.1.tgz", + "integrity": "sha1-DWK99EuRb9O73PLKsZGUj7CU8Ac=" + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "requires": { + "lcid": "1.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.0" + } + }, + "path-exists": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", + "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz", + "integrity": "sha1-Jj2tpmqz8vsQv3+dJN2PPlcO+RI=" + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.3", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "2.0.4" + } + }, + "plist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-1.2.0.tgz", + "integrity": "sha1-CEtQk93JJQbiWfh0uNmxr7jHlZM=", + "dev": true, + "requires": { + "base64-js": "0.0.8", + "util-deprecate": "1.0.2", + "xmlbuilder": "4.0.0", + "xmldom": "0.1.22" + } + }, + "pretty-bytes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", + "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", + "dev": true, + "requires": { + "get-stdin": "4.0.1", + "meow": "3.7.0" + } + }, + "process-nextick-args": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.6.tgz", + "integrity": "sha1-D5awAc6pCxJZLOVm7bl+wR5pvQU=" + }, + "progress-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz", + "integrity": "sha1-LNPP6jO6OonJwSHsM0er6asSX3c=", + "dev": true, + "requires": { + "speedometer": "0.1.4", + "through2": "0.2.3" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "pump": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.1.tgz", + "integrity": "sha1-8fFAn7m9EIW721drQ7hOxLXq3Bo=", + "requires": { + "end-of-stream": "1.1.0", + "once": "1.3.3" + } + }, + "q": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "dev": true + }, + "qs": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz", + "integrity": "sha1-7B0WJrJCeNmfD99FSeUk4k7O6yY=" + }, + "rc": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz", + "integrity": "sha1-Q2UbdrauU7XIAvEVH6P8OwWZack=", + "dev": true, + "requires": { + "deep-extend": "0.4.1", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "1.0.4" + } + }, + "rcedit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-0.5.0.tgz", + "integrity": "sha1-72a1p/AxB1IUGjTiLyPCJDPBzxU=", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.3.5", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.1", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "redeyed": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-0.5.0.tgz", + "integrity": "sha1-erAA5g7jh1rBFdKe2zLBQDxsJdE=", + "requires": { + "esprima-fb": "12001.1.0-dev-harmony-fb" + } + }, + "request": { + "version": "2.71.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.71.0.tgz", + "integrity": "sha1-bxRkPJxaZ8ruapXPjvBHfVYDvZE=", + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.3.2", + "bl": "1.1.2", + "caseless": "0.11.0", + "combined-stream": "1.0.5", + "extend": "3.0.0", + "forever-agent": "0.6.1", + "form-data": "1.0.0-rc4", + "har-validator": "2.0.6", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.10", + "node-uuid": "1.4.7", + "oauth-sign": "0.8.1", + "qs": "6.1.0", + "stringstream": "0.0.5", + "tough-cookie": "2.2.2", + "tunnel-agent": "0.4.2" + } + }, + "request-progress": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-1.0.2.tgz", + "integrity": "sha1-XUBvCBMJ32G0qKqDzVc032Pxi/U=", + "requires": { + "throttleit": "1.0.0" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "rimraf": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz", + "integrity": "sha1-YrqUf6TAtDY4Oa7+zU8PutYFlyY=", + "requires": { + "glob": "7.0.3" + } + }, + "run-series": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/run-series/-/run-series-1.1.4.tgz", + "integrity": "sha1-iac93F51ye+KtjIMChYA1qQRebk=", + "dev": true + }, + "semver": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz", + "integrity": "sha1-hfLPhVBGXE3wAM99hvawVBBqueU=" + }, + "shellwords": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.0.tgz", + "integrity": "sha1-Zq/Ue2oSky2Qccv9mKUueFzQuhQ=" + }, + "signal-exit": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-2.1.2.tgz", + "integrity": "sha1-N1h5sfkuvDszRIDQONxUam1VhWQ=", + "dev": true + }, + "single-line-log": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-0.4.1.tgz", + "integrity": "sha1-h6VWSfdJ14PsDc2AToFA2Yc8fO4=", + "dev": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "requires": { + "hoek": "2.16.3" + } + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true, + "requires": { + "spdx-license-ids": "1.2.1" + } + }, + "spdx-exceptions": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-1.0.4.tgz", + "integrity": "sha1-IguEI5EZrpBFqJLbgag/TOFvgP0=", + "dev": true + }, + "spdx-expression-parse": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.2.tgz", + "integrity": "sha1-1SsUtelnB3FECvIlvLVjEirEUvY=", + "dev": true, + "requires": { + "spdx-exceptions": "1.0.4", + "spdx-license-ids": "1.2.1" + } + }, + "spdx-license-ids": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.1.tgz", + "integrity": "sha1-0H6hek0v2TUfnZTi/5zsdBgP6PM=", + "dev": true + }, + "speedometer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz", + "integrity": "sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=", + "dev": true + }, + "sshpk": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.7.4.tgz", + "integrity": "sha1-rXtH3vymHIQV2WQkO2KwzmD7yjg=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "0.2.0", + "dashdash": "1.13.0", + "ecc-jsbn": "0.1.1", + "jodid25519": "1.0.2", + "jsbn": "0.1.0", + "tweetnacl": "0.14.3" + } + }, + "string-width": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.1.tgz", + "integrity": "sha1-ySEptvHX9SrPmvQkom44ZKBc6wo=", + "requires": { + "code-point-at": "1.0.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "strip-json-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "tar-fs": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.12.0.tgz", + "integrity": "sha1-pqgFU9ilTHPeHQrg553ncDVgXh0=", + "requires": { + "mkdirp": "0.5.1", + "pump": "1.0.1", + "tar-stream": "1.5.1" + } + }, + "tar-stream": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.1.tgz", + "integrity": "sha1-UWx00b6j4THMC5NIkpyag/CirRE=", + "requires": { + "bl": "1.1.2", + "end-of-stream": "1.1.0", + "readable-stream": "2.0.6", + "xtend": "4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.1", + "isarray": "1.0.0", + "process-nextick-args": "1.0.6", + "string_decoder": "0.10.31", + "util-deprecate": "1.0.2" + } + } + } + }, + "throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=" + }, + "through2": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", + "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", + "dev": true, + "requires": { + "readable-stream": "1.1.14", + "xtend": "2.1.2" + }, + "dependencies": { + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "dev": true, + "requires": { + "object-keys": "0.4.0" + } + } + } + }, + "touch": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/touch/-/touch-0.0.3.tgz", + "integrity": "sha1-Ua7z1ElXHU8oel2Hyci0kYGg2x0=", + "dev": true, + "requires": { + "nopt": "1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1.0.7" + } + } + } + }, + "tough-cookie": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz", + "integrity": "sha1-yDoYMPTl7wuT7yo0iOck+N4Basc=" + }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "tunnel-agent": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.2.tgz", + "integrity": "sha1-EQTj82rIcSXChycAZ9WC0YEzv+4=" + }, + "tweetnacl": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.3.tgz", + "integrity": "sha1-PaOC9nDyXe1417PReSEZvKC3Ey0=", + "optional": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.2" + } + }, + "verror": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "requires": { + "extsprintf": "1.0.2" + } + }, + "which": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.4.tgz", + "integrity": "sha1-FVf5YIBgTlsRs1meufRbUKnv1yI=", + "requires": { + "is-absolute": "0.1.7", + "isexe": "1.1.2" + } + }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" + }, + "wrap-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.0.0.tgz", + "integrity": "sha1-fTD4+HP5pbvDpk2ryNF34HGuQm8=", + "requires": { + "string-width": "1.0.1" + } + }, + "wrappy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz", + "integrity": "sha1-HmWWmWXMvC20VIxrhKbyxa7dRzk=" + }, + "xmlbuilder": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.0.0.tgz", + "integrity": "sha1-mLj2UcowqmJANvEn0RzGbce5B6M=", + "dev": true, + "requires": { + "lodash": "3.10.1" + } + }, + "xmldom": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.22.tgz", + "integrity": "sha1-EN5OXpZJgfA8jMcvrcCNFLbDqiY=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, + "yallist": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.0.0.tgz", + "integrity": "sha1-MGxUODXwnuGkyyO3vOmrNByRzdQ=" + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "requires": { + "camelcase": "2.1.1", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "os-locale": "1.4.0", + "string-width": "1.0.1", + "window-size": "0.1.4", + "y18n": "3.2.1" + } + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "1.0.1" + } + } + } +} diff --git a/server-console/package.json b/server-console/package.json index f72ffc347f..8d2a177e0a 100644 --- a/server-console/package.json +++ b/server-console/package.json @@ -8,7 +8,6 @@ "" ], "devDependencies": { - "electron-compilers": "^1.0.1", "electron-packager": "^6.0.2", "electron-prebuilt": "0.37.5" }, diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index cf11ef9e7a..16446c5071 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -23,4 +23,8 @@ if (BUILD_TOOLS) add_subdirectory(oven) set_target_properties(oven PROPERTIES FOLDER "Tools") + + add_subdirectory(auto-tester) + set_target_properties(auto-tester PROPERTIES FOLDER "Tools") endif() + diff --git a/tools/auto-tester/CMakeLists.txt b/tools/auto-tester/CMakeLists.txt new file mode 100644 index 0000000000..e5f2c1fb97 --- /dev/null +++ b/tools/auto-tester/CMakeLists.txt @@ -0,0 +1,57 @@ +set(TARGET_NAME auto-tester) +project(${TARGET_NAME}) + +# Automatically run UIC and MOC. This replaces the older WRAP macros +SET(CMAKE_AUTOUIC ON) +SET(CMAKE_AUTOMOC ON) + +setup_hifi_project(Core Widgets) +link_hifi_libraries() + +# FIX: Qt was built with -reduce-relocations +if (Qt5_POSITION_INDEPENDENT_CODE) + SET(CMAKE_POSITION_INDEPENDENT_CODE ON) +endif() + +# Qt includes +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) +include_directories(${Qt5Core_INCLUDE_DIRS}) +include_directories(${Qt5Widgets_INCLUDE_DIRS}) + +set(QT_LIBRARIES Qt5::Core Qt5::Widgets) + +# Find all sources files +file (GLOB_RECURSE SOURCES src/*.cpp) +file (GLOB_RECURSE HEADERS src/*.h) +file (GLOB_RECURSE UIS src/ui/*.ui) + +if (WIN32) + # Do not show Console + set_property(TARGET auto-tester PROPERTY WIN32_EXECUTABLE true) +endif() + +add_executable(PROJECT_NAME ${SOURCES} ${HEADERS} ${UIS}) + +target_link_libraries(PROJECT_NAME ${QT_LIBRARIES}) + +# Copy required dll's. +add_custom_command(TARGET auto-tester POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ + COMMAND ${CMAKE_COMMAND} -E copy_if_different $ $ +) + +if (WIN32) + find_program(WINDEPLOYQT_COMMAND windeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) + + if (NOT WINDEPLOYQT_COMMAND) + message(FATAL_ERROR "Could not find windeployqt at ${QT_DIR}/bin. windeployqt is required.") + endif () + + # add a post-build command to call windeployqt to copy Qt plugins + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> \"$\"" + ) +endif () \ No newline at end of file diff --git a/tools/auto-tester/ReadMe.md b/tools/auto-tester/ReadMe.md new file mode 100644 index 0000000000..57ec7ea623 --- /dev/null +++ b/tools/auto-tester/ReadMe.md @@ -0,0 +1,7 @@ +After building auto-tester, it needs to be deployed to Amazon SW + +* In folder hifi/build/tools/auto-tester + * Right click on the Release folder + * Select 7-Zip -> Add to archive + * Select Option ```Create SFX archive``` to create Release.exe +* Use Cyberduck (or any other AWS S3 client) to copy Release.exe to hifi-content/nissim/autoTester/ \ No newline at end of file diff --git a/tools/auto-tester/src/ImageComparer.cpp b/tools/auto-tester/src/ImageComparer.cpp new file mode 100644 index 0000000000..121c98e16e --- /dev/null +++ b/tools/auto-tester/src/ImageComparer.cpp @@ -0,0 +1,119 @@ +// +// ImageComparer.cpp +// +// Created by Nissim Hadar on 18 Nov 2017. +// Copyright 2013 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 "ImageComparer.h" + +#include + +// Computes SSIM - see https://en.wikipedia.org/wiki/Structural_similarity +// The value is computed for the luminance component and the average value is returned +double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) const { + // Make sure the image is 8 bits per colour + QImage::Format format = expectedImage.format(); + if (format != QImage::Format::Format_RGB32) { + throw -1; + } + + const int L = 255; // (2^number of bits per pixel) - 1 + const double K1{ 0.01 }; + const double K2{ 0.03 }; + const double c1 = pow((K1 * L), 2); + const double c2 = pow((K2 * L), 2); + + // Coefficients for luminosity calculation + const double R_Y = 0.212655f; + const double G_Y = 0.715158f; + const double B_Y = 0.072187f; + + // First go over all full 8x8 blocks + // This is done in 3 loops + // 1) Read the pixels into a linear array (an optimization) + // 2) Calculate mean + // 3) Calculate variance and covariance + // + // p - pixel in expected image + // q - pixel in result image + // + const int WIN_SIZE = 8; + int x{ 0 }; // column index (start of block) + int y{ 0 }; // row index (start of block + + // Pixels are processed in square blocks + double p[WIN_SIZE * WIN_SIZE]; + double q[WIN_SIZE * WIN_SIZE]; + + int windowCounter{ 0 }; + double ssim{ 0.0 }; + while (x < expectedImage.width()) { + int lastX = x + WIN_SIZE - 1; + if (lastX > expectedImage.width() - 1) { + x -= (lastX - expectedImage.width()); + } + + while (y < expectedImage.height()) { + int lastY = y + WIN_SIZE - 1; + if (lastY > expectedImage.height() - 1) { + y -= (lastY - expectedImage.height()); + } + + // Collect pixels into linear arrays + int i{ 0 }; + for (int xx = 0; xx < WIN_SIZE; ++xx) { + for (int yy = 0; yy < WIN_SIZE; ++yy) { + // Get pixels + QRgb pixelP = expectedImage.pixel(QPoint(x + xx, y + yy)); + QRgb pixelQ = resultImage.pixel(QPoint(x + xx, y + yy)); + + // Convert to luminance + p[i] = R_Y * qRed(pixelP) + G_Y * qGreen(pixelP) + B_Y * qBlue(pixelP); + q[i] = R_Y * qRed(pixelQ) + G_Y * qGreen(pixelQ) + B_Y * qBlue(pixelQ); + + ++i; + } + } + + // Calculate mean + double mP{ 0.0 }; // average value of expected pixel + double mQ{ 0.0 }; // average value of result pixel + for (int j = 0; j < WIN_SIZE * WIN_SIZE; ++j) { + mP += p[j]; + mQ += q[j]; + } + mP /= (WIN_SIZE * WIN_SIZE); + mQ /= (WIN_SIZE * WIN_SIZE); + + // Calculate variance and covariance + double sigsqP{ 0.0 }; + double sigsqQ{ 0.0 }; + double sigPQ{ 0.0 }; + for (int j = 0; j < WIN_SIZE * WIN_SIZE; ++j) { + sigsqP += pow((p[j] - mP), 2); + sigsqQ += pow((q[j] - mQ), 2); + + sigPQ += (p[j] - mP) * (q[j] - mQ); + } + sigsqP /= (WIN_SIZE * WIN_SIZE); + sigsqQ /= (WIN_SIZE * WIN_SIZE); + sigPQ /= (WIN_SIZE * WIN_SIZE); + + double numerator = (2.0 * mP * mQ + c1) * (2.0 * sigPQ + c2); + double denominator = (mP * mP + mQ * mQ + c1) * (sigsqP + sigsqQ + c2); + + ssim += numerator / denominator; + ++windowCounter; + + y += WIN_SIZE; + } + + x += WIN_SIZE; + y = 0; + } + + return ssim / windowCounter; +}; diff --git a/tools/auto-tester/src/ImageComparer.h b/tools/auto-tester/src/ImageComparer.h new file mode 100644 index 0000000000..7b7b8b0b74 --- /dev/null +++ b/tools/auto-tester/src/ImageComparer.h @@ -0,0 +1,21 @@ +// +// ImageComparer.h +// +// Created by Nissim Hadar on 18 Nov 2017. +// Copyright 2013 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_ImageComparer_h +#define hifi_ImageComparer_h + +#include +#include + +class ImageComparer { +public: + double compareImages(QImage resultImage, QImage expectedImage) const; +}; + +#endif // hifi_ImageComparer_h diff --git a/tools/auto-tester/src/Test.cpp b/tools/auto-tester/src/Test.cpp new file mode 100644 index 0000000000..8cb36fcfca --- /dev/null +++ b/tools/auto-tester/src/Test.cpp @@ -0,0 +1,383 @@ +// +// Test.cpp +// +// Created by Nissim Hadar on 2 Nov 2017. +// Copyright 2013 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 "Test.h" + +#include +#include +#include + +Test::Test() { + snapshotFilenameFormat = QRegularExpression("hifi-snap-by-.+-on-\\d\\d\\d\\d-\\d\\d-\\d\\d_\\d\\d-\\d\\d-\\d\\d.jpg"); + + expectedImageFilenameFormat = QRegularExpression("ExpectedImage_\\d+.jpg"); + + mismatchWindow.setModal(true); +} + +bool Test::compareImageLists(QStringList expectedImages, QStringList resultImages) { + // Loop over both lists and compare each pair of images + // Quit loop if user has aborted due to a failed test. + const double THRESHOLD{ 0.999 }; + bool success{ true }; + bool keepOn{ true }; + for (int i = 0; keepOn && i < expectedImages.length(); ++i) { + // First check that images are the same size + QImage resultImage(resultImages[i]); + QImage expectedImage(expectedImages[i]); + if (resultImage.width() != expectedImage.width() || resultImage.height() != expectedImage.height()) { + messageBox.critical(0, "Internal error", "Images are not the same size"); + exit(-1); + } + + double similarityIndex; // in [-1.0 .. 1.0], where 1.0 means images are identical + try { + similarityIndex = imageComparer.compareImages(resultImage, expectedImage); + } catch (...) { + messageBox.critical(0, "Internal error", "Image not in expected format"); + exit(-1); + } + + if (similarityIndex < THRESHOLD) { + mismatchWindow.setTestFailure(TestFailure{ + (float)similarityIndex, + expectedImages[i].left(expectedImages[i].lastIndexOf("/") + 1), // path to the test (including trailing /) + QFileInfo(expectedImages[i].toStdString().c_str()).fileName(), // filename of expected image + QFileInfo(resultImages[i].toStdString().c_str()).fileName() // filename of result image + }); + + mismatchWindow.exec(); + + switch (mismatchWindow.getUserResponse()) { + case USER_RESPONSE_PASS: + break; + case USE_RESPONSE_FAIL: + success = false; + break; + case USER_RESPONSE_ABORT: + keepOn = false; + success = false; + break; + default: + assert(false); + break; + } + } + } + + return success; +} + +void Test::evaluateTests() { + // Get list of JPEG images in folder, sorted by name + QString pathToImageDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images", ".", QFileDialog::ShowDirsOnly); + if (pathToImageDirectory == "") { + return; + } + + QStringList sortedImageFilenames = createListOfAllJPEGimagesInDirectory(pathToImageDirectory); + + // Separate images into two lists. The first is the expected images, the second is the test results + // Images that are in the wrong format are ignored. + QStringList expectedImages; + QStringList resultImages; + foreach(QString currentFilename, sortedImageFilenames) { + QString fullCurrentFilename = pathToImageDirectory + "/" + currentFilename; + if (isInExpectedImageFilenameFormat(currentFilename)) { + expectedImages << fullCurrentFilename; + } else if (isInSnapshotFilenameFormat(currentFilename)) { + resultImages << fullCurrentFilename; + } + } + + // The number of images in each list should be identical + if (expectedImages.length() != resultImages.length()) { + messageBox.critical(0, + "Test failed", + "Found " + QString::number(resultImages.length()) + " images in directory" + + "\nExpected to find " + QString::number(expectedImages.length()) + " images" + ); + + exit(-1); + } + + bool success = compareImageLists(expectedImages, resultImages); + + if (success) { + messageBox.information(0, "Success", "All images are as expected"); + } else { + messageBox.information(0, "Failure", "One or more images are not as expected"); + } +} + +// Two criteria are used to decide if a folder contains valid test results. +// 1) a 'test'js' file exists in the folder +// 2) the folder has the same number of actual and expected images +void Test::evaluateTestsRecursively() { + // Select folder to start recursing from + QString topLevelDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder that will contain the top level test script", ".", QFileDialog::ShowDirsOnly); + if (topLevelDirectory == "") { + return; + } + + bool success{ true }; + QDirIterator it(topLevelDirectory.toStdString().c_str(), QDirIterator::Subdirectories); + while (it.hasNext()) { + QString directory = it.next(); + if (directory[directory.length() - 1] == '.') { + // ignore '.', '..' directories + continue; + } + + // + const QString testPathname{ directory + "/" + testFilename }; + QFileInfo fileInfo(testPathname); + if (!fileInfo.exists()) { + // Folder does not contain 'test.js' + continue; + } + + QStringList sortedImageFilenames = createListOfAllJPEGimagesInDirectory(directory); + + // Separate images into two lists. The first is the expected images, the second is the test results + // Images that are in the wrong format are ignored. + QStringList expectedImages; + QStringList resultImages; + foreach(QString currentFilename, sortedImageFilenames) { + QString fullCurrentFilename = directory + "/" + currentFilename; + if (isInExpectedImageFilenameFormat(currentFilename)) { + expectedImages << fullCurrentFilename; + } else if (isInSnapshotFilenameFormat(currentFilename)) { + resultImages << fullCurrentFilename; + } + } + + if (expectedImages.length() != resultImages.length()) { + // Number of images doesn't match + continue; + } + + // Set success to false if any test has failed + success &= compareImageLists(expectedImages, resultImages); + } + + if (success) { + messageBox.information(0, "Success", "All images are as expected"); + } else { + messageBox.information(0, "Failure", "One or more images are not as expected"); + } +} + +void Test::importTest(QTextStream& textStream, const QString& testPathname, int testNumber) { + textStream << "var test" << testNumber << " = Script.require(\"" << "file:///" << testPathname + "\");" << endl; +} + +// Creates a single script in a user-selected folder. +// This script will run all text.js scripts in every applicable sub-folder +void Test::createRecursiveScript() { + // Select folder to start recursing from + QString topLevelDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder that will contain the top level test script", ".", QFileDialog::ShowDirsOnly); + if (topLevelDirectory == "") { + return; + } + + QFile allTestsFilename(topLevelDirectory + "/" + "allTests.js"); + if (!allTestsFilename.open(QIODevice::WriteOnly | QIODevice::Text)) { + messageBox.critical(0, + "Internal Error", + "Failed to create \"allTests.js\" in directory \"" + topLevelDirectory + "\""); + + exit(-1); + } + + QTextStream textStream(&allTestsFilename); + textStream << "// This is an automatically generated file, created by auto-tester" << endl; + + // The main will call each test after the previous test is completed + // This is implemented with an interval timer that periodically tests if a + // running test has increment a testNumber variable that it received as an input. + int testNumber = 1; + QVector testPathnames; + + // First test if top-level folder has a test.js file + const QString testPathname{ topLevelDirectory + "/" + testFilename }; + QFileInfo fileInfo(testPathname); + if (fileInfo.exists()) { + // Current folder contains a test + importTest(textStream, testPathname, testNumber); + ++testNumber; + + testPathnames << testPathname; + } + + QDirIterator it(topLevelDirectory.toStdString().c_str(), QDirIterator::Subdirectories); + while (it.hasNext()) { + QString directory = it.next(); + if (directory[directory.length() - 1] == '.') { + // ignore '.', '..' directories + continue; + } + + const QString testPathname{ directory + "/" + testFilename }; + QFileInfo fileInfo(testPathname); + if (fileInfo.exists()) { + // Current folder contains a test + importTest(textStream, testPathname, testNumber); + ++testNumber; + + testPathnames << testPathname; + } + } + + if (testPathnames.length() <= 0) { + messageBox.information(0, "Failure", "No \"test.js\" files found"); + allTestsFilename.close(); + return; + } + + textStream << endl; + + // Define flags for each test + for (int i = 1; i <= testPathnames.length(); ++i) { + textStream << "var test" << i << "HasNotStarted = true;" << endl; + } + + // Leave a blank line in the main + textStream << endl; + + const int TEST_PERIOD = 1000; // in milliseconds + const QString tab = " "; + + textStream << "// Check every second if the current test is complete and the next test can be run" << endl; + textStream << "var testTimer = Script.setInterval(" << endl; + textStream << tab << "function() {" << endl; + + const QString testFunction = "test"; + for (int i = 1; i <= testPathnames.length(); ++i) { + // First test starts immediately, all other tests wait for the previous test to complete. + // The script produced will look as follows: + // if (test1HasNotStarted) { + // test1HasNotStarted = false; + // test1.test(); + // print("******started test 1******"); + // } + // | + // | + // if (test5.complete && test6HasNotStarted) { + // test6HasNotStarted = false; + // test7.test(); + // print("******started test 6******"); + // } + // | + // | + // if (test12.complete) { + // print("******stopping******"); + // Script.stop(); + // } + // + if (i == 1) { + textStream << tab << tab << "if (test1HasNotStarted) {" << endl; + } else { + textStream << tab << tab << "if (test" << i - 1 << ".complete && test" << i << "HasNotStarted) {" << endl; + } + textStream << tab << tab << tab << "test" << i << "HasNotStarted = false;" << endl; + textStream << tab << tab << tab << "test" << i << "." << testFunction << "();" << endl; + textStream << tab << tab << tab << "print(\"******started test " << i << "******\");" << endl; + + textStream << tab << tab << "}" << endl << endl; + + } + + // Add extra step to stop the script + textStream << tab << tab << "if (test" << testPathnames.length() << ".complete) {" << endl; + textStream << tab << tab << tab << "print(\"******stopping******\");" << endl; + textStream << tab << tab << tab << "Script.stop();" << endl; + textStream << tab << tab << "}" << endl << endl; + + textStream << tab << "}," << endl; + textStream << endl; + textStream << tab << TEST_PERIOD << endl; + textStream << ");" << endl << endl; + + textStream << "// Stop the timer and clear the module cache" << endl; + textStream << "Script.scriptEnding.connect(" << endl; + textStream << tab << "function() {" << endl; + textStream << tab << tab << "Script.clearInterval(testTimer);" << endl; + textStream << tab << tab << "Script.require.cache = {};" << endl; + textStream << tab << "}" << endl; + textStream << ");" << endl; + + allTestsFilename.close(); + + messageBox.information(0, "Success", "Script has been created"); +} + +void Test::createTest() { + // Rename files sequentially, as ExpectedResult_1.jpeg, ExpectedResult_2.jpg and so on + // Any existing expected result images will be deleted + QString pathToImageDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images", ".", QFileDialog::ShowDirsOnly); + if (pathToImageDirectory == "") { + return; + } + + QStringList sortedImageFilenames = createListOfAllJPEGimagesInDirectory(pathToImageDirectory); + + int i = 1; + foreach (QString currentFilename, sortedImageFilenames) { + QString fullCurrentFilename = pathToImageDirectory + "/" + currentFilename; + if (isInExpectedImageFilenameFormat(currentFilename)) { + if (!QFile::remove(fullCurrentFilename)) { + messageBox.critical(0, "Error", "Could not delete existing file: " + currentFilename + "\nTest creation aborted"); + exit(-1); + } + } else if (isInSnapshotFilenameFormat(currentFilename)) { + const int MAX_IMAGES = 100000; + if (i >= MAX_IMAGES) { + messageBox.critical(0, "Error", "More than 100,000 images not supported"); + exit(-1); + } + QString newFilename = "ExpectedImage_" + QString::number(i-1).rightJustified(5, '0') + ".jpg"; + QString fullNewFileName = pathToImageDirectory + "/" + newFilename; + + if (!imageDirectory.rename(fullCurrentFilename, newFilename)) { + if (!QFile::exists(fullCurrentFilename)) { + messageBox.critical(0, "Error", "Could not rename file: " + fullCurrentFilename + " to: " + newFilename + "\n" + + fullCurrentFilename + " not found" + + "\nTest creation aborted" + ); + exit(-1); + } else { + messageBox.critical(0, "Error", "Could not rename file: " + fullCurrentFilename + " to: " + newFilename + "\n" + + "unknown error" + "\nTest creation aborted" + ); + exit(-1); + } + } + ++i; + } + } + + messageBox.information(0, "Success", "Test images have been created"); +} + +QStringList Test::createListOfAllJPEGimagesInDirectory(QString pathToImageDirectory) { + imageDirectory = QDir(pathToImageDirectory); + QStringList nameFilters; + nameFilters << "*.jpg"; + + return imageDirectory.entryList(nameFilters, QDir::Files, QDir::Name); +} + +bool Test::isInSnapshotFilenameFormat(QString filename) { + return (snapshotFilenameFormat.match(filename).hasMatch()); +} + +bool Test::isInExpectedImageFilenameFormat(QString filename) { + return (expectedImageFilenameFormat.match(filename).hasMatch()); +} \ No newline at end of file diff --git a/tools/auto-tester/src/Test.h b/tools/auto-tester/src/Test.h new file mode 100644 index 0000000000..1f7b1e92a7 --- /dev/null +++ b/tools/auto-tester/src/Test.h @@ -0,0 +1,55 @@ +// +// Test.h +// zone/ambientLightInheritence +// +// Created by Nissim Hadar on 2 Nov 2017. +// Copyright 2013 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_test_h +#define hifi_test_h + +#include +#include +#include + +#include "ImageComparer.h" +#include "ui/MismatchWindow.h" + +class Test { +public: + Test(); + + void evaluateTests(); + void evaluateTestsRecursively(); + void createRecursiveScript(); + void createTest(); + + QStringList createListOfAllJPEGimagesInDirectory(QString pathToImageDirectory); + + bool isInSnapshotFilenameFormat(QString filename); + bool isInExpectedImageFilenameFormat(QString filename); + + void importTest(QTextStream& textStream, const QString& testPathname, int testNumber); + +private: + const QString testFilename{ "test.js" }; + + QMessageBox messageBox; + + QDir imageDirectory; + + QRegularExpression snapshotFilenameFormat; + QRegularExpression expectedImageFilenameFormat; + + MismatchWindow mismatchWindow; + + ImageComparer imageComparer; + + bool compareImageLists(QStringList expectedImages, QStringList resultImages); +}; + +#endif // hifi_test_h diff --git a/tools/auto-tester/src/common.h b/tools/auto-tester/src/common.h new file mode 100644 index 0000000000..126177358f --- /dev/null +++ b/tools/auto-tester/src/common.h @@ -0,0 +1,37 @@ +// +// common.h +// +// Created by Nissim Hadar on 10 Nov 2017. +// Copyright 2013 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_common_h +#define hifi_common_h + +#include + +class TestFailure { +public: + TestFailure(float error, QString pathname, QString expectedImageFilename, QString actualImageFilename) : + _error(error), + _pathname(pathname), + _expectedImageFilename(expectedImageFilename), + _actualImageFilename(actualImageFilename) + {} + + double _error; + QString _pathname; + QString _expectedImageFilename; + QString _actualImageFilename; +}; + +enum UserResponse { + USER_RESPONSE_INVALID, + USER_RESPONSE_PASS, + USE_RESPONSE_FAIL, + USER_RESPONSE_ABORT +}; + +#endif // hifi_common_h diff --git a/tools/auto-tester/src/main.cpp b/tools/auto-tester/src/main.cpp new file mode 100644 index 0000000000..6e5e06b732 --- /dev/null +++ b/tools/auto-tester/src/main.cpp @@ -0,0 +1,20 @@ +// +// main.cpp +// +// Created by Nissim Hadar on 2 Nov 2017. +// Copyright 2013 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 "ui/AutoTester.h" + +int main(int argc, char *argv[]) { + QApplication application(argc, argv); + + AutoTester autoTester; + autoTester.show(); + + return application.exec(); +} diff --git a/tools/auto-tester/src/ui/AutoTester.cpp b/tools/auto-tester/src/ui/AutoTester.cpp new file mode 100644 index 0000000000..105baddb92 --- /dev/null +++ b/tools/auto-tester/src/ui/AutoTester.cpp @@ -0,0 +1,35 @@ +// +// AutoTester.cpp +// zone/ambientLightInheritence +// +// Created by Nissim Hadar on 2 Nov 2017. +// Copyright 2013 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 "AutoTester.h" + +AutoTester::AutoTester(QWidget *parent) : QMainWindow(parent) { + ui.setupUi(this); +} + +void AutoTester::on_evaluateTestsButton_clicked() { + test.evaluateTests(); +} + +void AutoTester::on_evaluateTestsRecursivelyButton_clicked() { + test.evaluateTestsRecursively(); +} + +void AutoTester::on_createRecursiveScriptButton_clicked() { + test.createRecursiveScript(); +} + +void AutoTester::on_createTestButton_clicked() { + test.createTest(); +} + +void AutoTester::on_closeButton_clicked() { + exit(0); +} \ No newline at end of file diff --git a/tools/auto-tester/src/ui/AutoTester.h b/tools/auto-tester/src/ui/AutoTester.h new file mode 100644 index 0000000000..acfea32ba1 --- /dev/null +++ b/tools/auto-tester/src/ui/AutoTester.h @@ -0,0 +1,37 @@ +// +// AutoTester.h +// zone/ambientLightInheritence +// +// Created by Nissim Hadar on 2 Nov 2017. +// Copyright 2013 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_AutoTester_h +#define hifi_AutoTester_h + +#include +#include "ui_AutoTester.h" +#include "../Test.h" + +class AutoTester : public QMainWindow { + Q_OBJECT + +public: + AutoTester(QWidget *parent = Q_NULLPTR); + +private slots: +void on_evaluateTestsButton_clicked(); +void on_evaluateTestsRecursivelyButton_clicked(); +void on_createRecursiveScriptButton_clicked(); + void on_createTestButton_clicked(); + void on_closeButton_clicked(); + +private: + Ui::AutoTesterClass ui; + + Test test; +}; + +#endif // hifi_AutoTester_h \ No newline at end of file diff --git a/tools/auto-tester/src/ui/AutoTester.ui b/tools/auto-tester/src/ui/AutoTester.ui new file mode 100644 index 0000000000..7032ef9710 --- /dev/null +++ b/tools/auto-tester/src/ui/AutoTester.ui @@ -0,0 +1,106 @@ + + + AutoTesterClass + + + + 0 + 0 + 286 + 470 + + + + AutoTester + + + + + + 60 + 360 + 160 + 40 + + + + Close + + + + + + 60 + 270 + 160 + 40 + + + + Create Test + + + + + + 60 + 20 + 160 + 40 + + + + Evaluate Test + + + + + + 60 + 210 + 160 + 40 + + + + Create Recursive Script + + + + + + 60 + 75 + 160 + 40 + + + + Evaluate Tests Recursively + + + + + + + 0 + 0 + 286 + 21 + + + + + + TopToolBarArea + + + false + + + + + + + + diff --git a/tools/auto-tester/src/ui/MismatchWindow.cpp b/tools/auto-tester/src/ui/MismatchWindow.cpp new file mode 100644 index 0000000000..07664a1667 --- /dev/null +++ b/tools/auto-tester/src/ui/MismatchWindow.cpp @@ -0,0 +1,46 @@ +// +// MismatchWindow.cpp +// +// Created by Nissim Hadar on 9 Nov 2017. +// Copyright 2013 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 "MismatchWindow.h" + +#include + +MismatchWindow::MismatchWindow(QWidget *parent) : QDialog(parent) { + setupUi(this); + + expectedImage->setScaledContents(true); + resultImage->setScaledContents(true); +} + +void MismatchWindow::setTestFailure(TestFailure testFailure) { + errorLabel->setText("Similarity: " + QString::number(testFailure._error)); + + imagePath->setText("Path to test: " + testFailure._pathname); + + expectedFilename->setText(testFailure._expectedImageFilename); + expectedImage->setPixmap(QPixmap(testFailure._pathname + testFailure._expectedImageFilename)); + + resultFilename->setText(testFailure._actualImageFilename); + resultImage->setPixmap(QPixmap(testFailure._pathname + testFailure._actualImageFilename)); +} + +void MismatchWindow::on_passTestButton_clicked() { + _userResponse = USER_RESPONSE_PASS; + close(); +} + +void MismatchWindow::on_failTestButton_clicked() { + _userResponse = USE_RESPONSE_FAIL; + close(); +} + +void MismatchWindow::on_abortTestsButton_clicked() { + _userResponse = USER_RESPONSE_ABORT; + close(); +} diff --git a/tools/auto-tester/src/ui/MismatchWindow.h b/tools/auto-tester/src/ui/MismatchWindow.h new file mode 100644 index 0000000000..7c72b7b0b7 --- /dev/null +++ b/tools/auto-tester/src/ui/MismatchWindow.h @@ -0,0 +1,38 @@ +// +// MismatchWindow.h +// +// Created by Nissim Hadar on 9 Nov 2017. +// Copyright 2013 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_MismatchWindow_h +#define hifi_MismatchWindow_h + +#include "ui_MismatchWindow.h" + +#include "../common.h" + +class MismatchWindow : public QDialog, public Ui::MismatchWindow +{ + Q_OBJECT + +public: + MismatchWindow(QWidget *parent = Q_NULLPTR); + + void setTestFailure(TestFailure testFailure); + + UserResponse getUserResponse() { return _userResponse; } + +private slots: + void on_passTestButton_clicked(); + void on_failTestButton_clicked(); + void on_abortTestsButton_clicked(); + +private: + UserResponse _userResponse{ USER_RESPONSE_INVALID }; +}; + + +#endif // hifi_MismatchWindow_h diff --git a/tools/auto-tester/src/ui/MismatchWindow.ui b/tools/auto-tester/src/ui/MismatchWindow.ui new file mode 100644 index 0000000000..cab6c61e1c --- /dev/null +++ b/tools/auto-tester/src/ui/MismatchWindow.ui @@ -0,0 +1,157 @@ + + + MismatchWindow + + + + 0 + 0 + 1585 + 694 + + + + MismatchWindow + + + + + 20 + 170 + 720 + 362 + + + + expected image + + + + + + 760 + 170 + 720 + 362 + + + + result image + + + + + + 760 + 90 + 800 + 28 + + + + + 16 + + + + result image filename + + + + + + 40 + 90 + 700 + 28 + + + + + 16 + + + + expected image filename + + + + + + 40 + 30 + 1200 + 28 + + + + + 16 + + + + image path + + + + + + 30 + 600 + 75 + 23 + + + + Pass + + + + + + 330 + 600 + 75 + 23 + + + + Fail + + + + + + 630 + 600 + 75 + 23 + + + + Abort Tests + + + + + + 810 + 600 + 720 + 28 + + + + + 16 + + + + similarity + + + + + + + diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 1022c204c5..321f81ba8f 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -17,4 +17,4 @@ if (UNIX) endif() endif () -set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE) +install_beside_console() diff --git a/tools/oven/src/BakerCLI.cpp b/tools/oven/src/BakerCLI.cpp index 5ab995be95..5af65c4dc0 100644 --- a/tools/oven/src/BakerCLI.cpp +++ b/tools/oven/src/BakerCLI.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "ModelBakingLoggingCategory.h" #include "Oven.h" @@ -22,22 +23,30 @@ BakerCLI::BakerCLI(Oven* parent) : QObject(parent) { } -void BakerCLI::bakeFile(QUrl inputUrl, const QString outputPath) { +void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString& type) { // if the URL doesn't have a scheme, assume it is a local file if (inputUrl.scheme() != "http" && inputUrl.scheme() != "https" && inputUrl.scheme() != "ftp") { inputUrl.setScheme("file"); } - static const QString MODEL_EXTENSION { ".fbx" }; + qDebug() << "Baking file type: " << type; + + static const QString MODEL_EXTENSION { "fbx" }; + + QString extension = type; + + if (extension.isNull()) { + auto url = inputUrl.toDisplayString(); + extension = url.mid(url.lastIndexOf('.')); + } // check what kind of baker we should be creating - bool isFBX = inputUrl.toDisplayString().endsWith(MODEL_EXTENSION, Qt::CaseInsensitive); - bool isSupportedImage = false; + bool isFBX = extension == MODEL_EXTENSION; - for (QByteArray format : QImageReader::supportedImageFormats()) { - isSupportedImage |= inputUrl.toDisplayString().endsWith(format, Qt::CaseInsensitive); - } + bool isSupportedImage = QImageReader::supportedImageFormats().contains(extension.toLatin1()); + + _outputPath = outputPath; // create our appropiate baker if (isFBX) { @@ -48,7 +57,7 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString outputPath) { _baker->moveToThread(qApp->getNextWorkerThread()); } else { qCDebug(model_baking) << "Failed to determine baker type for file" << inputUrl; - QApplication::exit(1); + QApplication::exit(OVEN_STATUS_CODE_FAIL); } // invoke the bake method on the baker thread @@ -60,5 +69,17 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString outputPath) { void BakerCLI::handleFinishedBaker() { qCDebug(model_baking) << "Finished baking file."; - QApplication::exit(_baker.get()->hasErrors()); + int exitCode = OVEN_STATUS_CODE_SUCCESS; + // Do we need this? + if (_baker->wasAborted()) { + exitCode = OVEN_STATUS_CODE_ABORT; + } else if (_baker->hasErrors()) { + exitCode = OVEN_STATUS_CODE_FAIL; + QFile errorFile { _outputPath.absoluteFilePath(OVEN_ERROR_FILENAME) }; + if (errorFile.open(QFile::WriteOnly)) { + errorFile.write(_baker->getErrors().join('\n').toUtf8()); + errorFile.close(); + } + } + QApplication::exit(exitCode); } diff --git a/tools/oven/src/BakerCLI.h b/tools/oven/src/BakerCLI.h index cb2b908059..7d362eb898 100644 --- a/tools/oven/src/BakerCLI.h +++ b/tools/oven/src/BakerCLI.h @@ -13,22 +13,32 @@ #define hifi_BakerCLI_h #include +#include + +#include #include "Baker.h" #include "Oven.h" +static const int OVEN_STATUS_CODE_SUCCESS { 0 }; +static const int OVEN_STATUS_CODE_FAIL { 1 }; +static const int OVEN_STATUS_CODE_ABORT { 2 }; + +static const QString OVEN_ERROR_FILENAME = "errors.txt"; + class BakerCLI : public QObject { Q_OBJECT public: BakerCLI(Oven* parent); - void bakeFile(QUrl inputUrl, const QString outputPath); + void bakeFile(QUrl inputUrl, const QString& outputPath, const QString& type = QString::null); private slots: void handleFinishedBaker(); private: + QDir _outputPath; std::unique_ptr _baker; }; -#endif // hifi_BakerCLI_h \ No newline at end of file +#endif // hifi_BakerCLI_h diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index d91206a592..9de06a35bb 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -24,6 +24,7 @@ static const QString OUTPUT_FOLDER = "/Users/birarda/code/hifi/lod/test-oven/exp static const QString CLI_INPUT_PARAMETER = "i"; static const QString CLI_OUTPUT_PARAMETER = "o"; +static const QString CLI_TYPE_PARAMETER = "t"; Oven::Oven(int argc, char* argv[]) : QApplication(argc, argv) @@ -39,7 +40,8 @@ Oven::Oven(int argc, char* argv[]) : parser.addOptions({ { CLI_INPUT_PARAMETER, "Path to file that you would like to bake.", "input" }, - { CLI_OUTPUT_PARAMETER, "Path to folder that will be used as output.", "output" } + { CLI_OUTPUT_PARAMETER, "Path to folder that will be used as output.", "output" }, + { CLI_TYPE_PARAMETER, "Type of asset.", "type" } }); parser.addHelpOption(); parser.process(*this); @@ -59,7 +61,8 @@ Oven::Oven(int argc, char* argv[]) : BakerCLI* cli = new BakerCLI(this); QUrl inputUrl(QDir::fromNativeSeparators(parser.value(CLI_INPUT_PARAMETER))); QUrl outputUrl(QDir::fromNativeSeparators(parser.value(CLI_OUTPUT_PARAMETER))); - cli->bakeFile(inputUrl, outputUrl.toString()); + QString type = parser.isSet(CLI_TYPE_PARAMETER) ? parser.value(CLI_TYPE_PARAMETER) : QString::null; + cli->bakeFile(inputUrl, outputUrl.toString(), type); } else { parser.showHelp(); QApplication::quit(); diff --git a/tools/oven/src/ui/BakeWidget.h b/tools/oven/src/ui/BakeWidget.h index 00996128ed..34cced537a 100644 --- a/tools/oven/src/ui/BakeWidget.h +++ b/tools/oven/src/ui/BakeWidget.h @@ -14,6 +14,8 @@ #include +#include + #include class BakeWidget : public QWidget {