From a4b31802716b1c36135eefb49808ff0c8816a107 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 23 Oct 2017 14:48:25 -0700 Subject: [PATCH 01/58] Fix sparse rectification for small textures --- libraries/image/src/image/Image.cpp | 52 +++++++++++++---------------- tests/gpu-test/src/main.cpp | 41 ++++++++--------------- 2 files changed, 37 insertions(+), 56 deletions(-) diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index 9f584c844f..42480450a5 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -19,15 +19,6 @@ #include #include - -#if defined(Q_OS_ANDROID) -#define CPU_MIPMAPS 0 -#else -#define CPU_MIPMAPS 1 -#include -#endif - - #include #include #include @@ -37,6 +28,12 @@ using namespace gpu; +#if defined(Q_OS_ANDROID) +#define CPU_MIPMAPS 0 +#else +#define CPU_MIPMAPS 1 +#include +#endif static const glm::uvec2 SPARSE_PAGE_SIZE(128); static const glm::uvec2 MAX_TEXTURE_SIZE(4096); @@ -51,25 +48,21 @@ static std::atomic compressNormalTextures { false }; static std::atomic compressGrayscaleTextures { false }; static std::atomic compressCubeTextures { false }; -bool needsSparseRectification(const glm::uvec2& size) { - // Don't attempt to rectify small textures (textures less than the sparse page size in any dimension) - if (glm::any(glm::lessThan(size, SPARSE_PAGE_SIZE))) { - return false; +uint rectifyDimension(const uint& dimension) { + if (dimension < SPARSE_PAGE_SIZE.x) { + uint newSize = SPARSE_PAGE_SIZE.x; + while (dimension <= newSize / 2) { + newSize /= 2; + } + return newSize; + } else { + uint pages = (dimension / SPARSE_PAGE_SIZE.x) + (dimension % SPARSE_PAGE_SIZE.x == 0 ? 0 : 1); + return pages * SPARSE_PAGE_SIZE.x; } - - // Don't rectify textures that are already an exact multiple of sparse page size - if (glm::uvec2(0) == (size % SPARSE_PAGE_SIZE)) { - return false; - } - - // Texture is not sparse compatible, but is bigger than the sparse page size in both dimensions, rectify! - return true; } -glm::uvec2 rectifyToSparseSize(const glm::uvec2& size) { - glm::uvec2 pages = ((size / SPARSE_PAGE_SIZE) + glm::clamp(size % SPARSE_PAGE_SIZE, glm::uvec2(0), glm::uvec2(1))); - glm::uvec2 result = pages * SPARSE_PAGE_SIZE; - return result; +glm::uvec2 rectifySize(const glm::uvec2& size) { + return { rectifyDimension(size.x), rectifyDimension(size.y) }; } @@ -307,9 +300,12 @@ QImage processSourceImage(const QImage& srcImage, bool cubemap) { ++DECIMATED_TEXTURE_COUNT; } - if (!cubemap && needsSparseRectification(targetSize)) { - ++RECTIFIED_TEXTURE_COUNT; - targetSize = rectifyToSparseSize(targetSize); + if (!cubemap) { + auto rectifiedSize = rectifySize(targetSize); + if (rectifiedSize != targetSize) { + ++RECTIFIED_TEXTURE_COUNT; + targetSize = rectifiedSize; + } } if (DEV_DECIMATE_TEXTURES && glm::all(glm::greaterThanEqual(targetSize / SPARSE_PAGE_SIZE, glm::uvec2(2)))) { diff --git a/tests/gpu-test/src/main.cpp b/tests/gpu-test/src/main.cpp index d37be7c790..6a509afe4e 100644 --- a/tests/gpu-test/src/main.cpp +++ b/tests/gpu-test/src/main.cpp @@ -163,46 +163,31 @@ class MyTestWindow : public TestWindow { } }; -extern bool needsSparseRectification(const uvec2& size); -extern uvec2 rectifyToSparseSize(const uvec2& size); +extern uvec2 rectifySize(const uvec2& size); void testSparseRectify() { - std::vector> NEEDS_SPARSE_TESTS {{ + std::vector> SPARSE_SIZE_TESTS { // Already sparse - { {1024, 1024 }, false }, - { { 128, 128 }, false }, + { {1024, 1024 }, { 1024, 1024 } }, + { { 128, 128 }, { 128, 128 } }, // Too small in one dimension - { { 127, 127 }, false }, - { { 1, 1 }, false }, - { { 1000, 1 }, false }, - { { 1024, 1 }, false }, - { { 100, 100 }, false }, - // needs rectification - { { 1000, 1000 }, true }, - { { 1024, 1000 }, true }, - } }; - - for (const auto& test : NEEDS_SPARSE_TESTS) { - const auto& size = test.first; - const auto& expected = test.second; - auto result = needsSparseRectification(size); - Q_ASSERT(expected == result); - result = needsSparseRectification(uvec2(size.y, size.x)); - Q_ASSERT(expected == result); - } - - std::vector> SPARSE_SIZE_TESTS { { + { { 127, 127 }, { 128, 128 } }, + { { 1, 1 }, { 1, 1 } }, + { { 1000, 1 }, { 1024, 1 } }, + { { 1024, 1 }, { 1024, 1 } }, + { { 100, 100 }, { 128, 128 } }, + { { 57, 510 }, { 64, 512 } }, // needs rectification { { 1000, 1000 }, { 1024, 1024 } }, { { 1024, 1000 }, { 1024, 1024 } }, - } }; + }; for (const auto& test : SPARSE_SIZE_TESTS) { const auto& size = test.first; const auto& expected = test.second; - auto result = rectifyToSparseSize(size); + auto result = rectifySize(size); Q_ASSERT(expected == result); - result = rectifyToSparseSize(uvec2(size.y, size.x)); + result = rectifySize(uvec2(size.y, size.x)); Q_ASSERT(expected == uvec2(result.y, result.x)); } } From 5688a166f8e245c8b3a7a798e516ab1eaeed03ba Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 5 Dec 2017 12:18:57 -0800 Subject: [PATCH 02/58] fix folder name creation for duplicated models --- tools/oven/src/DomainBaker.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 535d9a49a9..edc2492e82 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -195,9 +195,9 @@ void DomainBaker::enumerateEntities() { auto filename = modelURL.fileName(); auto baseName = filename.left(filename.lastIndexOf('.')); auto subDirName = "/" + baseName; - int i = 0; + int i = 1; while (QDir(_contentOutputPath + subDirName).exists()) { - subDirName = "/" + baseName + "-" + i++; + subDirName = "/" + baseName + "-" + QString::number(i++); } QSharedPointer baker { new FBXBaker(modelURL, []() -> QThread* { From 0f4206d330d47add73fe93c6296f39ff1eb097ef Mon Sep 17 00:00:00 2001 From: Nissim Hadar Date: Wed, 10 Jan 2018 18:21:34 -0800 Subject: [PATCH 03/58] Added option to delete old snapshots. --- tools/auto-tester/CMakeLists.txt | 44 ++-- tools/auto-tester/src/ImageComparer.cpp | 8 +- tools/auto-tester/src/Test.cpp | 239 +++++++++++++++++--- tools/auto-tester/src/Test.h | 26 ++- tools/auto-tester/src/common.h | 7 +- tools/auto-tester/src/main.cpp | 2 +- tools/auto-tester/src/ui/AutoTester.cpp | 12 +- tools/auto-tester/src/ui/AutoTester.h | 8 +- tools/auto-tester/src/ui/AutoTester.ui | 80 +++++-- tools/auto-tester/src/ui/MismatchWindow.cpp | 56 ++++- tools/auto-tester/src/ui/MismatchWindow.h | 7 +- tools/auto-tester/src/ui/MismatchWindow.ui | 105 ++++++--- 12 files changed, 459 insertions(+), 135 deletions(-) diff --git a/tools/auto-tester/CMakeLists.txt b/tools/auto-tester/CMakeLists.txt index e5f2c1fb97..a875f5676a 100644 --- a/tools/auto-tester/CMakeLists.txt +++ b/tools/auto-tester/CMakeLists.txt @@ -1,47 +1,41 @@ -set(TARGET_NAME auto-tester) +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) +SET (CMAKE_AUTOUIC ON) +SET (CMAKE_AUTOMOC ON) -setup_hifi_project(Core Widgets) -link_hifi_libraries() +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) + 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}) +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) +set (QT_LIBRARIES Qt5::Core Qt5::Widgets) if (WIN32) # Do not show Console - set_property(TARGET auto-tester PROPERTY WIN32_EXECUTABLE true) + set_property (TARGET auto-tester PROPERTY WIN32_EXECUTABLE true) endif() -add_executable(PROJECT_NAME ${SOURCES} ${HEADERS} ${UIS}) +target_zlib() +add_dependency_external_projects (quazip) +find_package (QuaZip REQUIRED) +target_include_directories( ${TARGET_NAME} SYSTEM PUBLIC ${QUAZIP_INCLUDE_DIRS}) +target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) -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 $ $ -) +package_libraries_for_deployment() if (WIN32) + add_paths_to_fixup_libs (${QUAZIP_DLL_PATH}) + find_program(WINDEPLOYQT_COMMAND windeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) if (NOT WINDEPLOYQT_COMMAND) diff --git a/tools/auto-tester/src/ImageComparer.cpp b/tools/auto-tester/src/ImageComparer.cpp index 121c98e16e..94b95a5ab6 100644 --- a/tools/auto-tester/src/ImageComparer.cpp +++ b/tools/auto-tester/src/ImageComparer.cpp @@ -8,6 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "ImageComparer.h" +#include "common.h" #include @@ -26,11 +27,6 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co 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) @@ -116,4 +112,4 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co } return ssim / windowCounter; -}; +}; \ No newline at end of file diff --git a/tools/auto-tester/src/Test.cpp b/tools/auto-tester/src/Test.cpp index 8cb36fcfca..6c637ab404 100644 --- a/tools/auto-tester/src/Test.cpp +++ b/tools/auto-tester/src/Test.cpp @@ -13,18 +13,62 @@ #include #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"); + 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) { +bool Test::createTestResultsFolderPathIfNeeded(QString directory) { + // The test results folder is located in the root of the tests (i.e. for recursive test evaluation) + if (testResultsFolderPath == "") { + testResultsFolderPath = directory + "/" + TEST_RESULTS_FOLDER; + QDir testResultsFolder(testResultsFolderPath); + + if (testResultsFolder.exists()) { + testResultsFolder.removeRecursively(); + } + + // Create a new test results folder + return QDir().mkdir(testResultsFolderPath); + } else { + return true; + } +} + +void Test::zipAndDeleteTestResultsFolder() { + QString zippedResultsFileName { testResultsFolderPath + ".zip" }; + QFileInfo fileInfo(zippedResultsFileName); + if (!fileInfo.exists()) { + QFile::remove(zippedResultsFileName); + } + + QDir testResultsFolder(testResultsFolderPath); + if (!testResultsFolder.isEmpty()) { + JlCompress::compressDir(testResultsFolderPath + ".zip", testResultsFolderPath); + } + + testResultsFolder.removeRecursively(); + + //In all cases, for the next evaluation + testResultsFolderPath = ""; + index = 1; +} + +bool Test::compareImageLists(QStringList expectedImages, QStringList resultImages, QString testDirectory, bool interactiveMode, QProgressBar* progressBar) { + progressBar->setMinimum(0); + progressBar->setMaximum(expectedImages.length() - 1); + progressBar->setValue(0); + progressBar->setVisible(true); + // 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 }; + const double THRESHOLD { 0.999 }; bool success{ true }; bool keepOn{ true }; for (int i = 0; keepOn && i < expectedImages.length(); ++i) { @@ -45,42 +89,107 @@ bool Test::compareImageLists(QStringList expectedImages, QStringList resultImage } if (similarityIndex < THRESHOLD) { - mismatchWindow.setTestFailure(TestFailure{ + TestFailure testFailure = 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(); + mismatchWindow.setTestFailure(testFailure); - 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; + if (!interactiveMode) { + appendTestResultsToFile(testResultsFolderPath, testFailure, mismatchWindow.getComparisonImage()); + success = false; + } else { + mismatchWindow.exec(); + + switch (mismatchWindow.getUserResponse()) { + case USER_RESPONSE_PASS: + break; + case USE_RESPONSE_FAIL: + appendTestResultsToFile(testResultsFolderPath, testFailure, mismatchWindow.getComparisonImage()); + success = false; + break; + case USER_RESPONSE_ABORT: + keepOn = false; + success = false; + break; + default: + assert(false); + break; + } } } + + progressBar->setValue(i); } + progressBar->setVisible(false); return success; } -void Test::evaluateTests() { +void Test::appendTestResultsToFile(QString testResultsFolderPath, TestFailure testFailure, QPixmap comparisonImage) { + if (!QDir().exists(testResultsFolderPath)) { + messageBox.critical(0, "Internal error", "Folder " + testResultsFolderPath + " not found"); + exit(-1); + } + + QString failureFolderPath { testResultsFolderPath + "/" + "Failure_" + QString::number(index) }; + if (!QDir().mkdir(failureFolderPath)) { + messageBox.critical(0, "Internal error", "Failed to create folder " + failureFolderPath); + exit(-1); + } + ++index; + + QFile descriptionFile(failureFolderPath + "/" + TEST_RESULTS_FILENAME); + if (!descriptionFile.open(QIODevice::ReadWrite)) { + messageBox.critical(0, "Internal error", "Failed to create file " + TEST_RESULTS_FILENAME); + exit(-1); + } + + // Create text file describing the failure + QTextStream stream(&descriptionFile); + stream << "Test failed in folder " << testFailure._pathname.left(testFailure._pathname.length() - 1) << endl; // remove trailing '/' + stream << "Expected image was " << testFailure._expectedImageFilename << endl; + stream << "Actual image was " << testFailure._actualImageFilename << endl; + stream << "Similarity index was " << testFailure._error << endl; + + descriptionFile.close(); + + // Copy expected and actual images, and save the difference image + QString sourceFile; + QString destinationFile; + + sourceFile = testFailure._pathname + testFailure._expectedImageFilename; + destinationFile = failureFolderPath + "/" + "Expected Image.jpg"; + if (!QFile::copy(sourceFile, destinationFile)) { + messageBox.critical(0, "Internal error", "Failed to copy " + sourceFile + " to " + destinationFile); + exit(-1); + } + + sourceFile = testFailure._pathname + testFailure._actualImageFilename; + destinationFile = failureFolderPath + "/" + "Actual Image.jpg"; + if (!QFile::copy(sourceFile, destinationFile)) { + messageBox.critical(0, "Internal error", "Failed to copy " + sourceFile + " to " + destinationFile); + exit(-1); + } + + comparisonImage.save(failureFolderPath + "/" + "Difference Image.jpg"); +} + +void Test::evaluateTests(bool interactiveMode, QProgressBar* progressBar) { // 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; } + // Leave if test results folder could not be created + if (!createTestResultsFolderPathIfNeeded(pathToImageDirectory)) { + return; + } + QStringList sortedImageFilenames = createListOfAllJPEGimagesInDirectory(pathToImageDirectory); // Separate images into two lists. The first is the expected images, the second is the test results @@ -107,36 +216,57 @@ void Test::evaluateTests() { exit(-1); } - bool success = compareImageLists(expectedImages, resultImages); + bool success = compareImageLists(expectedImages, resultImages, pathToImageDirectory, interactiveMode, progressBar); if (success) { messageBox.information(0, "Success", "All images are as expected"); } else { messageBox.information(0, "Failure", "One or more images are not as expected"); } + + zipAndDeleteTestResultsFolder(); +} + +bool Test::isAValidDirectory(QString pathname) { + // Only process directories + QDir dir(pathname); + if (!dir.exists()) { + return false; + } + + // Ignore '.', '..' directories + if (pathname[pathname.length() - 1] == '.') { + return false; + } + + return true; } // 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() { +void Test::evaluateTestsRecursively(bool interactiveMode, QProgressBar* progressBar) { // 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; } + // Leave if test results folder could not be created + if (!createTestResultsFolderPathIfNeeded(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 + + if (!isAValidDirectory(directory)) { continue; } - // - const QString testPathname{ directory + "/" + testFilename }; + const QString testPathname{ directory + "/" + TEST_FILENAME }; QFileInfo fileInfo(testPathname); if (!fileInfo.exists()) { // Folder does not contain 'test.js' @@ -164,7 +294,7 @@ void Test::evaluateTestsRecursively() { } // Set success to false if any test has failed - success &= compareImageLists(expectedImages, resultImages); + success &= compareImageLists(expectedImages, resultImages, directory, interactiveMode, progressBar); } if (success) { @@ -172,6 +302,8 @@ void Test::evaluateTestsRecursively() { } else { messageBox.information(0, "Failure", "One or more images are not as expected"); } + + zipAndDeleteTestResultsFolder(); } void Test::importTest(QTextStream& textStream, const QString& testPathname, int testNumber) { @@ -191,7 +323,8 @@ void Test::createRecursiveScript() { if (!allTestsFilename.open(QIODevice::WriteOnly | QIODevice::Text)) { messageBox.critical(0, "Internal Error", - "Failed to create \"allTests.js\" in directory \"" + topLevelDirectory + "\""); + "Failed to create \"allTests.js\" in directory \"" + topLevelDirectory + "\"" + ); exit(-1); } @@ -206,7 +339,7 @@ void Test::createRecursiveScript() { QVector testPathnames; // First test if top-level folder has a test.js file - const QString testPathname{ topLevelDirectory + "/" + testFilename }; + const QString testPathname{ topLevelDirectory + "/" + TEST_FILENAME }; QFileInfo fileInfo(testPathname); if (fileInfo.exists()) { // Current folder contains a test @@ -219,12 +352,14 @@ void Test::createRecursiveScript() { QDirIterator it(topLevelDirectory.toStdString().c_str(), QDirIterator::Subdirectories); while (it.hasNext()) { QString directory = it.next(); - if (directory[directory.length() - 1] == '.') { - // ignore '.', '..' directories + + // Only process directories + QDir dir(directory); + if (!isAValidDirectory(directory)) { continue; } - const QString testPathname{ directory + "/" + testFilename }; + const QString testPathname{ directory + "/" + TEST_FILENAME }; QFileInfo fileInfo(testPathname); if (fileInfo.exists()) { // Current folder contains a test @@ -264,7 +399,7 @@ void Test::createRecursiveScript() { // The script produced will look as follows: // if (test1HasNotStarted) { // test1HasNotStarted = false; - // test1.test(); + // test1.test("auto"); // print("******started test 1******"); // } // | @@ -287,7 +422,7 @@ void Test::createRecursiveScript() { 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 << "test" << i << "." << testFunction << "(\"auto\");" << endl; textStream << tab << tab << tab << "print(\"******started test " << i << "******\");" << endl; textStream << tab << tab << "}" << endl << endl; @@ -366,6 +501,41 @@ void Test::createTest() { messageBox.information(0, "Success", "Test images have been created"); } +void Test::deleteOldSnapshots() { + // Select folder to start recursing from + QString topLevelDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select root folder for snapshot deletion", ".", QFileDialog::ShowDirsOnly); + if (topLevelDirectory == "") { + return; + } + + // Recurse over folders + QDirIterator it(topLevelDirectory.toStdString().c_str(), QDirIterator::Subdirectories); + while (it.hasNext()) { + QString directory = it.next(); + + // Only process directories + QDir dir(directory); + if (!isAValidDirectory(directory)) { + continue; + } + + QStringList sortedImageFilenames = createListOfAllJPEGimagesInDirectory(directory); + + // Delete any file that is a snapshot (NOT the Expected Images) + QStringList expectedImages; + QStringList resultImages; + foreach(QString currentFilename, sortedImageFilenames) { + QString fullCurrentFilename = directory + "/" + currentFilename; + if (isInSnapshotFilenameFormat(currentFilename)) { + if (!QFile::remove(fullCurrentFilename)) { + messageBox.critical(0, "Error", "Could not delete existing file: " + currentFilename + "\nSnapshot deletion aborted"); + exit(-1); + } + } + } + } +} + QStringList Test::createListOfAllJPEGimagesInDirectory(QString pathToImageDirectory) { imageDirectory = QDir(pathToImageDirectory); QStringList nameFilters; @@ -374,6 +544,7 @@ QStringList Test::createListOfAllJPEGimagesInDirectory(QString pathToImageDirect return imageDirectory.entryList(nameFilters, QDir::Files, QDir::Name); } +// Use regular expressions to check if files are in specific format bool Test::isInSnapshotFilenameFormat(QString filename) { return (snapshotFilenameFormat.match(filename).hasMatch()); } diff --git a/tools/auto-tester/src/Test.h b/tools/auto-tester/src/Test.h index 1f7b1e92a7..aa1346fa2a 100644 --- a/tools/auto-tester/src/Test.h +++ b/tools/auto-tester/src/Test.h @@ -1,6 +1,5 @@ // // Test.h -// zone/ambientLightInheritence // // Created by Nissim Hadar on 2 Nov 2017. // Copyright 2013 High Fidelity, Inc. @@ -15,6 +14,7 @@ #include #include #include +#include #include "ImageComparer.h" #include "ui/MismatchWindow.h" @@ -23,10 +23,13 @@ class Test { public: Test(); - void evaluateTests(); - void evaluateTestsRecursively(); + void evaluateTests(bool interactiveMode, QProgressBar* progressBar); + void evaluateTestsRecursively(bool interactiveMode, QProgressBar* progressBar); void createRecursiveScript(); void createTest(); + void deleteOldSnapshots(); + + bool compareImageLists(QStringList expectedImages, QStringList resultImages, QString testDirectory, bool interactiveMode, QProgressBar* progressBar); QStringList createListOfAllJPEGimagesInDirectory(QString pathToImageDirectory); @@ -35,8 +38,17 @@ public: void importTest(QTextStream& textStream, const QString& testPathname, int testNumber); + void appendTestResultsToFile(QString testResultsFolderPath, TestFailure testFailure, QPixmap comparisonImage); + + bool createTestResultsFolderPathIfNeeded(QString directory); + void zipAndDeleteTestResultsFolder(); + + bool isAValidDirectory(QString pathname); + private: - const QString testFilename{ "test.js" }; + const QString TEST_FILENAME { "test.js" }; + const QString TEST_RESULTS_FOLDER { "TestResults" }; + const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; QMessageBox messageBox; @@ -49,7 +61,9 @@ private: ImageComparer imageComparer; - bool compareImageLists(QStringList expectedImages, QStringList resultImages); + + QString testResultsFolderPath { "" }; + int index { 1 }; }; -#endif // hifi_test_h +#endif // hifi_test_h \ No newline at end of file diff --git a/tools/auto-tester/src/common.h b/tools/auto-tester/src/common.h index 126177358f..939814df62 100644 --- a/tools/auto-tester/src/common.h +++ b/tools/auto-tester/src/common.h @@ -34,4 +34,9 @@ enum UserResponse { USER_RESPONSE_ABORT }; -#endif // hifi_common_h +// Coefficients for luminosity calculation +const double R_Y = 0.212655f; +const double G_Y = 0.715158f; +const double B_Y = 0.072187f; + +#endif // hifi_common_h \ No newline at end of file diff --git a/tools/auto-tester/src/main.cpp b/tools/auto-tester/src/main.cpp index 6e5e06b732..45a3743482 100644 --- a/tools/auto-tester/src/main.cpp +++ b/tools/auto-tester/src/main.cpp @@ -17,4 +17,4 @@ int main(int argc, char *argv[]) { autoTester.show(); return application.exec(); -} +} \ No newline at end of file diff --git a/tools/auto-tester/src/ui/AutoTester.cpp b/tools/auto-tester/src/ui/AutoTester.cpp index 105baddb92..2834ff81e0 100644 --- a/tools/auto-tester/src/ui/AutoTester.cpp +++ b/tools/auto-tester/src/ui/AutoTester.cpp @@ -12,14 +12,18 @@ AutoTester::AutoTester(QWidget *parent) : QMainWindow(parent) { ui.setupUi(this); + + ui.checkBoxInteractiveMode->setChecked(true); + + ui.progressBar->setVisible(false); } void AutoTester::on_evaluateTestsButton_clicked() { - test.evaluateTests(); + test.evaluateTests(ui.checkBoxInteractiveMode->isChecked(), ui.progressBar); } void AutoTester::on_evaluateTestsRecursivelyButton_clicked() { - test.evaluateTestsRecursively(); + test.evaluateTestsRecursively(ui.checkBoxInteractiveMode->isChecked(), ui.progressBar); } void AutoTester::on_createRecursiveScriptButton_clicked() { @@ -30,6 +34,10 @@ void AutoTester::on_createTestButton_clicked() { test.createTest(); } +void AutoTester::on_deleteOldSnapshotsButton_clicked() { + test.deleteOldSnapshots(); +} + 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 index acfea32ba1..35f609a89d 100644 --- a/tools/auto-tester/src/ui/AutoTester.h +++ b/tools/auto-tester/src/ui/AutoTester.h @@ -1,6 +1,5 @@ // // AutoTester.h -// zone/ambientLightInheritence // // Created by Nissim Hadar on 2 Nov 2017. // Copyright 2013 High Fidelity, Inc. @@ -22,10 +21,11 @@ public: AutoTester(QWidget *parent = Q_NULLPTR); private slots: -void on_evaluateTestsButton_clicked(); -void on_evaluateTestsRecursivelyButton_clicked(); -void on_createRecursiveScriptButton_clicked(); + void on_evaluateTestsButton_clicked(); + void on_evaluateTestsRecursivelyButton_clicked(); + void on_createRecursiveScriptButton_clicked(); void on_createTestButton_clicked(); + void on_deleteOldSnapshotsButton_clicked(); void on_closeButton_clicked(); private: diff --git a/tools/auto-tester/src/ui/AutoTester.ui b/tools/auto-tester/src/ui/AutoTester.ui index 7032ef9710..d06255acf6 100644 --- a/tools/auto-tester/src/ui/AutoTester.ui +++ b/tools/auto-tester/src/ui/AutoTester.ui @@ -6,8 +6,8 @@ 0 0 - 286 - 470 + 607 + 395 @@ -17,9 +17,9 @@ - 60 - 360 - 160 + 190 + 300 + 220 40 @@ -30,9 +30,9 @@ - 60 - 270 - 160 + 360 + 130 + 220 40 @@ -43,9 +43,9 @@ - 60 - 20 - 160 + 20 + 75 + 220 40 @@ -56,9 +56,9 @@ - 60 - 210 - 160 + 360 + 75 + 220 40 @@ -69,9 +69,9 @@ - 60 - 75 - 160 + 20 + 130 + 220 40 @@ -79,13 +79,55 @@ Evaluate Tests Recursively + + + + 23 + 40 + 131 + 20 + + + + <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> + + + Interactive Mode + + + + + + 20 + 190 + 255 + 23 + + + + 24 + + + + + + 360 + 240 + 220 + 40 + + + + Delete Old Snapshots + + 0 0 - 286 + 607 21 @@ -103,4 +145,4 @@ - + \ No newline at end of file diff --git a/tools/auto-tester/src/ui/MismatchWindow.cpp b/tools/auto-tester/src/ui/MismatchWindow.cpp index 07664a1667..d880a1abdc 100644 --- a/tools/auto-tester/src/ui/MismatchWindow.cpp +++ b/tools/auto-tester/src/ui/MismatchWindow.cpp @@ -11,11 +11,48 @@ #include +#include + MismatchWindow::MismatchWindow(QWidget *parent) : QDialog(parent) { setupUi(this); expectedImage->setScaledContents(true); resultImage->setScaledContents(true); + diffImage->setScaledContents(true); +} + +QPixmap MismatchWindow::computeDiffPixmap(QImage expectedImage, QImage resultImage) { + // This is an optimization, as QImage.setPixel() is embarrassingly slow + unsigned char* buffer = new unsigned char[expectedImage.height() * expectedImage.width() * 3]; + + // loop over each pixel + for (int y = 0; y < expectedImage.height(); ++y) { + for (int x = 0; x < expectedImage.width(); ++x) { + QRgb pixelP = expectedImage.pixel(QPoint(x, y)); + QRgb pixelQ = resultImage.pixel(QPoint(x, y)); + + // Convert to luminance + double p = R_Y * qRed(pixelP) + G_Y * qGreen(pixelP) + B_Y * qBlue(pixelP); + double q = R_Y * qRed(pixelQ) + G_Y * qGreen(pixelQ) + B_Y * qBlue(pixelQ); + + // The intensity value is modified to increase the brightness of the displayed image + double absoluteDifference = fabs(p - q) / 255.0; + double modifiedDifference = sqrt(absoluteDifference); + + int difference = (int)(modifiedDifference * 255.0); + + buffer[3 * (x + y * expectedImage.width()) + 0] = difference; + buffer[3 * (x + y * expectedImage.width()) + 1] = difference; + buffer[3 * (x + y * expectedImage.width()) + 2] = difference; + } + } + + QImage diffImage(buffer, expectedImage.width(), expectedImage.height(), QImage::Format_RGB888); + QPixmap resultPixmap = QPixmap::fromImage(diffImage); + + delete[] buffer; + + return resultPixmap; } void MismatchWindow::setTestFailure(TestFailure testFailure) { @@ -24,10 +61,19 @@ void MismatchWindow::setTestFailure(TestFailure testFailure) { 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)); + + QPixmap expectedPixmap = QPixmap(testFailure._pathname + testFailure._expectedImageFilename); + QPixmap actualPixmap = QPixmap(testFailure._pathname + testFailure._actualImageFilename); + + diffPixmap = computeDiffPixmap( + QImage(testFailure._pathname + testFailure._expectedImageFilename), + QImage(testFailure._pathname + testFailure._actualImageFilename) + ); + + expectedImage->setPixmap(expectedPixmap); + resultImage->setPixmap(actualPixmap); + diffImage->setPixmap(diffPixmap); } void MismatchWindow::on_passTestButton_clicked() { @@ -44,3 +90,7 @@ void MismatchWindow::on_abortTestsButton_clicked() { _userResponse = USER_RESPONSE_ABORT; close(); } + +QPixmap MismatchWindow::getComparisonImage() { + return diffPixmap; +} diff --git a/tools/auto-tester/src/ui/MismatchWindow.h b/tools/auto-tester/src/ui/MismatchWindow.h index 7c72b7b0b7..cdbdcb4098 100644 --- a/tools/auto-tester/src/ui/MismatchWindow.h +++ b/tools/auto-tester/src/ui/MismatchWindow.h @@ -25,6 +25,9 @@ public: UserResponse getUserResponse() { return _userResponse; } + QPixmap computeDiffPixmap(QImage expectedImage, QImage resultImage); + QPixmap getComparisonImage(); + private slots: void on_passTestButton_clicked(); void on_failTestButton_clicked(); @@ -32,7 +35,9 @@ private slots: private: UserResponse _userResponse{ USER_RESPONSE_INVALID }; + + QPixmap diffPixmap; }; -#endif // hifi_MismatchWindow_h +#endif // hifi_MismatchWindow_h \ No newline at end of file diff --git a/tools/auto-tester/src/ui/MismatchWindow.ui b/tools/auto-tester/src/ui/MismatchWindow.ui index cab6c61e1c..72f86261ab 100644 --- a/tools/auto-tester/src/ui/MismatchWindow.ui +++ b/tools/auto-tester/src/ui/MismatchWindow.ui @@ -6,8 +6,8 @@ 0 0 - 1585 - 694 + 1782 + 942 @@ -16,10 +16,10 @@ - 20 - 170 - 720 - 362 + 10 + 25 + 800 + 450 @@ -29,28 +29,41 @@ - 760 - 170 - 720 - 362 + 900 + 25 + 800 + 450 result image + + + + 540 + 480 + 800 + 450 + + + + diff image + + - 760 - 90 - 800 + 60 + 660 + 480 28 - 16 + 12 @@ -60,15 +73,15 @@ - 40 - 90 - 700 + 60 + 630 + 480 28 - 16 + 12 @@ -78,15 +91,15 @@ - 40 - 30 + 20 + 600 1200 28 - 16 + 12 @@ -97,7 +110,7 @@ 30 - 600 + 790 75 23 @@ -109,8 +122,8 @@ - 330 - 600 + 120 + 790 75 23 @@ -122,36 +135,62 @@ - 630 - 600 - 75 + 210 + 790 + 121 23 - Abort Tests + Abort current test - 810 - 600 - 720 + 30 + 850 + 500 28 - 16 + 12 similarity + + + + 30 + 5 + 151 + 16 + + + + Expected Image + + + + + + 930 + 5 + 151 + 16 + + + + Actual Image + + - + \ No newline at end of file From 9b0fb19cfa54e93e568a79216a534938dde0d394 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 12 Jan 2018 12:16:58 +1300 Subject: [PATCH 04/58] Remove unused "anchor" Overlays property --- interface/src/ui/overlays/Circle3DOverlay.cpp | 2 -- interface/src/ui/overlays/Cube3DOverlay.cpp | 2 -- interface/src/ui/overlays/Grid3DOverlay.cpp | 2 -- interface/src/ui/overlays/Image3DOverlay.cpp | 2 -- interface/src/ui/overlays/Line3DOverlay.cpp | 2 -- interface/src/ui/overlays/ModelOverlay.cpp | 2 -- interface/src/ui/overlays/Overlay.cpp | 18 ++---------------- interface/src/ui/overlays/Overlay.h | 8 -------- interface/src/ui/overlays/OverlaysPayload.cpp | 18 +----------------- .../src/ui/overlays/Rectangle3DOverlay.cpp | 2 -- interface/src/ui/overlays/Shape3DOverlay.cpp | 2 -- interface/src/ui/overlays/Sphere3DOverlay.cpp | 2 -- interface/src/ui/overlays/Text3DOverlay.cpp | 2 -- interface/src/ui/overlays/Web3DOverlay.cpp | 2 -- 14 files changed, 3 insertions(+), 63 deletions(-) diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index a7c586df1f..5e38f28a06 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -386,8 +386,6 @@ void Circle3DOverlay::setProperties(const QVariantMap& properties) { * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise * used.) * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. - * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and - * rotating as you move your avatar. * * @property {string} name="" - A friendly name for the overlay. * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp index c3a772e565..f13f782482 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.cpp +++ b/interface/src/ui/overlays/Cube3DOverlay.cpp @@ -154,8 +154,6 @@ void Cube3DOverlay::setProperties(const QVariantMap& properties) { * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise * used.) * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. - * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and - * rotating as you move your avatar. * * @property {string} name="" - A friendly name for the overlay. * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and diff --git a/interface/src/ui/overlays/Grid3DOverlay.cpp b/interface/src/ui/overlays/Grid3DOverlay.cpp index 679a4afbd2..621c19944b 100644 --- a/interface/src/ui/overlays/Grid3DOverlay.cpp +++ b/interface/src/ui/overlays/Grid3DOverlay.cpp @@ -132,8 +132,6 @@ void Grid3DOverlay::setProperties(const QVariantMap& properties) { * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise * used.) * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. - * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and - * rotating as you move your avatar. * * @property {string} name="" - A friendly name for the overlay. * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index ca7af3a08b..df93245922 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -208,8 +208,6 @@ void Image3DOverlay::setProperties(const QVariantMap& properties) { * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise * used.) * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. - * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and - * rotating as you move your avatar. * * @property {string} name="" - A friendly name for the overlay. * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index a8425d482f..7200abf74e 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -275,8 +275,6 @@ void Line3DOverlay::setProperties(const QVariantMap& originalProperties) { * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise * used.) * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. - * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and - * rotating as you move your avatar. * * @property {string} name="" - A friendly name for the overlay. * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index ec586771af..d29ee93e62 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -306,8 +306,6 @@ vectorType ModelOverlay::mapJoints(mapFunction function) const { * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise * used.) * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. - * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and - * rotating as you move your avatar. * * @property {string} name="" - A friendly name for the overlay. * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index 5cf9fc7c8b..3c952b8338 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -31,8 +31,7 @@ Overlay::Overlay() : _alphaPulse(0.0f), _colorPulse(0.0f), _color(DEFAULT_OVERLAY_COLOR), - _visible(true), - _anchor(NO_ANCHOR) + _visible(true) { } @@ -49,8 +48,7 @@ Overlay::Overlay(const Overlay* overlay) : _alphaPulse(overlay->_alphaPulse), _colorPulse(overlay->_colorPulse), _color(overlay->_color), - _visible(overlay->_visible), - _anchor(overlay->_anchor) + _visible(overlay->_visible) { } @@ -92,13 +90,6 @@ void Overlay::setProperties(const QVariantMap& properties) { bool visible = properties["visible"].toBool(); setVisible(visible); } - - if (properties["anchor"].isValid()) { - QString property = properties["anchor"].toString(); - if (property == "MyAvatar") { - setAnchor(MY_AVATAR); - } - } } // JSDoc for copying to @typedefs of overlay types that inherit Overlay. @@ -119,8 +110,6 @@ void Overlay::setProperties(const QVariantMap& properties) { * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise * used.) * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. - * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and - * rotating as you move your avatar. */ QVariant Overlay::getProperty(const QString& property) { if (property == "type") { @@ -150,9 +139,6 @@ QVariant Overlay::getProperty(const QString& property) { if (property == "visible") { return _visible; } - if (property == "anchor") { - return _anchor == MY_AVATAR ? "MyAvatar" : ""; - } return QVariant(); } diff --git a/interface/src/ui/overlays/Overlay.h b/interface/src/ui/overlays/Overlay.h index 9b07f24600..e9271f3c3f 100644 --- a/interface/src/ui/overlays/Overlay.h +++ b/interface/src/ui/overlays/Overlay.h @@ -26,11 +26,6 @@ class Overlay : public QObject { Q_OBJECT public: - enum Anchor { - NO_ANCHOR, - MY_AVATAR - }; - typedef std::shared_ptr Pointer; typedef render::Payload Payload; typedef std::shared_ptr PayloadPointer; @@ -63,7 +58,6 @@ public: virtual bool isTransparent() { return getAlphaPulse() != 0.0f || getAlpha() != 1.0f; }; xColor getColor(); float getAlpha(); - Anchor getAnchor() const { return _anchor; } float getPulseMax() const { return _pulseMax; } float getPulseMin() const { return _pulseMin; } @@ -78,7 +72,6 @@ public: void setDrawHUDLayer(bool drawHUDLayer); void setColor(const xColor& color) { _color = color; } void setAlpha(float alpha) { _alpha = alpha; } - void setAnchor(Anchor anchor) { _anchor = anchor; } void setPulseMax(float value) { _pulseMax = value; } void setPulseMin(float value) { _pulseMin = value; } @@ -118,7 +111,6 @@ protected: xColor _color; bool _visible; // should the overlay be drawn at all - Anchor _anchor; unsigned int _stackOrder { 0 }; diff --git a/interface/src/ui/overlays/OverlaysPayload.cpp b/interface/src/ui/overlays/OverlaysPayload.cpp index fceb261503..449ac62998 100644 --- a/interface/src/ui/overlays/OverlaysPayload.cpp +++ b/interface/src/ui/overlays/OverlaysPayload.cpp @@ -65,23 +65,7 @@ namespace render { } template <> void payloadRender(const Overlay::Pointer& overlay, RenderArgs* args) { if (args) { - if (overlay->getAnchor() == Overlay::MY_AVATAR) { - auto batch = args->_batch; - auto avatar = DependencyManager::get()->getMyAvatar(); - glm::quat myAvatarRotation = avatar->getWorldOrientation(); - glm::vec3 myAvatarPosition = avatar->getWorldPosition(); - float angle = glm::degrees(glm::angle(myAvatarRotation)); - glm::vec3 axis = glm::axis(myAvatarRotation); - float myAvatarScale = avatar->getModelScale(); - Transform transform = Transform(); - transform.setTranslation(myAvatarPosition); - transform.setRotation(glm::angleAxis(angle, axis)); - transform.setScale(myAvatarScale); - batch->setModelTransform(transform); - overlay->render(args); - } else { - overlay->render(args); - } + overlay->render(args); } } template <> const ShapeKey shapeGetShapeKey(const Overlay::Pointer& overlay) { diff --git a/interface/src/ui/overlays/Rectangle3DOverlay.cpp b/interface/src/ui/overlays/Rectangle3DOverlay.cpp index c535b7e503..e765f3fc18 100644 --- a/interface/src/ui/overlays/Rectangle3DOverlay.cpp +++ b/interface/src/ui/overlays/Rectangle3DOverlay.cpp @@ -127,8 +127,6 @@ const render::ShapeKey Rectangle3DOverlay::getShapeKey() { * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise * used.) * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. - * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and - * rotating as you move your avatar. * * @property {string} name="" - A friendly name for the overlay. * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and diff --git a/interface/src/ui/overlays/Shape3DOverlay.cpp b/interface/src/ui/overlays/Shape3DOverlay.cpp index 1ae0dc6fd3..97342a80ab 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.cpp +++ b/interface/src/ui/overlays/Shape3DOverlay.cpp @@ -128,8 +128,6 @@ void Shape3DOverlay::setProperties(const QVariantMap& properties) { * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise * used.) * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. - * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and - * rotating as you move your avatar. * * @property {string} name="" - A friendly name for the overlay. * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index 137d273c7d..3021aa4404 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -47,8 +47,6 @@ Sphere3DOverlay::Sphere3DOverlay(const Sphere3DOverlay* Sphere3DOverlay) : * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise * used.) * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. - * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and - * rotating as you move your avatar. * * @property {string} name="" - A friendly name for the overlay. * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and diff --git a/interface/src/ui/overlays/Text3DOverlay.cpp b/interface/src/ui/overlays/Text3DOverlay.cpp index f036516f1e..bed20be698 100644 --- a/interface/src/ui/overlays/Text3DOverlay.cpp +++ b/interface/src/ui/overlays/Text3DOverlay.cpp @@ -224,8 +224,6 @@ void Text3DOverlay::setProperties(const QVariantMap& properties) { * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise * used.) * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. - * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and - * rotating as you move your avatar. * * @property {string} name="" - A friendly name for the overlay. * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 96b68f5ae0..7c9bd120ab 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -482,8 +482,6 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) { * the pulse multiplier is applied out of phase with the pulse period. (The magnitude of the property isn't otherwise * used.) * @property {boolean} visible=true - If true, the overlay is rendered, otherwise it is not rendered. - * @property {string} anchor="" - If set to "MyAvatar" then the overlay is attached to your avatar, moving and - * rotating as you move your avatar. * * @property {string} name="" - A friendly name for the overlay. * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and From 5dca387055efb3350dee2b0549698e19eb457a19 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 12 Jan 2018 12:47:22 +1300 Subject: [PATCH 05/58] Remove old, unused overlay panel code --- interface/src/ui/overlays/OverlayPanel.cpp | 190 ------------------ interface/src/ui/overlays/OverlayPanel.h | 86 -------- interface/src/ui/overlays/Overlays.cpp | 120 ----------- interface/src/ui/overlays/Overlays.h | 38 ---- interface/src/ui/overlays/PanelAttachable.cpp | 19 -- interface/src/ui/overlays/PanelAttachable.h | 11 - 6 files changed, 464 deletions(-) delete mode 100644 interface/src/ui/overlays/OverlayPanel.cpp delete mode 100644 interface/src/ui/overlays/OverlayPanel.h diff --git a/interface/src/ui/overlays/OverlayPanel.cpp b/interface/src/ui/overlays/OverlayPanel.cpp deleted file mode 100644 index 06480109ce..0000000000 --- a/interface/src/ui/overlays/OverlayPanel.cpp +++ /dev/null @@ -1,190 +0,0 @@ -// -// OverlayPanel.cpp -// interface/src/ui/overlays -// -// Created by Zander Otavka on 7/2/15. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "OverlayPanel.h" - -#if OVERLAY_PANELS - -#include -#include -#include -#include - -#include "avatar/AvatarManager.h" -#include "avatar/MyAvatar.h" -#include "Base3DOverlay.h" - -PropertyBinding::PropertyBinding(QString avatar, QUuid entity) : - avatar(avatar), - entity(entity) -{ -} - -QVariant propertyBindingToVariant(const PropertyBinding& value) { - QVariantMap obj; - - if (value.avatar == "MyAvatar") { - obj["avatar"] = "MyAvatar"; - } else if (!value.entity.isNull()) { - obj["entity"] = value.entity; - } - - return obj; -} - -void propertyBindingFromVariant(const QVariant& objectVar, PropertyBinding& value) { - auto object = objectVar.toMap(); - auto avatar = object["avatar"]; - auto entity = object["entity"]; - - if (avatar.isValid() && !avatar.isNull()) { - value.avatar = avatar.toString(); - } else if (entity.isValid() && !entity.isNull()) { - value.entity = entity.toUuid(); - } -} - - -void OverlayPanel::addChild(OverlayID childId) { - if (!_children.contains(childId)) { - _children.append(childId); - } -} - -void OverlayPanel::removeChild(OverlayID childId) { - if (_children.contains(childId)) { - _children.removeOne(childId); - } -} - -QVariant OverlayPanel::getProperty(const QString &property) { - if (property == "anchorPosition") { - return vec3toVariant(getAnchorPosition()); - } - if (property == "anchorPositionBinding") { - return propertyBindingToVariant(PropertyBinding(_anchorPositionBindMyAvatar ? - "MyAvatar" : "", - _anchorPositionBindEntity)); - } - if (property == "anchorRotation") { - return quatToVariant(getAnchorRotation()); - } - if (property == "anchorRotationBinding") { - return propertyBindingToVariant(PropertyBinding(_anchorRotationBindMyAvatar ? - "MyAvatar" : "", - _anchorRotationBindEntity)); - } - if (property == "anchorScale") { - return vec3toVariant(getAnchorScale()); - } - if (property == "visible") { - return getVisible(); - } - if (property == "children") { - QVariantList array; - for (int i = 0; i < _children.length(); i++) { - array.append(OverlayIDtoScriptValue(nullptr, _children[i]).toVariant()); - } - return array; - } - - auto value = Billboardable::getProperty(property); - if (value.isValid()) { - return value; - } - return PanelAttachable::getProperty(property); -} - -void OverlayPanel::setProperties(const QVariantMap& properties) { - PanelAttachable::setProperties(properties); - Billboardable::setProperties(properties); - - auto anchorPosition = properties["anchorPosition"]; - if (anchorPosition.isValid()) { - setAnchorPosition(vec3FromVariant(anchorPosition)); - } - - auto anchorPositionBinding = properties["anchorPositionBinding"]; - if (anchorPositionBinding.isValid()) { - PropertyBinding binding = {}; - propertyBindingFromVariant(anchorPositionBinding, binding); - _anchorPositionBindMyAvatar = binding.avatar == "MyAvatar"; - _anchorPositionBindEntity = binding.entity; - } - - auto anchorRotation = properties["anchorRotation"]; - if (anchorRotation.isValid()) { - setAnchorRotation(quatFromVariant(anchorRotation)); - } - - auto anchorRotationBinding = properties["anchorRotationBinding"]; - if (anchorRotationBinding.isValid()) { - PropertyBinding binding = {}; - propertyBindingFromVariant(anchorPositionBinding, binding); - _anchorRotationBindMyAvatar = binding.avatar == "MyAvatar"; - _anchorRotationBindEntity = binding.entity; - } - - auto anchorScale = properties["anchorScale"]; - if (anchorScale.isValid()) { - setAnchorScale(vec3FromVariant(anchorScale)); - } - - auto visible = properties["visible"]; - if (visible.isValid()) { - setVisible(visible.toBool()); - } -} - -void OverlayPanel::applyTransformTo(Transform& transform, bool force) { - if (force || usecTimestampNow() > _transformExpiry) { - PanelAttachable::applyTransformTo(transform, true); - if (!getParentPanel()) { - if (_anchorPositionBindMyAvatar) { - transform.setTranslation(DependencyManager::get()->getMyAvatar() - ->getPosition()); - } else if (!_anchorPositionBindEntity.isNull()) { - EntityTreePointer entityTree = DependencyManager::get()->getEntityTree(); - entityTree->withReadLock([&] { - EntityItemPointer foundEntity = entityTree->findEntityByID(_anchorPositionBindEntity); - if (foundEntity) { - transform.setTranslation(foundEntity->getPosition()); - } - }); - } else { - transform.setTranslation(getAnchorPosition()); - } - - if (_anchorRotationBindMyAvatar) { - transform.setRotation(DependencyManager::get()->getMyAvatar() - ->getOrientation()); - } else if (!_anchorRotationBindEntity.isNull()) { - EntityTreePointer entityTree = DependencyManager::get()->getEntityTree(); - entityTree->withReadLock([&] { - EntityItemPointer foundEntity = entityTree->findEntityByID(_anchorRotationBindEntity); - if (foundEntity) { - transform.setRotation(foundEntity->getRotation()); - } - }); - } else { - transform.setRotation(getAnchorRotation()); - } - - transform.setScale(getAnchorScale()); - - transform.postTranslate(getOffsetPosition()); - transform.postRotate(getOffsetRotation()); - transform.postScale(getOffsetScale()); - } - pointTransformAtCamera(transform, getOffsetRotation()); - } -} -#endif \ No newline at end of file diff --git a/interface/src/ui/overlays/OverlayPanel.h b/interface/src/ui/overlays/OverlayPanel.h deleted file mode 100644 index cff2bc224d..0000000000 --- a/interface/src/ui/overlays/OverlayPanel.h +++ /dev/null @@ -1,86 +0,0 @@ -// -// OverlayPanel.h -// interface/src/ui/overlays -// -// Created by Zander Otavka on 7/2/15. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_OverlayPanel_h -#define hifi_OverlayPanel_h - -#include - -#include -#include -#include - -#include "PanelAttachable.h" -#include "Billboardable.h" -#include "Overlay.h" - -#if OVERLAY_PANELS -class PropertyBinding { -public: - PropertyBinding() {} - PropertyBinding(QString avatar, QUuid entity); - QString avatar; - QUuid entity; -}; - -QVariant propertyBindingToVariant(const PropertyBinding& value); -void propertyBindingFromVariant(const QVariant& object, PropertyBinding& value); - - -class OverlayPanel : public QObject, public PanelAttachable, public Billboardable { - Q_OBJECT - -public: - typedef std::shared_ptr Pointer; - - void init(QScriptEngine* scriptEngine) { _scriptEngine = scriptEngine; } - - // getters - glm::vec3 getAnchorPosition() const { return _anchorTransform.getTranslation(); } - glm::quat getAnchorRotation() const { return _anchorTransform.getRotation(); } - glm::vec3 getAnchorScale() const { return _anchorTransform.getScale(); } - bool getVisible() const { return _visible; } - - // setters - void setAnchorPosition(const glm::vec3& position) { _anchorTransform.setTranslation(position); } - void setAnchorRotation(const glm::quat& rotation) { _anchorTransform.setRotation(rotation); } - void setAnchorScale(float scale) { _anchorTransform.setScale(scale); } - void setAnchorScale(const glm::vec3& scale) { _anchorTransform.setScale(scale); } - void setVisible(bool visible) { _visible = visible; } - - const QList& getChildren() { return _children; } - void addChild(OverlayID childId); - void removeChild(OverlayID childId); - OverlayID popLastChild() { return _children.takeLast(); } - - void setProperties(const QVariantMap& properties); - QVariant getProperty(const QString& property); - - virtual void applyTransformTo(Transform& transform, bool force = false) override; - -private: - Transform _anchorTransform; - - bool _anchorPositionBindMyAvatar = false; - QUuid _anchorPositionBindEntity; - - bool _anchorRotationBindMyAvatar = false; - QUuid _anchorRotationBindEntity; - - bool _visible = true; - QList _children; - - QScriptEngine* _scriptEngine; -}; - -#endif - -#endif // hifi_OverlayPanel_h diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 052ef0b6d8..7f897aee4a 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -68,16 +68,10 @@ void Overlays::cleanupAllOverlays() { foreach(Overlay::Pointer overlay, overlaysWorld) { _overlaysToDelete.push_back(overlay); } -#if OVERLAY_PANELS - _panels.clear(); -#endif cleanupOverlaysToDelete(); } void Overlays::init() { -#if OVERLAY_PANELS - _scriptEngine = new QScriptEngine(); -#endif } void Overlays::update(float deltatime) { @@ -300,12 +294,6 @@ OverlayID Overlays::cloneOverlay(OverlayID id) { if (thisOverlay) { OverlayID cloneId = addOverlay(Overlay::Pointer(thisOverlay->createClone(), [](Overlay* ptr) { ptr->deleteLater(); })); -#if OVERLAY_PANELS - auto attachable = std::dynamic_pointer_cast(thisOverlay); - if (attachable && attachable->getParentPanel()) { - attachable->getParentPanel()->addChild(cloneId); - } -#endif return cloneId; } @@ -381,15 +369,6 @@ void Overlays::deleteOverlay(OverlayID id) { } } -#if OVERLAY_PANELS - auto attachable = std::dynamic_pointer_cast(overlayToDelete); - if (attachable && attachable->getParentPanel()) { - attachable->getParentPanel()->removeChild(id); - attachable->setParentPanel(nullptr); - } -#endif - - _overlaysToDelete.push_back(overlayToDelete); emit overlayDeleted(id); } @@ -424,49 +403,6 @@ QObject* Overlays::getOverlayObject(OverlayID id) { return nullptr; } -#if OVERLAY_PANELS -OverlayID Overlays::getParentPanel(OverlayID childId) const { - Overlay::Pointer overlay = getOverlay(childId); - auto attachable = std::dynamic_pointer_cast(overlay); - if (attachable) { - return _panels.key(attachable->getParentPanel()); - } else if (_panels.contains(childId)) { - return _panels.key(getPanel(childId)->getParentPanel()); - } - return UNKNOWN_OVERLAY_ID; -} - -void Overlays::setParentPanel(OverlayID childId, OverlayID panelId) { - auto attachable = std::dynamic_pointer_cast(getOverlay(childId)); - if (attachable) { - if (_panels.contains(panelId)) { - auto panel = getPanel(panelId); - panel->addChild(childId); - attachable->setParentPanel(panel); - } else { - auto panel = attachable->getParentPanel(); - if (panel) { - panel->removeChild(childId); - attachable->setParentPanel(nullptr); - } - } - } else if (_panels.contains(childId)) { - OverlayPanel::Pointer child = getPanel(childId); - if (_panels.contains(panelId)) { - auto panel = getPanel(panelId); - panel->addChild(childId); - child->setParentPanel(panel); - } else { - auto panel = child->getParentPanel(); - if (panel) { - panel->removeChild(childId); - child->setParentPanel(0); - } - } - } -} -#endif - OverlayID Overlays::getOverlayAtPoint(const glm::vec2& point) { if (!_enabled) { return UNKNOWN_OVERLAY_ID; @@ -717,62 +653,6 @@ QSizeF Overlays::textSize(OverlayID id, const QString& text) { return QSizeF(0.0f, 0.0f); } -#if OVERLAY_PANELS -OverlayID Overlays::addPanel(OverlayPanel::Pointer panel) { - QWriteLocker lock(&_lock); - - OverlayID thisID = QUuid::createUuid(); - _panels[thisID] = panel; - - return thisID; -} - -OverlayID Overlays::addPanel(const QVariant& properties) { - OverlayPanel::Pointer panel = std::make_shared(); - panel->init(_scriptEngine); - panel->setProperties(properties.toMap()); - return addPanel(panel); -} - -void Overlays::editPanel(OverlayID panelId, const QVariant& properties) { - if (_panels.contains(panelId)) { - _panels[panelId]->setProperties(properties.toMap()); - } -} - -OverlayPropertyResult Overlays::getPanelProperty(OverlayID panelId, const QString& property) { - OverlayPropertyResult result; - if (_panels.contains(panelId)) { - OverlayPanel::Pointer thisPanel = getPanel(panelId); - QReadLocker lock(&_lock); - result.value = thisPanel->getProperty(property); - } - return result; -} - - -void Overlays::deletePanel(OverlayID panelId) { - OverlayPanel::Pointer panelToDelete; - - { - QWriteLocker lock(&_lock); - if (_panels.contains(panelId)) { - panelToDelete = _panels.take(panelId); - } else { - return; - } - } - - while (!panelToDelete->getChildren().isEmpty()) { - OverlayID childId = panelToDelete->popLastChild(); - deleteOverlay(childId); - deletePanel(childId); - } - - emit panelDeleted(panelId); -} -#endif - bool Overlays::isAddedOverlay(OverlayID id) { if (QThread::currentThread() != thread()) { bool result; diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 04c0d01fa2..fd57869048 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -27,7 +27,6 @@ #include "Overlay.h" #include "PanelAttachable.h" -#include "OverlayPanel.h" class PickRay; @@ -93,9 +92,6 @@ public: void enable(); Overlay::Pointer getOverlay(OverlayID id) const; -#if OVERLAY_PANELS - OverlayPanel::Pointer getPanel(OverlayID id) const { return _panels[id]; } -#endif /// adds an overlay that's already been created OverlayID addOverlay(Overlay* overlay) { return addOverlay(Overlay::Pointer(overlay)); } @@ -468,30 +464,6 @@ public slots: */ bool isAddedOverlay(OverlayID id); -#if OVERLAY_PANELS - OverlayID getParentPanel(OverlayID childId) const; - void setParentPanel(OverlayID childId, OverlayID panelId); - - /// adds a panel that has already been created - OverlayID addPanel(OverlayPanel::Pointer panel); - - /// creates and adds a panel based on a set of properties - OverlayID addPanel(const QVariant& properties); - - /// edit the properties of a panel - void editPanel(OverlayID panelId, const QVariant& properties); - - /// get a property of a panel - OverlayPropertyResult getPanelProperty(OverlayID panelId, const QString& property); - - /// deletes a panel and all child overlays - void deletePanel(OverlayID panelId); - - /// return true if there is a panel with that id else false - bool isAddedPanel(OverlayID id) { return _panels.contains(id); } - -#endif - /**jsdoc * Generate a mouse press event on an overlay. * @function Overlays.sendMousePressOnOverlay @@ -612,10 +584,6 @@ signals: */ void overlayDeleted(OverlayID id); -#if OVERLAY_PANELS - void panelDeleted(OverlayID id); -#endif - /**jsdoc * Triggered when a mouse press event occurs on an overlay. Only occurs for 3D overlays (unless you use * {@link Overlays.sendMousePressOnOverlay|sendMousePressOnOverlay} for a 2D overlay). @@ -732,15 +700,9 @@ private: QMap _overlaysHUD; QMap _overlaysWorld; -#if OVERLAY_PANELS - QMap _panels; -#endif QList _overlaysToDelete; unsigned int _stackOrder { 1 }; -#if OVERLAY_PANELS - QScriptEngine* _scriptEngine; -#endif bool _enabled = true; PointerEvent calculateOverlayPointerEvent(OverlayID overlayID, PickRay ray, RayToOverlayIntersectionResult rayPickResult, diff --git a/interface/src/ui/overlays/PanelAttachable.cpp b/interface/src/ui/overlays/PanelAttachable.cpp index bcd32b2850..b53474390c 100644 --- a/interface/src/ui/overlays/PanelAttachable.cpp +++ b/interface/src/ui/overlays/PanelAttachable.cpp @@ -13,18 +13,8 @@ #include -#include "OverlayPanel.h" - bool PanelAttachable::getParentVisible() const { -#if OVERLAY_PANELS - if (getParentPanel()) { - return getParentPanel()->getVisible() && getParentPanel()->getParentVisible(); - } else { - return true; - } -#else return true; -#endif } // JSDoc for copying to @typedefs of overlay types that inherit PanelAttachable. @@ -67,15 +57,6 @@ bool PanelAttachable::applyTransformTo(Transform& transform, bool force) { if (force || usecTimestampNow() > _transformExpiry) { const quint64 TRANSFORM_UPDATE_PERIOD = 100000; // frequency is 10 Hz _transformExpiry = usecTimestampNow() + TRANSFORM_UPDATE_PERIOD; -#if OVERLAY_PANELS - if (getParentPanel()) { - getParentPanel()->applyTransformTo(transform, true); - transform.postTranslate(getOffsetPosition()); - transform.postRotate(getOffsetRotation()); - transform.postScale(getOffsetScale()); - return true; - } -#endif } return false; } diff --git a/interface/src/ui/overlays/PanelAttachable.h b/interface/src/ui/overlays/PanelAttachable.h index 1598aa4700..95faf38cf2 100644 --- a/interface/src/ui/overlays/PanelAttachable.h +++ b/interface/src/ui/overlays/PanelAttachable.h @@ -30,8 +30,6 @@ #ifndef hifi_PanelAttachable_h #define hifi_PanelAttachable_h -#define OVERLAY_PANELS 0 - #include #include @@ -44,18 +42,12 @@ class OverlayPanel; class PanelAttachable { public: // getters -#if OVERLAY_PANELS - std::shared_ptr getParentPanel() const { return _parentPanel; } -#endif glm::vec3 getOffsetPosition() const { return _offset.getTranslation(); } glm::quat getOffsetRotation() const { return _offset.getRotation(); } glm::vec3 getOffsetScale() const { return _offset.getScale(); } bool getParentVisible() const; // setters -#if OVERLAY_PANELS - void setParentPanel(std::shared_ptr panel) { _parentPanel = panel; } -#endif void setOffsetPosition(const glm::vec3& position) { _offset.setTranslation(position); } void setOffsetRotation(const glm::quat& rotation) { _offset.setRotation(rotation); } void setOffsetScale(float scale) { _offset.setScale(scale); } @@ -71,9 +63,6 @@ protected: quint64 _transformExpiry = 0; private: -#if OVERLAY_PANELS - std::shared_ptr _parentPanel = nullptr; -#endif Transform _offset; }; From c3eb4b5ecd12ebbec9fcf24058087df14055e30b Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 16 Jan 2018 10:25:03 -0800 Subject: [PATCH 06/58] destroy pointermanager before pickmanager --- interface/src/Application.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 449b014c13..6f9a9f1128 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2093,6 +2093,11 @@ void Application::cleanupBeforeQuit() { DependencyManager::destroy(); DependencyManager::destroy(); + // The PointerManager must be destroyed before the PickManager because when a Pointer is deleted, + // it accesses the PickManager to delete its associated Pick + DependencyManager::destroy(); + DependencyManager::destroy(); + qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; } From 85bda1da8e3663fdfc663359283a00cebe6c2622 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 17 Jan 2018 09:57:20 +1300 Subject: [PATCH 07/58] Add Window browseChanged signal as replacement for openFileChanged --- .../src/scripting/WindowScriptingInterface.cpp | 3 ++- .../src/scripting/WindowScriptingInterface.h | 16 +++++++++++++--- scripts/system/edit.js | 6 +++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index e36b84ac96..74c5b274bd 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -277,7 +277,8 @@ void WindowScriptingInterface::browseAsync(const QString& title, const QString& if (!result.isEmpty()) { setPreviousBrowseLocation(QFileInfo(result).absolutePath()); } - emit openFileChanged(result); + emit browseChanged(result); + emit openFileChanged(result); // Deprecated signal; to be removed in due course. }); } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index bfad5644bf..86334d1d6d 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -197,18 +197,19 @@ public slots: /**jsdoc * Prompt the user to choose a file. Displays a non-modal dialog that navigates the directory tree. A - * {@link Window.openFileChanged|openFileChanged} signal is emitted when a file is chosen; no signal is emitted if the user + * {@link Window.browseChanged|browseChanged} signal is emitted when a file is chosen; no signal is emitted if the user * cancels the dialog. + * @deprecated A deprecated {@link Window.openFileChanged|openFileChanged} signal is also emitted when a file is chosen. * @function Window.browseAsync * @param {string} title="" - The title to display at the top of the dialog. * @param {string} directory="" - The initial directory to start browsing at. * @param {string} nameFilter="" - The types of files to display. Examples: "*.json" and * "Images (*.png *.jpg *.svg)". All files are displayed if a filter isn't specified. * @example Ask the user to choose an image file without waiting for the answer. - * function onOpenFileChanged(filename) { + * function onBrowseChanged(filename) { * print("File: " + filename); * } - * Window.openFileChanged.connect(onOpenFileChanged); + * Window.browseChanged.connect(onBrowseChanged); * * Window.browseAsync("Select Image File", Paths.resources, "Images (*.png *.jpg *.svg)"); * print("Script continues without waiting"); @@ -652,9 +653,18 @@ signals: */ void saveFileChanged(QString filename); + /**jsdoc + * Triggered when the user chooses a file in a {@link Window.browseAsync|browseAsync} dialog. + * @function Window.browseChanged + * @param {string} filename - The path and name of the file the user chose in the dialog. + * @returns {Signal} + */ + void browseChanged(QString filename); + /**jsdoc * Triggered when the user chooses a file in a {@link Window.browseAsync|browseAsync} dialog. * @function Window.openFileChanged + * @deprecated This signal is being replaced with {@link Window.browseChanged|browseChanged} and will be removed. * @param {string} filename - The path and name of the file the user chose in the dialog. * @returns {Signal} */ diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 87cd3e0faf..92ccdf6565 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -448,7 +448,7 @@ var toolBar = (function () { }); addButton("importEntitiesButton", "assets-01.svg", function() { - Window.openFileChanged.connect(onFileOpenChanged); + Window.browseChanged.connect(onFileOpenChanged); Window.browseAsync("Select Model to Import", "", "*.json"); }); @@ -1497,7 +1497,7 @@ function onFileOpenChanged(filename) { // disconnect the event, otherwise the requests will stack up try { // Not all calls to onFileOpenChanged() connect an event. - Window.openFileChanged.disconnect(onFileOpenChanged); + Window.browseChanged.disconnect(onFileOpenChanged); } catch (e) { // Ignore. } @@ -1549,7 +1549,7 @@ function handeMenuEvent(menuItem) { } } else if (menuItem === "Import Entities" || menuItem === "Import Entities from URL") { if (menuItem === "Import Entities") { - Window.openFileChanged.connect(onFileOpenChanged); + Window.browseChanged.connect(onFileOpenChanged); Window.browseAsync("Select Model to Import", "", "*.json"); } else { Window.promptTextChanged.connect(onPromptTextChanged); From 2883e242854b42dadc55b24f44f7fc24b122947e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 17 Jan 2018 12:00:35 +1300 Subject: [PATCH 08/58] Add Window.protocolVersion() as a replacement for location's --- interface/src/Application.cpp | 3 +-- interface/src/scripting/WindowScriptingInterface.cpp | 4 ++++ interface/src/scripting/WindowScriptingInterface.h | 7 +++++++ libraries/networking/src/AddressManager.h | 1 + scripts/system/tablet-goto.js | 2 +- .../marketplace/camera-move/app-camera-move.js | 2 +- .../modules/custom-settings-app/CustomSettingsApp.js | 2 +- 7 files changed, 16 insertions(+), 5 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index cc4e951235..4e3a4d097d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -573,8 +573,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { } }; reportAndQuit("--protocolVersion", [&](FILE* fp) { - DependencyManager::set(); - auto version = DependencyManager::get()->protocolVersion(); + auto version = protocolVersionsSignatureBase64(); fputs(version.toLatin1().data(), fp); }); reportAndQuit("--version", [&](FILE* fp) { diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index e36b84ac96..0ef1090fef 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -390,6 +390,10 @@ QString WindowScriptingInterface::checkVersion() { return QCoreApplication::applicationVersion(); } +QString WindowScriptingInterface::protocolVersion() { + return protocolVersionsSignatureBase64(); +} + int WindowScriptingInterface::getInnerWidth() { return qApp->getDeviceSize().x; } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index bfad5644bf..a02025bb39 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -305,6 +305,13 @@ public slots: */ QString checkVersion(); + /**jsdoc + * Get Interface's protocol version. + * @function Window.protocolVersion + * @returns {string} A string uniquely identifying the version of the metaverse protocol that Interface is using. + */ + QString protocolVersion(); + /**jsdoc * Copies text to the operating system's clipboard. * @function Window.copyToClipboard diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index 7302e0e997..ef55b57de1 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -73,6 +73,7 @@ public: * Get Interface's protocol version. * @function location.protocolVersion * @returns {string} A string uniquely identifying the version of the metaverse protocol that Interface is using. + * @deprecated This function is deprecated and will removed. Use {@link Window.protocolVersion} instead. */ Q_INVOKABLE QString protocolVersion(); diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 2a0e827932..a4d549ee47 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -136,7 +136,7 @@ 'include_actions=' + actions, 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), 'require_online=true', - 'protocol=' + encodeURIComponent(location.protocolVersion()), + 'protocol=' + encodeURIComponent(Window.protocolVersion()), 'per_page=' + count ]; var url = Account.metaverseServerURL + '/api/v1/user_stories?' + options.join('&'); diff --git a/unpublishedScripts/marketplace/camera-move/app-camera-move.js b/unpublishedScripts/marketplace/camera-move/app-camera-move.js index f58dd3d3bd..4596d8803e 100644 --- a/unpublishedScripts/marketplace/camera-move/app-camera-move.js +++ b/unpublishedScripts/marketplace/camera-move/app-camera-move.js @@ -133,7 +133,7 @@ var DEBUG_INFO = { Reticle: { supportsScale: 'scale' in Reticle, }, - protocolVersion: location.protocolVersion, + protocolVersion: Window.protocolVersion(), }; var globalState = { diff --git a/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js index c76adea940..4c7cf71218 100644 --- a/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js +++ b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js @@ -52,7 +52,7 @@ function CustomSettingsApp(options) { this.extraParams = Object.assign(options.extraParams || {}, { customSettingsVersion: CustomSettingsApp.version+'', - protocolVersion: location.protocolVersion && location.protocolVersion() + protocolVersion: Window.protocolVersion && Window.protocolVersion() }); var params = { From c90cdf52b4ff66df236b2a9d863207b93aa41af3 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 15 Dec 2017 17:17:27 -0800 Subject: [PATCH 09/58] remove unecessary parenting of Agent script engine --- assignment-client/src/Agent.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index d1c2c6597b..a42b78a6fa 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -343,7 +343,6 @@ void Agent::scriptRequestFinished() { void Agent::executeScript() { _scriptEngine = scriptEngineFactory(ScriptEngine::AGENT_SCRIPT, _scriptContents, _payload); - _scriptEngine->setParent(this); // be the parent of the script engine so it gets moved when we do DependencyManager::get()->setScriptEngine(_scriptEngine); From 88ba5e5a0014d52e205f992d54912fb486cd71ea Mon Sep 17 00:00:00 2001 From: Liv Erickson Date: Wed, 17 Jan 2018 13:45:56 -0800 Subject: [PATCH 10/58] change ImageOverlay to use AnimatedImage instead of Image to support .gif files in image overlay objects --- interface/resources/qml/hifi/overlays/ImageOverlay.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/overlays/ImageOverlay.qml b/interface/resources/qml/hifi/overlays/ImageOverlay.qml index 6899c38e67..cbcf6c7910 100644 --- a/interface/resources/qml/hifi/overlays/ImageOverlay.qml +++ b/interface/resources/qml/hifi/overlays/ImageOverlay.qml @@ -7,7 +7,7 @@ import "." Overlay { id: root - Image { + AnimatedImage { id: image property bool scaleFix: true property real xStart: 0 From 1dde25e38fae02d0c6da1b77ee9f6d8b5f876dc5 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 17 Jan 2018 16:51:38 -0800 Subject: [PATCH 11/58] Don't make links in txn history for highfidelity and marketplace --- interface/src/commerce/Ledger.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 50ea6629cf..7686ecfc46 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -137,10 +137,14 @@ QString hfcString(const QJsonValue& sentValue, const QJsonValue& receivedValue) return result; } static const QString USER_PAGE_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/users/"; +static const QStringList KNOWN_USERS(QStringList() << "highfidelity" << "marketplace"); QString userLink(const QString& username) { - if (username.isEmpty()) { + if (username.isEmpty() ) { return QString("someone"); } + if (KNOWN_USERS.contains(username)) { + return username; + } return QString("%2").arg(USER_PAGE_BASE_URL, username); } From ee2936d13345fe22a357bbd826c0cdc378cc0dbe Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 18 Jan 2018 14:07:07 +1300 Subject: [PATCH 12/58] Add Pointers API function that disables hover events for nominated items --- interface/src/raypick/PointerScriptingInterface.cpp | 4 ++++ interface/src/raypick/PointerScriptingInterface.h | 8 ++++++++ libraries/pointers/src/Pointer.cpp | 11 +++++++++++ libraries/pointers/src/Pointer.h | 3 +++ libraries/pointers/src/PointerManager.cpp | 7 +++++++ libraries/pointers/src/PointerManager.h | 1 + 6 files changed, 34 insertions(+) diff --git a/interface/src/raypick/PointerScriptingInterface.cpp b/interface/src/raypick/PointerScriptingInterface.cpp index ac5a467e76..8e50b1d629 100644 --- a/interface/src/raypick/PointerScriptingInterface.cpp +++ b/interface/src/raypick/PointerScriptingInterface.cpp @@ -24,6 +24,10 @@ void PointerScriptingInterface::setIncludeItems(unsigned int uid, const QScriptV DependencyManager::get()->setIncludeItems(uid, qVectorQUuidFromScriptValue(includeItems)); } +void PointerScriptingInterface::setNonHoverItems(unsigned int uid, const QScriptValue& nonHoverItems) const { + DependencyManager::get()->setNonHoverItems(uid, qVectorQUuidFromScriptValue(nonHoverItems)); +} + unsigned int PointerScriptingInterface::createPointer(const PickQuery::PickType& type, const QVariant& properties) { // Interaction with managers should always happen on the main thread if (QThread::currentThread() != qApp->thread()) { diff --git a/interface/src/raypick/PointerScriptingInterface.h b/interface/src/raypick/PointerScriptingInterface.h index 1cc7b56503..4791fd802e 100644 --- a/interface/src/raypick/PointerScriptingInterface.h +++ b/interface/src/raypick/PointerScriptingInterface.h @@ -191,6 +191,14 @@ public: */ Q_INVOKABLE void setIncludeItems(unsigned int uid, const QScriptValue& includeEntities) const; + /**jsdoc + * Sets a list of Entity IDs, Overlay IDs, and/or Avatar IDs that the pointer should not send hover events to. + * @function Pointers.setNonHoverItems + * @param {number} uid - The ID of the Pointer, as returned by {@link Pointers.createPointer}. + * @param {Uuid[]} nonHoverItems - A list of IDs to that hover events should not be sent to. + */ + Q_INVOKABLE void setNonHoverItems(unsigned int uid, const QScriptValue& nonHoverItems) const; + /**jsdoc * Lock a Pointer onto a specific object (overlay, entity, or avatar). Optionally, provide an offset in object-space, otherwise the Pointer will lock on to the center of the object. * Not used by Stylus Pointers. diff --git a/libraries/pointers/src/Pointer.cpp b/libraries/pointers/src/Pointer.cpp index 5307e17355..ead3c22687 100644 --- a/libraries/pointers/src/Pointer.cpp +++ b/libraries/pointers/src/Pointer.cpp @@ -52,6 +52,12 @@ void Pointer::setIncludeItems(const QVector& includeItems) const { DependencyManager::get()->setIncludeItems(_pickUID, includeItems); } +void Pointer::setNonHoverItems(const QVector& nonHoverItems) { + withWriteLock([&] { + _nonHoverItems = nonHoverItems; + }); +} + bool Pointer::isLeftHand() const { return DependencyManager::get()->isLeftHand(_pickUID); } @@ -96,6 +102,11 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin // Hover events bool doHover = shouldHover(pickResult); + + auto pickResultMap = pickResult->toVariantMap(); + auto uuid = QUuid(pickResultMap.value("objectID", "").toString()); + doHover = doHover && !_nonHoverItems.contains(uuid); + Pointer::PickedObject hoveredObject = getHoveredObject(pickResult); PointerEvent hoveredEvent = buildPointerEvent(hoveredObject, pickResult); hoveredEvent.setType(PointerEvent::Move); diff --git a/libraries/pointers/src/Pointer.h b/libraries/pointers/src/Pointer.h index 3197c80cad..6cb366e92a 100644 --- a/libraries/pointers/src/Pointer.h +++ b/libraries/pointers/src/Pointer.h @@ -54,6 +54,8 @@ public: virtual void setIgnoreItems(const QVector& ignoreItems) const; virtual void setIncludeItems(const QVector& includeItems) const; + void setNonHoverItems(const QVector& nonHoverItems); + bool isLeftHand() const; bool isRightHand() const; bool isMouse() const; @@ -102,6 +104,7 @@ private: PointerEvent::Button chooseButton(const std::string& button); + QVector _nonHoverItems; }; #endif // hifi_Pick_h diff --git a/libraries/pointers/src/PointerManager.cpp b/libraries/pointers/src/PointerManager.cpp index be890da392..f2953ce8c8 100644 --- a/libraries/pointers/src/PointerManager.cpp +++ b/libraries/pointers/src/PointerManager.cpp @@ -108,6 +108,13 @@ void PointerManager::setIncludeItems(unsigned int uid, const QVector& inc } } +void PointerManager::setNonHoverItems(unsigned int uid, const QVector& nonHoverItems) const { + auto pointer = find(uid); + if (pointer) { + pointer->setNonHoverItems(nonHoverItems); + } +} + void PointerManager::setLength(unsigned int uid, float length) const { auto pointer = find(uid); if (pointer) { diff --git a/libraries/pointers/src/PointerManager.h b/libraries/pointers/src/PointerManager.h index b98558622f..a2a1e9dcd2 100644 --- a/libraries/pointers/src/PointerManager.h +++ b/libraries/pointers/src/PointerManager.h @@ -34,6 +34,7 @@ public: void setPrecisionPicking(unsigned int uid, bool precisionPicking) const; void setIgnoreItems(unsigned int uid, const QVector& ignoreEntities) const; void setIncludeItems(unsigned int uid, const QVector& includeEntities) const; + void setNonHoverItems(unsigned int uid, const QVector& nonHoverItems) const; void setLength(unsigned int uid, float length) const; void setLockEndUUID(unsigned int uid, const QUuid& objectID, bool isOverlay, const glm::mat4& offsetMat = glm::mat4()) const; From c661bec8fd0ca34f60066e2a09520f24456c5eca Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 17 Jan 2018 17:15:14 -0800 Subject: [PATCH 13/58] whitespace --- interface/src/commerce/Ledger.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 7686ecfc46..88bdb1f405 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -139,7 +139,7 @@ QString hfcString(const QJsonValue& sentValue, const QJsonValue& receivedValue) static const QString USER_PAGE_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/users/"; static const QStringList KNOWN_USERS(QStringList() << "highfidelity" << "marketplace"); QString userLink(const QString& username) { - if (username.isEmpty() ) { + if (username.isEmpty()) { return QString("someone"); } if (KNOWN_USERS.contains(username)) { From 5878e36ea8fc38c2c1e904e9398af9efa467c3da Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 18 Jan 2018 14:24:23 +1300 Subject: [PATCH 14/58] Display both lasers --- .../controllerModules/webSurfaceLaserInput.js | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js index 3d9d7979d5..19b5330492 100644 --- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js +++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -87,20 +87,11 @@ Script.include("/~/system/libraries/controllers.js"); return MyAvatar.getDominantHand() === "right" ? 1 : 0; }; - this.dominantHandOverride = false; - this.isReady = function(controllerData) { - var otherModuleRunning = this.getOtherModule().running; - otherModuleRunning = otherModuleRunning && this.getDominantHand() !== this.hand; // Auto-swap to dominant hand. var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE && controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE; - if ((!otherModuleRunning || isTriggerPressed) - && (this.isPointingAtOverlay(controllerData) || this.isPointingAtWebEntity(controllerData))) { + if (this.isPointingAtOverlay(controllerData) || this.isPointingAtWebEntity(controllerData)) { this.updateAllwaysOn(); - if (isTriggerPressed) { - this.dominantHandOverride = true; // Override dominant hand. - this.getOtherModule().dominantHandOverride = false; - } if (this.parameters.handLaser.allwaysOn || isTriggerPressed) { return makeRunningValues(true, [], []); } @@ -109,11 +100,8 @@ Script.include("/~/system/libraries/controllers.js"); }; this.run = function(controllerData, deltaTime) { - var otherModuleRunning = this.getOtherModule().running; - otherModuleRunning = otherModuleRunning && this.getDominantHand() !== this.hand; // Auto-swap to dominant hand. - otherModuleRunning = otherModuleRunning || this.getOtherModule().dominantHandOverride; // Override dominant hand. var grabModuleNeedsToRun = this.grabModuleWantsNearbyOverlay(controllerData); - if (!otherModuleRunning && !grabModuleNeedsToRun && (controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE + if (!grabModuleNeedsToRun && (controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE || this.parameters.handLaser.allwaysOn && (this.isPointingAtOverlay(controllerData) || this.isPointingAtWebEntity(controllerData)))) { this.running = true; @@ -121,7 +109,6 @@ Script.include("/~/system/libraries/controllers.js"); } this.deleteContextOverlay(); this.running = false; - this.dominantHandOverride = false; return makeRunningValues(false, [], []); }; } From 45406177e517bbadd1e20c455aa2bc9c261ef743 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 17 Jan 2018 17:56:25 -0800 Subject: [PATCH 15/58] avoid a deadlock when clearing avatar entities --- .../avatars-renderer/src/avatars-renderer/Avatar.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 0f9573bb84..95a8a9f1fd 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -328,13 +328,15 @@ void Avatar::updateAvatarEntities() { AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs(); if (!recentlyDettachedAvatarEntities.empty()) { // only lock this thread when absolutely necessary + AvatarEntityMap avatarEntityData; _avatarEntitiesLock.withReadLock([&] { - foreach (auto entityID, recentlyDettachedAvatarEntities) { - if (!_avatarEntityData.contains(entityID)) { - entityTree->deleteEntity(entityID, true, true); - } - } + avatarEntityData = _avatarEntityData; }); + foreach (auto entityID, recentlyDettachedAvatarEntities) { + if (!avatarEntityData.contains(entityID)) { + entityTree->deleteEntity(entityID, true, true); + } + } // remove stale data hashes foreach (auto entityID, recentlyDettachedAvatarEntities) { From 49e72346a4d7fb57229d61fb74dd170fb9b4ab53 Mon Sep 17 00:00:00 2001 From: Elisa Lupin-Jimenez Date: Wed, 17 Jan 2018 18:08:05 -0800 Subject: [PATCH 16/58] fixed typo in macro name --- libraries/entities/src/EntityItemProperties.cpp | 12 ++++++------ libraries/entities/src/EntityItemPropertiesMacros.h | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index a19ed14d4a..e2a5ddf8b5 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -722,7 +722,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(collisionless, bool, setCollisionless); COPY_PROPERTY_FROM_QSCRIPTVALUE_GETTER(ignoreForCollisions, bool, setCollisionless, getCollisionless); // legacy support COPY_PROPERTY_FROM_QSCRIPTVALUE(collisionMask, uint8_t, setCollisionMask); - COPY_PROPERTY_FROM_QSCRITPTVALUE_ENUM(collidesWith, CollisionMask); + COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(collidesWith, CollisionMask); COPY_PROPERTY_FROM_QSCRIPTVALUE_GETTER(collisionsWillMove, bool, setDynamic, getDynamic); // legacy support COPY_PROPERTY_FROM_QSCRIPTVALUE(dynamic, bool, setDynamic); COPY_PROPERTY_FROM_QSCRIPTVALUE(isSpotlight, bool, setIsSpotlight); @@ -737,7 +737,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(lineHeight, float, setLineHeight); COPY_PROPERTY_FROM_QSCRIPTVALUE(textColor, xColor, setTextColor); COPY_PROPERTY_FROM_QSCRIPTVALUE(backgroundColor, xColor, setBackgroundColor); - COPY_PROPERTY_FROM_QSCRITPTVALUE_ENUM(shapeType, ShapeType); + COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(shapeType, ShapeType); COPY_PROPERTY_FROM_QSCRIPTVALUE(maxParticles, quint32, setMaxParticles); COPY_PROPERTY_FROM_QSCRIPTVALUE(lifespan, float, setLifespan); COPY_PROPERTY_FROM_QSCRIPTVALUE(isEmitting, bool, setIsEmitting); @@ -775,10 +775,10 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(name, QString, setName); COPY_PROPERTY_FROM_QSCRIPTVALUE(collisionSoundURL, QString, setCollisionSoundURL); - COPY_PROPERTY_FROM_QSCRITPTVALUE_ENUM(hazeMode, HazeMode); - COPY_PROPERTY_FROM_QSCRITPTVALUE_ENUM(keyLightMode, KeyLightMode); - COPY_PROPERTY_FROM_QSCRITPTVALUE_ENUM(ambientLightMode, AmbientLightMode); - COPY_PROPERTY_FROM_QSCRITPTVALUE_ENUM(skyboxMode, SkyboxMode); + COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(hazeMode, HazeMode); + COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(keyLightMode, KeyLightMode); + COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(ambientLightMode, AmbientLightMode); + COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(skyboxMode, SkyboxMode); COPY_PROPERTY_FROM_QSCRIPTVALUE(sourceUrl, QString, setSourceUrl); COPY_PROPERTY_FROM_QSCRIPTVALUE(voxelVolumeSize, glmVec3, setVoxelVolumeSize); diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h index 278d945ab0..f291973e52 100644 --- a/libraries/entities/src/EntityItemPropertiesMacros.h +++ b/libraries/entities/src/EntityItemPropertiesMacros.h @@ -367,7 +367,7 @@ inline xColor xColor_convertFromScriptValue(const QScriptValue& v, bool& isValid } \ } -#define COPY_PROPERTY_FROM_QSCRITPTVALUE_ENUM(P, S) \ +#define COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(P, S) \ QScriptValue P = object.property(#P); \ if (P.isValid()) { \ QString newValue = P.toVariant().toString(); \ From 4eb2a5cf5e900a80abda46677038b0e88de53445 Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 17 Jan 2018 19:25:20 -0800 Subject: [PATCH 17/58] added a property check in renderableModelEntity for the animation url this fixes the bug stopping you from changing animations on an entity --- .../src/RenderableModelEntityItem.cpp | 14 +++++++++++++- .../src/RenderableModelEntityItem.h | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 827507c3aa..684540453b 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1394,7 +1394,19 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce // That is where _currentFrame and _lastAnimated were updated. if (_animating) { DETAILED_PROFILE_RANGE(simulation_physics, "Animate"); - if (!jointsMapped()) { + // check animation url change + auto newprops = entity->getAnimationProperties(); + if (newprops != _previousAnimationProperties) { + if (newprops.getURL() != _previousAnimationProperties.getURL()) { + _animation = DependencyManager::get()->getAnimation(entity->getAnimationURL()); + _jointMappingCompleted = false; + mapJoints(entity, model->getJointNames()); + } + _previousAnimationProperties = newprops; + } + // + if (!jointsMapped() || _animation->getURL().toString() != entity->getAnimationURL()) { + qCDebug(entitiesrenderer) << "changed animation or started animation"; mapJoints(entity, model->getJointNames()); } if (!(entity->getAnimationFirstFrame() < 0) && !(entity->getAnimationFirstFrame() > entity->getAnimationLastFrame())) { diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index b3988e0239..ce4d44daf8 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -193,6 +193,9 @@ private: bool _animating { false }; uint64_t _lastAnimated { 0 }; + //fix test + AnimationPropertyGroup _previousAnimationProperties; + render::ItemKey _itemKey { render::ItemKey::Builder().withTypeMeta() }; }; From 403dd3d7d029a73c63e5e1c4f101d40efeff06ce Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 18 Jan 2018 22:31:12 +1300 Subject: [PATCH 18/58] Only first laser highlights --- .../controllers/controllerDispatcher.js | 64 +++++++++++++++++++ .../controllerModules/webSurfaceLaserInput.js | 45 ++++++++++++- 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 16f1d086b7..15b025a7ea 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -44,7 +44,12 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.highVarianceCount = 0; this.veryhighVarianceCount = 0; this.tabletID = null; + this.TABLET_UI_UUIDS = []; this.blacklist = []; + this.leftPointerNonHoverItem = null; + this.leftPointerNonHoverItemChanged = false; + this.rightPointerNonHoverItem = null; + this.rightPointerNonHoverItemChanged = false; this.pointerManager = new PointerManager(); // a module can occupy one or more "activity" slots while it's running. If all the required slots for a module are @@ -122,6 +127,10 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); return getControllerWorldLocation(Controller.Standard.RightHand, true); }; + this.isTabletID = function (uuid) { + return _this.TABLET_UI_UUIDS.indexOf(uuid) !== -1; + }; + this.updateTimings = function () { _this.intervalCount++; var thisInterval = Date.now(); @@ -148,11 +157,35 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.setIgnorePointerItems = function() { if (HMD.tabletID !== this.tabletID) { this.tabletID = HMD.tabletID; + this.TABLET_UI_UUIDS = [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID, HMD.homeButtonHighlightID]; Pointers.setIgnoreItems(_this.leftPointer, _this.blacklist); Pointers.setIgnoreItems(_this.rightPointer, _this.blacklist); } }; + this.setNonHoverItems = function () { + if (_this.leftPointerNonHoverItemChanged) { + if (_this.leftPointerNonHoverItem === null) { + Pointers.setNonHoverItems(_this.leftPointer, []); + } else if (_this.isTabletID(_this.leftPointerNonHoverItem)) { + Pointers.setNonHoverItems(_this.leftPointer, _this.TABLET_UI_UUIDS); + } else { + Pointers.setNonHoverItems(_this.leftPointer, [_this.leftPointerNonHoverItem]); + } + _this.leftPointerNonHoverItemChanged = false; + } + if (_this.rightPointerNonHoverItemChanged) { + if (_this.rightPointerNonHoverItem === null) { + Pointers.setNonHoverItems(_this.rightPointer, []); + } else if (_this.isTabletID(_this.rightPointerNonHoverItem)) { + Pointers.setNonHoverItems(_this.rightPointer, _this.TABLET_UI_UUIDS); + } else { + Pointers.setNonHoverItems(_this.rightPointer, [_this.rightPointerNonHoverItem]); + } + _this.rightPointerNonHoverItemChanged = false; + } + }; + this.update = function () { try { _this.updateInternal(); @@ -324,6 +357,19 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); _this.runningPluginNames[orderedPluginName] = true; _this.markSlots(candidatePlugin, orderedPluginName); _this.pointerManager.makePointerVisible(candidatePlugin.parameters.handLaser); + + if (candidatePlugin.parameters.handLaser.nonHoverItem !== undefined) { + if (candidatePlugin.parameters.handLaser.hand === LEFT_HAND + && _this.leftPointerNonHoverItem !== candidatePlugin.parameters.handLaser.nonHoverItem) { + _this.leftPointerNonHoverItem = candidatePlugin.parameters.handLaser.nonHoverItem; + _this.leftPointerNonHoverItemChanged = true; + } else if (candidatePlugin.parameters.handLaser.hand === RIGHT_HAND + && _this.rightPointerNonHoverItem !== candidatePlugin.parameters.handLaser.nonHoverItem) { + _this.rightPointerNonHoverItem = candidatePlugin.parameters.handLaser.nonHoverItem; + _this.rightPointerNonHoverItemChanged = true; + } + } + if (DEBUG) { print("controllerDispatcher running " + orderedPluginName); } @@ -354,6 +400,21 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); Script.beginProfileRange("dispatch.run." + runningPluginName); } var runningness = plugin.run(controllerData, deltaTime); + + if (runningness.active) { + if (plugin.parameters.handLaser.nonHoverItem !== undefined) { + if (plugin.parameters.handLaser.hand === LEFT_HAND + && _this.leftPointerNonHoverItem !== plugin.parameters.handLaser.nonHoverItem) { + _this.leftPointerNonHoverItem = plugin.parameters.handLaser.nonHoverItem; + _this.leftPointerNonHoverItemChanged = true; + } else if (plugin.parameters.handLaser.hand === RIGHT_HAND + && _this.rightPointerNonHoverItem !== plugin.parameters.handLaser.nonHoverItem) { + _this.rightPointerNonHoverItem = plugin.parameters.handLaser.nonHoverItem; + _this.rightPointerNonHoverItemChanged = true; + } + } + } + if (!runningness.active) { // plugin is finished running, for now. remove it from the list // of running plugins and mark its activity-slots as "not in use" @@ -372,6 +433,9 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } } _this.pointerManager.updatePointersRenderState(controllerData.triggerClicks, controllerData.triggerValues); + + _this.setNonHoverItems(); + if (PROFILE) { Script.endProfileRange("dispatch.run"); } diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js index 19b5330492..5b9afa3a2a 100644 --- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js +++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -87,28 +87,69 @@ Script.include("/~/system/libraries/controllers.js"); return MyAvatar.getDominantHand() === "right" ? 1 : 0; }; + this.hoverItem = null; + + this.isTabletID = function (uuid) { + return [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID, HMD.homeButtonHightlightID].indexOf(uuid) !== -1; + }; + this.isReady = function(controllerData) { var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE && controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE; if (this.isPointingAtOverlay(controllerData) || this.isPointingAtWebEntity(controllerData)) { this.updateAllwaysOn(); if (this.parameters.handLaser.allwaysOn || isTriggerPressed) { + var pointingAt = controllerData.rayPicks[this.hand].objectID; + if (this.isTabletID(pointingAt)) { + pointingAt = HMD.tabletID; + } + + if (pointingAt !== this.getOtherModule().hoverItem) { + this.hoverItem = pointingAt; + this.getOtherModule().parameters.handLaser.nonHoverItem = pointingAt; + } else { + this.hoverItem = null; + this.getOtherModule().parameters.handLaser.nonHoverItem = null; + } + return makeRunningValues(true, [], []); } } + + this.hoverItem = null; + this.getOtherModule().parameters.handLaser.nonHoverItem = null; + return makeRunningValues(false, [], []); }; this.run = function(controllerData, deltaTime) { var grabModuleNeedsToRun = this.grabModuleWantsNearbyOverlay(controllerData); - if (!grabModuleNeedsToRun && (controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE - || this.parameters.handLaser.allwaysOn + var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE; + if (!grabModuleNeedsToRun && (isTriggerPressed || this.parameters.handLaser.allwaysOn && (this.isPointingAtOverlay(controllerData) || this.isPointingAtWebEntity(controllerData)))) { this.running = true; + + var pointingAt = controllerData.rayPicks[this.hand].objectID; + if (this.isTabletID(pointingAt)) { + pointingAt = HMD.tabletID; + } + + if (pointingAt !== this.getOtherModule().hoverItem || isTriggerPressed) { + this.hoverItem = pointingAt; + this.getOtherModule().parameters.handLaser.nonHoverItem = pointingAt; + } else { + this.hoverItem = null; + this.getOtherModule().parameters.handLaser.nonHoverItem = null; + } + return makeRunningValues(true, [], []); } this.deleteContextOverlay(); this.running = false; + + this.hoverItem = null; + this.getOtherModule().parameters.handLaser.nonHoverItem = null; + return makeRunningValues(false, [], []); }; } From 82cab5e4a1552c6a987fba8d4ae3c9cfa8aef65a Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 11 Jan 2018 17:53:34 -0800 Subject: [PATCH 19/58] add scripted/qml debug UI for render LOD --- interface/src/Application.cpp | 5 +- interface/src/LODManager.cpp | 44 ++++++++--- interface/src/LODManager.h | 49 +++++++++--- scripts/developer/utilities/render/lod.js | 77 ++++++++++++++++++ scripts/developer/utilities/render/lod.qml | 92 ++++++++++++++++++++++ scripts/system/assets/images/lod-a.svg | 46 +++++++++++ scripts/system/assets/images/lod-i.svg | 46 +++++++++++ 7 files changed, 333 insertions(+), 26 deletions(-) create mode 100644 scripts/developer/utilities/render/lod.js create mode 100644 scripts/developer/utilities/render/lod.qml create mode 100644 scripts/system/assets/images/lod-a.svg create mode 100644 scripts/system/assets/images/lod-i.svg diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6849bd07b5..cccb61c1f2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4351,8 +4351,9 @@ void Application::updateLOD(float deltaTime) const { float presentTime = getActiveDisplayPlugin()->getAveragePresentTime(); float engineRunTime = (float)(_renderEngine->getConfiguration().get()->getCPURunTime()); float gpuTime = getGPUContext()->getFrameTimerGPUAverage(); - float maxRenderTime = glm::max(gpuTime, glm::max(presentTime, engineRunTime)); - DependencyManager::get()->autoAdjustLOD(maxRenderTime, deltaTime); + auto lodManager = DependencyManager::get(); + lodManager->setRenderTimes(presentTime, engineRunTime, gpuTime); + lodManager->autoAdjustLOD(deltaTime); } else { DependencyManager::get()->resetLODAdjust(); } diff --git a/interface/src/LODManager.cpp b/interface/src/LODManager.cpp index 087a93cffe..d2d502dc47 100644 --- a/interface/src/LODManager.cpp +++ b/interface/src/LODManager.cpp @@ -26,21 +26,21 @@ Setting::Handle hmdLODDecreaseFPS("hmdLODDecreaseFPS", DEFAULT_HMD_LOD_DO LODManager::LODManager() { } -float LODManager::getLODDecreaseFPS() { +float LODManager::getLODDecreaseFPS() const { if (qApp->isHMDMode()) { return getHMDLODDecreaseFPS(); } return getDesktopLODDecreaseFPS(); } -float LODManager::getLODIncreaseFPS() { +float LODManager::getLODIncreaseFPS() const { if (qApp->isHMDMode()) { return getHMDLODIncreaseFPS(); } return getDesktopLODIncreaseFPS(); } -// We use a "time-weighted running average" of the renderTime and compare it against min/max thresholds +// We use a "time-weighted running average" of the maxRenderTime and compare it against min/max thresholds // to determine if we should adjust the level of detail (LOD). // // A time-weighted running average has a timescale which determines how fast the average tracks the measured @@ -58,11 +58,18 @@ const uint64_t LOD_AUTO_ADJUST_PERIOD = 5 * (uint64_t)(LOD_ADJUST_RUNNING_AVG_TI const float LOD_AUTO_ADJUST_DECREMENT_FACTOR = 0.8f; const float LOD_AUTO_ADJUST_INCREMENT_FACTOR = 1.2f; -void LODManager::autoAdjustLOD(float renderTime, float realTimeDelta) { - // compute time-weighted running average renderTime +void LODManager::setRenderTimes(float presentTime, float engineRunTime, float gpuTime) { + _presentTime = presentTime; + _engineRunTime = engineRunTime; + _gpuTime = gpuTime; +} + +void LODManager::autoAdjustLOD(float realTimeDelta) { + float maxRenderTime = glm::max(glm::max(_presentTime, _engineRunTime), _gpuTime); + // compute time-weighted running average maxRenderTime // Note: we MUST clamp the blend to 1.0 for stability float blend = (realTimeDelta < LOD_ADJUST_RUNNING_AVG_TIMESCALE) ? realTimeDelta / LOD_ADJUST_RUNNING_AVG_TIMESCALE : 1.0f; - _avgRenderTime = (1.0f - blend) * _avgRenderTime + blend * renderTime; // msec + _avgRenderTime = (1.0f - blend) * _avgRenderTime + blend * maxRenderTime; // msec if (!_automaticLODAdjust) { // early exit return; @@ -84,6 +91,10 @@ void LODManager::autoAdjustLOD(float renderTime, float realTimeDelta) { << "targetFPS =" << getLODDecreaseFPS() << "octreeSizeScale =" << _octreeSizeScale; emit LODDecreased(); + // Assuming the LOD adjustment will work: we optimistically reset _avgRenderTime + // to provide an FPS just above the decrease threshold. It will drift close to its + // true value after a few LOD_ADJUST_TIMESCALEs and we'll adjust again as necessary. + _avgRenderTime = (float)MSECS_PER_SECOND / (getLODDecreaseFPS() + 1.0f); } _decreaseFPSExpiry = now + LOD_AUTO_ADJUST_PERIOD; } @@ -105,6 +116,10 @@ void LODManager::autoAdjustLOD(float renderTime, float realTimeDelta) { << "targetFPS =" << getLODDecreaseFPS() << "octreeSizeScale =" << _octreeSizeScale; emit LODIncreased(); + // Assuming the LOD adjustment will work: we optimistically reset _avgRenderTime + // to provide an FPS just below the increase threshold. It will drift close to its + // true value after a few LOD_ADJUST_TIMESCALEs and we'll adjust again as necessary. + _avgRenderTime = (float)MSECS_PER_SECOND / (getLODIncreaseFPS() - 1.0f); } _increaseFPSExpiry = now + LOD_AUTO_ADJUST_PERIOD; } @@ -119,11 +134,6 @@ void LODManager::autoAdjustLOD(float renderTime, float realTimeDelta) { if (lodToolsDialog) { lodToolsDialog->reloadSliders(); } - // Assuming the LOD adjustment will work: we optimistically reset _avgRenderTime - // to be at middle of target zone. It will drift close to its true value within - // about three few LOD_ADJUST_TIMESCALEs and we'll adjust again as necessary. - float expectedFPS = 0.5f * (getLODIncreaseFPS() + getLODDecreaseFPS()); - _avgRenderTime = MSECS_PER_SECOND / expectedFPS; } } @@ -131,6 +141,18 @@ void LODManager::resetLODAdjust() { _decreaseFPSExpiry = _increaseFPSExpiry = usecTimestampNow() + LOD_AUTO_ADJUST_PERIOD; } +float LODManager::getLODLevel() const { + // simpleLOD is a linearized and normalized number that represents how much LOD is being applied. + // It ranges from: + // 1.0 = normal (max) level of detail + // 0.0 = min level of detail + // In other words: as LOD "drops" the value of simpleLOD will also "drop", and it cannot go lower than 0.0. + const float LOG_MIN_LOD_RATIO = logf(ADJUST_LOD_MIN_SIZE_SCALE / ADJUST_LOD_MAX_SIZE_SCALE); + float power = logf(_octreeSizeScale / ADJUST_LOD_MAX_SIZE_SCALE); + float simpleLOD = (LOG_MIN_LOD_RATIO - power) / LOG_MIN_LOD_RATIO; + return simpleLOD; +} + const float MIN_DECREASE_FPS = 0.5f; void LODManager::setDesktopLODDecreaseFPS(float fps) { diff --git a/interface/src/LODManager.h b/interface/src/LODManager.h index cf38342db0..344ee49850 100644 --- a/interface/src/LODManager.h +++ b/interface/src/LODManager.h @@ -37,7 +37,7 @@ class AABox; class LODManager : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY - + public: Q_INVOKABLE void setAutomaticLODAdjust(bool value) { _automaticLODAdjust = value; } Q_INVOKABLE bool getAutomaticLODAdjust() const { return _automaticLODAdjust; } @@ -49,34 +49,57 @@ public: Q_INVOKABLE void setHMDLODDecreaseFPS(float value); Q_INVOKABLE float getHMDLODDecreaseFPS() const; Q_INVOKABLE float getHMDLODIncreaseFPS() const; - + // User Tweakable LOD Items Q_INVOKABLE QString getLODFeedbackText(); Q_INVOKABLE void setOctreeSizeScale(float sizeScale); Q_INVOKABLE float getOctreeSizeScale() const { return _octreeSizeScale; } - + Q_INVOKABLE void setBoundaryLevelAdjust(int boundaryLevelAdjust); Q_INVOKABLE int getBoundaryLevelAdjust() const { return _boundaryLevelAdjust; } - - Q_INVOKABLE float getLODDecreaseFPS(); - Q_INVOKABLE float getLODIncreaseFPS(); - + + Q_INVOKABLE float getLODDecreaseFPS() const; + Q_INVOKABLE float getLODIncreaseFPS() const; + Q_INVOKABLE float getFoo() const { return getMeasuredFPS(); } + + Q_PROPERTY(float presentTime READ getPresentTime) + Q_PROPERTY(float engineRunTime READ getEngineRunTime) + Q_PROPERTY(float gpuTime READ getGPUTime) + Q_PROPERTY(float avgRenderTime READ getAverageRenderTime) + Q_PROPERTY(float fps READ getMeasuredFPS) + Q_PROPERTY(float lodLevel READ getLODLevel) + + Q_PROPERTY(float lodDecreaseFPS READ getLODDecreaseFPS) + Q_PROPERTY(float lodIncreaseFPS READ getLODIncreaseFPS) + + float getPresentTime() const { return _presentTime; } + float getEngineRunTime() const { return _engineRunTime; } + float getGPUTime() const { return _gpuTime; } + static bool shouldRender(const RenderArgs* args, const AABox& bounds); - void autoAdjustLOD(float renderTime, float realTimeDelta); - + void setRenderTimes(float presentTime, float engineRunTime, float gpuTime); + void autoAdjustLOD(float realTimeDelta); + void loadSettings(); void saveSettings(); void resetLODAdjust(); - + + float getAverageRenderTime() const { return _avgRenderTime; }; + float getMeasuredFPS() const { return (float)MSEC_PER_SECOND / _avgRenderTime; }; + float getLODLevel() const; + signals: void LODIncreased(); void LODDecreased(); - + private: LODManager(); - + bool _automaticLODAdjust = true; - float _avgRenderTime { 0.0f }; + float _presentTime { 0.0f }; // msec + float _engineRunTime { 0.0f }; // msec + float _gpuTime { 0.0f }; // msec + float _avgRenderTime { 0.0f }; // msec float _desktopMaxRenderTime { DEFAULT_DESKTOP_MAX_RENDER_TIME }; float _hmdMaxRenderTime { DEFAULT_HMD_MAX_RENDER_TIME }; diff --git a/scripts/developer/utilities/render/lod.js b/scripts/developer/utilities/render/lod.js new file mode 100644 index 0000000000..dc0b99edc2 --- /dev/null +++ b/scripts/developer/utilities/render/lod.js @@ -0,0 +1,77 @@ +"use strict"; + +// +// lodi.js +// tablet-engine app +// +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + var TABLET_BUTTON_NAME = "LOD"; + var QMLAPP_URL = Script.resolvePath("./lod.qml"); + var ICON_URL = Script.resolvePath("../../../system/assets/images/lod-i.svg"); + var ACTIVE_ICON_URL = Script.resolvePath("../../../system/assets/images/lod-a.svg"); + + var onScreen = false; + + function onClicked() { + if (onScreen) { + tablet.gotoHomeScreen(); + } else { + tablet.loadQMLSource(QMLAPP_URL); + } + } + + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + text: TABLET_BUTTON_NAME, + icon: ICON_URL, + activeIcon: ACTIVE_ICON_URL, + sortOrder: 1 + }); + + var hasEventBridge = false; + + function wireEventBridge(on) { + if (!tablet) { + print("Warning in wireEventBridge(): 'tablet' undefined!"); + return; + } + if (on) { + if (!hasEventBridge) { + tablet.fromQml.connect(fromQml); + hasEventBridge = true; + } + } else { + if (hasEventBridge) { + tablet.fromQml.disconnect(fromQml); + hasEventBridge = false; + } + } + } + + function onScreenChanged(type, url) { + onScreen = (url === QMLAPP_URL); + button.editProperties({isActive: onScreen}); + wireEventBridge(onScreen); + } + + function fromQml(message) { + } + + button.clicked.connect(onClicked); + tablet.screenChanged.connect(onScreenChanged); + + Script.scriptEnding.connect(function () { + if (onScreen) { + tablet.gotoHomeScreen(); + } + button.clicked.disconnect(onClicked); + tablet.screenChanged.disconnect(onScreenChanged); + tablet.removeButton(button); + }); +}()); diff --git a/scripts/developer/utilities/render/lod.qml b/scripts/developer/utilities/render/lod.qml new file mode 100644 index 0000000000..d7b9f1cd57 --- /dev/null +++ b/scripts/developer/utilities/render/lod.qml @@ -0,0 +1,92 @@ +// +// lod.qml +// scripts/developer/utilities/render +// +// Created by Andrew Meadows on 2018.01.10 +// Copyright 2018 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 "../lib/plotperf" + +Item { + id: lodIU + anchors.fill:parent + + Column { + id: stats + spacing: 8 + anchors.fill:parent + + function evalEvenHeight() { + // Why do we have to do that manually ? cannot seem to find a qml / anchor / layout mode that does that ? + return (height - spacing * (children.length - 1)) / children.length + } + + PlotPerf { + title: "Load Indicators" + height: parent.evalEvenHeight() + object: LODManager + valueScale: 1 + valueUnit: "ms" + plots: [ + { + prop: "presentTime", + label: "present", + color: "#FFFF00" + }, + { + prop: "engineRunTime", + label: "engineRun", + color: "#FF00FF" + }, + { + prop: "gpuTime", + label: "gpu", + color: "#00FFFF" + } + ] + } + PlotPerf { + title: "FPS" + height: parent.evalEvenHeight() + object: LODManager + valueScale: 1 + valueUnit: "Hz" + plots: [ + { + prop: "lodIncreaseFPS", + label: "LOD++", + color: "#66FF66" + }, + { + prop: "fps", + label: "FPS", + color: "#FFFFFF" + }, + { + prop: "lodDecreaseFPS", + label: "LOD--", + color: "#FF6666" + } + ] + } + PlotPerf { + title: "LOD" + height: parent.evalEvenHeight() + object: LODManager + valueScale: 0.1 + valueUnit: "" + plots: [ + { + prop: "lodLevel", + label: "LOD", + color: "#9999FF" + } + ] + } + } +} diff --git a/scripts/system/assets/images/lod-a.svg b/scripts/system/assets/images/lod-a.svg new file mode 100644 index 0000000000..6845e0ff78 --- /dev/null +++ b/scripts/system/assets/images/lod-a.svg @@ -0,0 +1,46 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/scripts/system/assets/images/lod-i.svg b/scripts/system/assets/images/lod-i.svg new file mode 100644 index 0000000000..f909f3b495 --- /dev/null +++ b/scripts/system/assets/images/lod-i.svg @@ -0,0 +1,46 @@ + + + +image/svg+xml \ No newline at end of file From 9a1a10b4e115f4580fce86e8680f1048492cac30 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 11 Jan 2018 17:57:25 -0800 Subject: [PATCH 20/58] remove cruft and use more correct names --- interface/src/LODManager.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/interface/src/LODManager.h b/interface/src/LODManager.h index 344ee49850..d9a73387d3 100644 --- a/interface/src/LODManager.h +++ b/interface/src/LODManager.h @@ -60,13 +60,12 @@ public: Q_INVOKABLE float getLODDecreaseFPS() const; Q_INVOKABLE float getLODIncreaseFPS() const; - Q_INVOKABLE float getFoo() const { return getMeasuredFPS(); } Q_PROPERTY(float presentTime READ getPresentTime) Q_PROPERTY(float engineRunTime READ getEngineRunTime) Q_PROPERTY(float gpuTime READ getGPUTime) Q_PROPERTY(float avgRenderTime READ getAverageRenderTime) - Q_PROPERTY(float fps READ getMeasuredFPS) + Q_PROPERTY(float fps READ getMaxTheoreticalFPS) Q_PROPERTY(float lodLevel READ getLODLevel) Q_PROPERTY(float lodDecreaseFPS READ getLODDecreaseFPS) @@ -85,7 +84,7 @@ public: void resetLODAdjust(); float getAverageRenderTime() const { return _avgRenderTime; }; - float getMeasuredFPS() const { return (float)MSEC_PER_SECOND / _avgRenderTime; }; + float getMaxTheoreticalFPS() const { return (float)MSEC_PER_SECOND / _avgRenderTime; }; float getLODLevel() const; signals: From 80c43bd0f77669359f8b8766cc3e129d1398fa04 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 12 Jan 2018 07:40:24 -0800 Subject: [PATCH 21/58] fix typo in variable name --- interface/src/LODManager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/LODManager.h b/interface/src/LODManager.h index d9a73387d3..ca6be9380b 100644 --- a/interface/src/LODManager.h +++ b/interface/src/LODManager.h @@ -84,7 +84,7 @@ public: void resetLODAdjust(); float getAverageRenderTime() const { return _avgRenderTime; }; - float getMaxTheoreticalFPS() const { return (float)MSEC_PER_SECOND / _avgRenderTime; }; + float getMaxTheoreticalFPS() const { return (float)MSECS_PER_SECOND / _avgRenderTime; }; float getLODLevel() const; signals: From 2c43138e1493483c163b61214e475c4b622aa0b2 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 12 Jan 2018 07:41:15 -0800 Subject: [PATCH 22/58] slightly faster auto LOD adjust --- interface/src/LODManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/LODManager.cpp b/interface/src/LODManager.cpp index d2d502dc47..73547cdb5e 100644 --- a/interface/src/LODManager.cpp +++ b/interface/src/LODManager.cpp @@ -47,13 +47,13 @@ float LODManager::getLODIncreaseFPS() const { // value in real-time. Given a step-function in the mesured value, and assuming measurements happen // faster than the runningAverage is computed, the error between the value and its runningAverage will be // reduced by 1/e every timescale of real-time that passes. -const float LOD_ADJUST_RUNNING_AVG_TIMESCALE = 0.1f; // sec +const float LOD_ADJUST_RUNNING_AVG_TIMESCALE = 0.08f; // sec // // Assuming the measured value is affected by logic invoked by the runningAverage bumping up against its // thresholds, we expect the adjustment to introduce a step-function. We want the runningAverage to settle // to the new value BEFORE we test it aginst its thresholds again. Hence we test on a period that is a few // multiples of the running average timescale: -const uint64_t LOD_AUTO_ADJUST_PERIOD = 5 * (uint64_t)(LOD_ADJUST_RUNNING_AVG_TIMESCALE * (float)USECS_PER_MSEC); // usec +const uint64_t LOD_AUTO_ADJUST_PERIOD = 4 * (uint64_t)(LOD_ADJUST_RUNNING_AVG_TIMESCALE * (float)USECS_PER_MSEC); // usec const float LOD_AUTO_ADJUST_DECREMENT_FACTOR = 0.8f; const float LOD_AUTO_ADJUST_INCREMENT_FACTOR = 1.2f; From 9f484a7f57e870bf4d3691dce47d3cf80243571b Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 18 Jan 2018 08:54:05 -0800 Subject: [PATCH 23/58] Removed extraneous comments in RenderableItemEntity.cpp and .h --- libraries/entities-renderer/src/RenderableModelEntityItem.cpp | 1 - libraries/entities-renderer/src/RenderableModelEntityItem.h | 2 -- 2 files changed, 3 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 684540453b..cdbdd31d71 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1404,7 +1404,6 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce } _previousAnimationProperties = newprops; } - // if (!jointsMapped() || _animation->getURL().toString() != entity->getAnimationURL()) { qCDebug(entitiesrenderer) << "changed animation or started animation"; mapJoints(entity, model->getJointNames()); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index ce4d44daf8..014c5bbf67 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -192,8 +192,6 @@ private: bool _shouldHighlight { false }; bool _animating { false }; uint64_t _lastAnimated { 0 }; - - //fix test AnimationPropertyGroup _previousAnimationProperties; render::ItemKey _itemKey { render::ItemKey::Builder().withTypeMeta() }; From 93594fb0bba5d78fc0d23a514245e0ec37f7fca3 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 18 Jan 2018 08:55:51 -0800 Subject: [PATCH 24/58] make dynamic entities collide with avatar when removing grab action --- libraries/entities/src/EntityItem.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 722ad416e4..fe5213baa8 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -2086,6 +2086,10 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPoi EntityDynamicPointer action = _objectActions[actionID]; auto removedActionType = action->getType(); + action->setOwnerEntity(nullptr); + action->setIsMine(false); + _objectActions.remove(actionID); + 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 @@ -2101,9 +2105,6 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPoi // because they should have been set correctly when the action was added // and/or when children were linked } - action->setOwnerEntity(nullptr); - action->setIsMine(false); - _objectActions.remove(actionID); if (simulation) { action->removeFromSimulation(simulation); From 482ca3f03801326cbf06252522f604b17fd46aea Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 10 Jan 2018 17:28:27 -0800 Subject: [PATCH 25/58] Make type name hash static const --- libraries/networking/src/LimitedNodeList.cpp | 7 ---- libraries/networking/src/Node.cpp | 40 +++++++++----------- libraries/networking/src/Node.h | 5 --- libraries/networking/src/NodeType.h | 2 - 4 files changed, 18 insertions(+), 36 deletions(-) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 1b80c3b3af..0950cb5556 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -63,13 +63,6 @@ LimitedNodeList::LimitedNodeList(int socketListenPort, int dtlsListenPort) : _packetStatTimer(), _permissions(NodePermissions()) { - static bool firstCall = true; - if (firstCall) { - NodeType::init(); - - firstCall = false; - } - qRegisterMetaType("ConnectionStep"); auto port = (socketListenPort != INVALID_PORT) ? socketListenPort : LIMITED_NODELIST_LOCAL_PORT.get(); _nodeSocket.bind(QHostAddress::AnyIPv4, port); diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 25eef38dbd..bd895c8ef1 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -29,28 +29,25 @@ int NodePtrMetaTypeId = qRegisterMetaType("Node*"); int sharedPtrNodeMetaTypeId = qRegisterMetaType>("QSharedPointer"); int sharedNodePtrMetaTypeId = qRegisterMetaType("SharedNodePointer"); -void NodeType::init() { - QHash& TypeNameHash = Node::getTypeNameHash(); - - TypeNameHash.insert(NodeType::DomainServer, "Domain Server"); - TypeNameHash.insert(NodeType::EntityServer, "Entity Server"); - TypeNameHash.insert(NodeType::Agent, "Agent"); - TypeNameHash.insert(NodeType::AudioMixer, "Audio Mixer"); - TypeNameHash.insert(NodeType::AvatarMixer, "Avatar Mixer"); - TypeNameHash.insert(NodeType::MessagesMixer, "Messages Mixer"); - TypeNameHash.insert(NodeType::AssetServer, "Asset Server"); - TypeNameHash.insert(NodeType::EntityScriptServer, "Entity Script Server"); - TypeNameHash.insert(NodeType::UpstreamAudioMixer, "Upstream Audio Mixer"); - TypeNameHash.insert(NodeType::UpstreamAvatarMixer, "Upstream Avatar Mixer"); - TypeNameHash.insert(NodeType::DownstreamAudioMixer, "Downstream Audio Mixer"); - TypeNameHash.insert(NodeType::DownstreamAvatarMixer, "Downstream Avatar Mixer"); - TypeNameHash.insert(NodeType::Unassigned, "Unassigned"); -} +static const QHash TYPE_NAME_HASH { + { NodeType::DomainServer, "Domain Server" }, + { NodeType::EntityServer, "Entity Server" }, + { NodeType::Agent, "Agent" }, + { NodeType::AudioMixer, "Audio Mixer" }, + { NodeType::AvatarMixer, "Avatar Mixer" }, + { NodeType::MessagesMixer, "Messages Mixer" }, + { NodeType::AssetServer, "Asset Server" }, + { NodeType::EntityScriptServer, "Entity Script Server" }, + { NodeType::UpstreamAudioMixer, "Upstream Audio Mixer" }, + { NodeType::UpstreamAvatarMixer, "Upstream Avatar Mixer" }, + { NodeType::DownstreamAudioMixer, "Downstream Audio Mixer" }, + { NodeType::DownstreamAvatarMixer, "Downstream Avatar Mixer" }, + { NodeType::Unassigned, "Unassigned" } +}; const QString& NodeType::getNodeTypeName(NodeType_t nodeType) { - QHash& TypeNameHash = Node::getTypeNameHash(); - QHash::iterator matchedTypeName = TypeNameHash.find(nodeType); - return matchedTypeName != TypeNameHash.end() ? matchedTypeName.value() : UNKNOWN_NodeType_t_NAME; + const auto matchedTypeName = TYPE_NAME_HASH.find(nodeType); + return matchedTypeName != TYPE_NAME_HASH.end() ? matchedTypeName.value() : UNKNOWN_NodeType_t_NAME; } bool NodeType::isUpstream(NodeType_t nodeType) { @@ -84,8 +81,7 @@ NodeType_t NodeType::downstreamType(NodeType_t primaryType) { } NodeType_t NodeType::fromString(QString type) { - QHash& TypeNameHash = Node::getTypeNameHash(); - return TypeNameHash.key(type, NodeType::Unassigned); + return TYPE_NAME_HASH.key(type, NodeType::Unassigned); } diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 00d3c61fd0..93b6a649d4 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -89,11 +89,6 @@ public: bool isIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled; } - static QHash& getTypeNameHash() { - static QHash TypeNameHash; - return TypeNameHash; - } - private: // privatize copy and assignment operator to disallow Node copying Node(const Node &otherNode); diff --git a/libraries/networking/src/NodeType.h b/libraries/networking/src/NodeType.h index 0130e92cfc..2b2cc4e011 100644 --- a/libraries/networking/src/NodeType.h +++ b/libraries/networking/src/NodeType.h @@ -31,8 +31,6 @@ namespace NodeType { const NodeType_t DownstreamAvatarMixer = 'w'; const NodeType_t Unassigned = 1; - void init(); - const QString& getNodeTypeName(NodeType_t nodeType); bool isUpstream(NodeType_t nodeType); bool isDownstream(NodeType_t nodeType); From d88ff701b5f9c21e5b3363d9edacea59af513311 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 18 Jan 2018 10:48:35 -0800 Subject: [PATCH 26/58] reduce calls to isPointOnDesktopWindow --- .../controllerModules/farActionGrabEntity.js | 25 +++++++++++-------- .../controllerModules/hudOverlayPointer.js | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index 88195d7024..32bf7316a9 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -370,20 +370,23 @@ Script.include("/~/system/libraries/Xform.js"); }; this.isReady = function (controllerData) { - if (this.notPointingAtEntity(controllerData)) { - return makeRunningValues(false, [], []); - } + if (HMD.active) { + if (this.notPointingAtEntity(controllerData)) { + return makeRunningValues(false, [], []); + } - this.distanceHolding = false; - this.distanceRotating = false; + this.distanceHolding = false; + this.distanceRotating = false; - if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) { - this.prepareDistanceRotatingData(controllerData); - return makeRunningValues(true, [], []); - } else { - this.destroyContextOverlay(); - return makeRunningValues(false, [], []); + if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) { + this.prepareDistanceRotatingData(controllerData); + return makeRunningValues(true, [], []); + } else { + this.destroyContextOverlay(); + return makeRunningValues(false, [], []); + } } + return makeRunningValues(false, [], []); }; this.run = function (controllerData) { diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js index a7a186b07a..afc9256875 100644 --- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js +++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js @@ -89,7 +89,7 @@ this.isReady = function (controllerData) { var otherModuleRunning = this.getOtherModule().running; - if (!otherModuleRunning) { + if (!otherModuleRunning && HMD.active) { if (this.processLaser(controllerData)) { this.running = true; return ControllerDispatcherUtils.makeRunningValues(true, [], []); From 2cdbc1159ef2c62c891b0aedb5b714e881817c06 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 18 Jan 2018 11:15:32 -0800 Subject: [PATCH 27/58] nearby hfc transfers should send the location too --- interface/src/commerce/Ledger.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 50ea6629cf..0533b26e5a 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -17,6 +17,7 @@ #include "Ledger.h" #include "CommerceLogging.h" #include +#include // inventory answers {status: 'success', data: {assets: [{id: "guid", title: "name", preview: "url"}....]}} // balance answers {status: 'success', data: {balance: integer}} @@ -137,9 +138,14 @@ QString hfcString(const QJsonValue& sentValue, const QJsonValue& receivedValue) return result; } static const QString USER_PAGE_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/users/"; -QString userLink(const QString& username) { +static const QString PLACE_PAGE_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/places/"; +QString userLink(const QString& username, const QString& placename) { if (username.isEmpty()) { - return QString("someone"); + if (placename.isEmpty()) { + return QString("someone"); + } else { + return QString("someone in ").arg(PLACE_PAGE_BASE_URL, placename); + } } return QString("%2").arg(USER_PAGE_BASE_URL, username); } @@ -156,10 +162,10 @@ QString transactionString(const QJsonObject& valueObject) { if (sentCerts <= 0 && receivedCerts <= 0) { // this is an hfc transfer. if (sent > 0) { - QString recipient = userLink(valueObject["recipient_name"].toString()); + QString recipient = userLink(valueObject["recipient_name"].toString(), valueObject["place_name"].toString()); result += QString("Money sent to %1").arg(recipient); } else { - QString sender = userLink(valueObject["sender_name"].toString()); + QString sender = userLink(valueObject["sender_name"].toString(), valueObject["place_name"].toString()); result += QString("Money from %1").arg(sender); } if (!message.isEmpty()) { @@ -310,6 +316,7 @@ void Ledger::transferHfcToNode(const QString& hfc_key, const QString& nodeID, co transaction["node_id"] = nodeID; transaction["quantity"] = amount; transaction["message"] = optionalMessage; + transaction["place_name"] = DependencyManager::get()->getPlaceName(); QJsonDocument transactionDoc{ transaction }; auto transactionString = transactionDoc.toJson(QJsonDocument::Compact); signedSend("transaction", transactionString, hfc_key, "transfer_hfc_to_node", "transferHfcToNodeSuccess", "transferHfcToNodeFailure"); From 1bda3faaf277d928b9d211211c89e76893524f45 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 18 Jan 2018 11:14:56 -0800 Subject: [PATCH 28/58] Fix the Space Bubble button flash behavior --- scripts/system/bubble.js | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js index 4ea684ff06..98bc7d319f 100644 --- a/scripts/system/bubble.js +++ b/scripts/system/bubble.js @@ -18,8 +18,6 @@ var bubbleOverlayTimestamp; // Used for flashing the HUD button upon activation var bubbleButtonFlashState = false; - // Used for flashing the HUD button upon activation - var bubbleButtonTimestamp; // Affects bubble height var BUBBLE_HEIGHT_SCALE = 0.15; // The bubble model itself @@ -36,6 +34,7 @@ var bubbleActivateSound = SoundCache.getSound(Script.resolvePath("assets/sounds/bubble.wav")); // Is the update() function connected? var updateConnected = false; + var bubbleFlashTimer = false; var BUBBLE_VISIBLE_DURATION_MS = 3000; var BUBBLE_RAISE_ANIMATION_DURATION_MS = 750; @@ -81,9 +80,16 @@ visible: true }); bubbleOverlayTimestamp = Date.now(); - bubbleButtonTimestamp = bubbleOverlayTimestamp; Script.update.connect(update); updateConnected = true; + + // Flash button + if (!bubbleFlashTimer) { + bubbleFlashTimer = Script.setInterval(function () { + writeButtonProperties(bubbleButtonFlashState); + bubbleButtonFlashState = !bubbleButtonFlashState; + }, 500); + } } // Called from the C++ scripting interface to show the bubble overlay @@ -103,12 +109,6 @@ var delay = (timestamp - bubbleOverlayTimestamp); var overlayAlpha = 1.0 - (delay / BUBBLE_VISIBLE_DURATION_MS); if (overlayAlpha > 0) { - // Flash button - if ((timestamp - bubbleButtonTimestamp) >= BUBBLE_VISIBLE_DURATION_MS) { - writeButtonProperties(bubbleButtonFlashState); - bubbleButtonTimestamp = timestamp; - bubbleButtonFlashState = !bubbleButtonFlashState; - } if (delay < BUBBLE_RAISE_ANIMATION_DURATION_MS) { Overlays.editOverlay(bubbleOverlay, { @@ -157,8 +157,11 @@ Script.update.disconnect(update); updateConnected = false; } - var bubbleActive = Users.getIgnoreRadiusEnabled(); - writeButtonProperties(bubbleActive); + if (bubbleFlashTimer) { + Script.clearTimeout(bubbleFlashTimer); + bubbleFlashTimer = false; + } + writeButtonProperties(Users.getIgnoreRadiusEnabled()); } } @@ -166,6 +169,10 @@ // NOTE: the c++ calls this with just the first param -- we added a second // just for not logging the initial state of the bubble when we startup. function onBubbleToggled(enabled, doNotLog) { + if (bubbleFlashTimer) { + Script.clearTimeout(bubbleFlashTimer); + bubbleFlashTimer = false; + } writeButtonProperties(enabled); if (doNotLog !== true) { UserActivityLogger.bubbleToggled(enabled); @@ -200,6 +207,10 @@ // Cleanup the tablet button and overlays when script is stopped Script.scriptEnding.connect(function () { button.clicked.disconnect(Users.toggleIgnoreRadius); + if (bubbleFlashTimer) { + Script.clearTimeout(bubbleFlashTimer); + bubbleFlashTimer = false; + } if (tablet) { tablet.removeButton(button); } From 13d8d643f7f284c3d38183aab3a820efc85cbcc1 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 18 Jan 2018 12:05:16 -0800 Subject: [PATCH 29/58] Also rate limit the bubble sound --- scripts/system/bubble.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js index 98bc7d319f..994bde49eb 100644 --- a/scripts/system/bubble.js +++ b/scripts/system/bubble.js @@ -16,6 +16,8 @@ var button; // Used for animating and disappearing the bubble var bubbleOverlayTimestamp; + // Used for rate limiting the bubble sound + var lastBubbleSoundTimestamp = 0; // Used for flashing the HUD button upon activation var bubbleButtonFlashState = false; // Affects bubble height @@ -38,6 +40,7 @@ var BUBBLE_VISIBLE_DURATION_MS = 3000; var BUBBLE_RAISE_ANIMATION_DURATION_MS = 750; + var BUBBLE_SOUND_RATE_LIMIT_MS = 15000; // Hides the bubble model overlay and resets the button flash state function hideOverlays() { @@ -49,11 +52,15 @@ // Make the bubble overlay visible, set its position, and play the sound function createOverlays() { - Audio.playSound(bubbleActivateSound, { - position: { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z }, - localOnly: true, - volume: 0.2 - }); + var nowTimestamp = Date.now(); + if (nowTimestamp - lastBubbleSoundTimestamp >= BUBBLE_SOUND_RATE_LIMIT_MS) { + Audio.playSound(bubbleActivateSound, { + position: { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z }, + localOnly: true, + volume: 0.2 + }); + lastBubbleSoundTimestamp = nowTimestamp; + } hideOverlays(); if (updateConnected === true) { updateConnected = false; @@ -79,7 +86,7 @@ }, visible: true }); - bubbleOverlayTimestamp = Date.now(); + bubbleOverlayTimestamp = nowTimestamp; Script.update.connect(update); updateConnected = true; From 8c2427dd58c6e257e043e3872f8f67c505c01061 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 19 Jan 2018 09:06:16 +1300 Subject: [PATCH 30/58] Rename Window.protocolVersion() to Window.protocolSignature() --- interface/src/scripting/WindowScriptingInterface.cpp | 2 +- interface/src/scripting/WindowScriptingInterface.h | 6 +++--- libraries/networking/src/AddressManager.h | 2 +- scripts/system/tablet-goto.js | 2 +- .../marketplace/camera-move/app-camera-move.js | 2 +- .../modules/custom-settings-app/CustomSettingsApp.js | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 0ef1090fef..5ded796dc0 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -390,7 +390,7 @@ QString WindowScriptingInterface::checkVersion() { return QCoreApplication::applicationVersion(); } -QString WindowScriptingInterface::protocolVersion() { +QString WindowScriptingInterface::protocolSignature() { return protocolVersionsSignatureBase64(); } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index a02025bb39..1225342d89 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -306,11 +306,11 @@ public slots: QString checkVersion(); /**jsdoc - * Get Interface's protocol version. - * @function Window.protocolVersion + * Get the signature for Interface's protocol version. + * @function Window.protocolSignature * @returns {string} A string uniquely identifying the version of the metaverse protocol that Interface is using. */ - QString protocolVersion(); + QString protocolSignature(); /**jsdoc * Copies text to the operating system's clipboard. diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index ef55b57de1..0173a1fad7 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -73,7 +73,7 @@ public: * Get Interface's protocol version. * @function location.protocolVersion * @returns {string} A string uniquely identifying the version of the metaverse protocol that Interface is using. - * @deprecated This function is deprecated and will removed. Use {@link Window.protocolVersion} instead. + * @deprecated This function is deprecated and will be removed. Use {@link Window.protocolSignature} instead. */ Q_INVOKABLE QString protocolVersion(); diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index a4d549ee47..43dae9625a 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -136,7 +136,7 @@ 'include_actions=' + actions, 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), 'require_online=true', - 'protocol=' + encodeURIComponent(Window.protocolVersion()), + 'protocol=' + encodeURIComponent(Window.protocolSignature()), 'per_page=' + count ]; var url = Account.metaverseServerURL + '/api/v1/user_stories?' + options.join('&'); diff --git a/unpublishedScripts/marketplace/camera-move/app-camera-move.js b/unpublishedScripts/marketplace/camera-move/app-camera-move.js index 4596d8803e..f9361c6091 100644 --- a/unpublishedScripts/marketplace/camera-move/app-camera-move.js +++ b/unpublishedScripts/marketplace/camera-move/app-camera-move.js @@ -133,7 +133,7 @@ var DEBUG_INFO = { Reticle: { supportsScale: 'scale' in Reticle, }, - protocolVersion: Window.protocolVersion(), + protocolVersion: Window.protocolSignature(), }; var globalState = { diff --git a/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js index 4c7cf71218..95dba5c558 100644 --- a/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js +++ b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js @@ -52,7 +52,7 @@ function CustomSettingsApp(options) { this.extraParams = Object.assign(options.extraParams || {}, { customSettingsVersion: CustomSettingsApp.version+'', - protocolVersion: Window.protocolVersion && Window.protocolVersion() + protocolVersion: Window.protocolSignature && Window.protocolSignature() }); var params = { From 1c97272767e3806c3fa9aa35d9474097c57e9934 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 18 Jan 2018 12:09:28 -0800 Subject: [PATCH 31/58] Cleaned up the code a bit so that there is no need for a new class variable for properties to verify the change of url --- .../src/RenderableModelEntityItem.cpp | 18 +++++++----------- .../src/RenderableModelEntityItem.h | 1 - 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index cdbdd31d71..8b5a23b787 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1394,18 +1394,14 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce // That is where _currentFrame and _lastAnimated were updated. if (_animating) { DETAILED_PROFILE_RANGE(simulation_physics, "Animate"); - // check animation url change - auto newprops = entity->getAnimationProperties(); - if (newprops != _previousAnimationProperties) { - if (newprops.getURL() != _previousAnimationProperties.getURL()) { - _animation = DependencyManager::get()->getAnimation(entity->getAnimationURL()); - _jointMappingCompleted = false; - mapJoints(entity, model->getJointNames()); - } - _previousAnimationProperties = newprops; + + if (!jointsMapped()) { + mapJoints(entity, model->getJointNames()); } - if (!jointsMapped() || _animation->getURL().toString() != entity->getAnimationURL()) { - qCDebug(entitiesrenderer) << "changed animation or started animation"; + //else the joints have been mapped before but we have new animation to load + else if (_animation && (_animation->getURL().toString() != entity->getAnimationURL())) { + _animation = DependencyManager::get()->getAnimation(entity->getAnimationURL()); + _jointMappingCompleted = false; mapJoints(entity, model->getJointNames()); } if (!(entity->getAnimationFirstFrame() < 0) && !(entity->getAnimationFirstFrame() > entity->getAnimationLastFrame())) { diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 014c5bbf67..b3988e0239 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -192,7 +192,6 @@ private: bool _shouldHighlight { false }; bool _animating { false }; uint64_t _lastAnimated { 0 }; - AnimationPropertyGroup _previousAnimationProperties; render::ItemKey _itemKey { render::ItemKey::Builder().withTypeMeta() }; }; From a74b093b60c3541c96e541bba44d5d799361469e Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Thu, 11 Jan 2018 21:36:55 +0300 Subject: [PATCH 32/58] FB11109 Preview disabled image swap (Commerce only) --- .../resources/images/preview-privacy.png | Bin 0 -> 51845 bytes interface/src/Application.cpp | 3 ++ .../display-plugins/hmd/HmdDisplayPlugin.cpp | 14 ++---- libraries/ui/src/DesktopPreviewProvider.cpp | 45 ++++++++++++++++++ libraries/ui/src/DesktopPreviewProvider.h | 41 ++++++++++++++++ scripts/system/commerce/wallet.js | 8 +++- 6 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 interface/resources/images/preview-privacy.png create mode 100644 libraries/ui/src/DesktopPreviewProvider.cpp create mode 100644 libraries/ui/src/DesktopPreviewProvider.h diff --git a/interface/resources/images/preview-privacy.png b/interface/resources/images/preview-privacy.png new file mode 100644 index 0000000000000000000000000000000000000000..1a556b37f25f02cf0f3e1c1ec294725daef6cea1 GIT binary patch literal 51845 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 literal 0 HcmV?d00001 diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6849bd07b5..85545e9def 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -209,6 +209,7 @@ #include "commerce/QmlCommerce.h" #include "webbrowser/WebBrowserSuggestionsEngine.h" +#include // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. @@ -632,6 +633,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(std::bind(&Application::getUserAgent, qApp)); DependencyManager::set(); DependencyManager::set(ScriptEngine::CLIENT_SCRIPT); @@ -5758,6 +5760,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerFunction("OverlayWindow", QmlWindowClass::constructor); scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("DesktopPreviewProvider", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("Snapshot", DependencyManager::get().data()); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 1d7fee38eb..90bb83a663 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -33,6 +33,7 @@ #include "../Logging.h" #include "../CompositorHelper.h" +#include "DesktopPreviewProvider.h" #include "render-utils/hmd_ui_vert.h" #include "render-utils/hmd_ui_frag.h" @@ -254,17 +255,9 @@ void HmdDisplayPlugin::internalPresent() { swapBuffers(); } else if (_clearPreviewFlag) { - QImage image; - if (_vsyncEnabled) { - image = QImage(PathUtils::resourcesPath() + "images/preview.png"); - } else { - image = QImage(PathUtils::resourcesPath() + "images/preview-disabled.png"); - } - image = image.mirrored(); - image = image.convertToFormat(QImage::Format_RGBA8888); - if (!_previewTexture) { - _previewTexture = gpu::Texture::createStrict( + QImage image = DesktopPreviewProvider::getInstance()->getPreviewDisabledImage(_vsyncEnabled); + _previewTexture = gpu::Texture::createStrict( gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, @@ -274,7 +267,6 @@ void HmdDisplayPlugin::internalPresent() { _previewTexture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); _previewTexture->assignStoredMip(0, image.byteCount(), image.constBits()); _previewTexture->setAutoGenerateMips(true); - } auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); diff --git a/libraries/ui/src/DesktopPreviewProvider.cpp b/libraries/ui/src/DesktopPreviewProvider.cpp new file mode 100644 index 0000000000..4eeb058a57 --- /dev/null +++ b/libraries/ui/src/DesktopPreviewProvider.cpp @@ -0,0 +1,45 @@ +#include "DesktopPreviewProvider.h" +#include +#include +#include +#include + +DesktopPreviewProvider::DesktopPreviewProvider() { +} + +QSharedPointer DesktopPreviewProvider::getInstance() { + static QSharedPointer instance = DependencyManager::get(); + return instance; +} + +QImage DesktopPreviewProvider::getPreviewDisabledImage(bool vsyncEnabled) const { + + auto imageIndex = vsyncEnabled ? VSYNC : m_previewDisabledReason; + assert(imageIndex >= 0 && imageIndex <= VSYNC); + + static const QString imagePaths[] = { + "images/preview-disabled.png", // USER + "images/preview-privacy.png", // WALLET + "images/preview.png", // VSYNC + }; + + return !m_previewDisabled[imageIndex].isNull() ? m_previewDisabled[imageIndex] : loadPreviewImage(m_previewDisabled[imageIndex], PathUtils::resourcesPath() + imagePaths[imageIndex]); +} + +void DesktopPreviewProvider::setPreviewDisabledReason(PreviewDisabledReasons reason) { + m_previewDisabledReason = reason; +} + +void DesktopPreviewProvider::setPreviewDisabledReason(const QString& reasonString) { + PreviewDisabledReasons reason = USER; + bool ok = false; + + reason = (PreviewDisabledReasons) QMetaEnum::fromType().keyToValue(reasonString.toLatin1().data(), &ok); + if (ok) { + setPreviewDisabledReason(reason); + } +} + +QImage& DesktopPreviewProvider::loadPreviewImage(QImage& image, const QString& path) const { + return image = QImage(path).mirrored().convertToFormat(QImage::Format_RGBA8888); +} \ No newline at end of file diff --git a/libraries/ui/src/DesktopPreviewProvider.h b/libraries/ui/src/DesktopPreviewProvider.h new file mode 100644 index 0000000000..2204f26378 --- /dev/null +++ b/libraries/ui/src/DesktopPreviewProvider.h @@ -0,0 +1,41 @@ +// +// Created by Alexander Ivash on 2018/01/08 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include + +class DesktopPreviewProvider : public QObject, public Dependency { + SINGLETON_DEPENDENCY + Q_OBJECT + + DesktopPreviewProvider(); + DesktopPreviewProvider(const DesktopPreviewProvider& other) = delete; +public: + enum PreviewDisabledReasons { + USER = 0, + SECURE_SCREEN + }; + Q_ENUM(PreviewDisabledReasons) + + int VSYNC { 2 }; + + static QSharedPointer DesktopPreviewProvider::getInstance(); + + QImage getPreviewDisabledImage(bool vsyncEnabled) const; + void setPreviewDisabledReason(PreviewDisabledReasons reason); + +public slots: + void setPreviewDisabledReason(const QString& reason); + +private: + QImage& loadPreviewImage(QImage& image, const QString& path) const; + + PreviewDisabledReasons m_previewDisabledReason = { USER }; + + mutable QImage m_previewDisabled[3]; +}; \ No newline at end of file diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 0826325a57..f93d6714f1 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -562,9 +562,11 @@ break; case 'disableHmdPreview': isHmdPreviewDisabled = Menu.isOptionChecked("Disable Preview"); + DesktopPreviewProvider.setPreviewDisabledReason("SECURE_SCREEN"); Menu.setIsOptionChecked("Disable Preview", true); break; case 'maybeEnableHmdPreview': + DesktopPreviewProvider.setPreviewDisabledReason("USER"); Menu.setIsOptionChecked("Disable Preview", isHmdPreviewDisabled); break; case 'passphraseReset': @@ -635,7 +637,11 @@ // -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string // value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. function onTabletScreenChanged(type, url) { - onWalletScreen = (type === "QML" && url === WALLET_QML_SOURCE); + var onWalletScreenNow = (type === "QML" && url === WALLET_QML_SOURCE); + if (!onWalletScreenNow && onWalletScreen) { + DesktopPreviewProvider.setPreviewDisabledReason("USER"); + } + onWalletScreen = onWalletScreenNow; wireEventBridge(onWalletScreen); // Change button to active when window is first openend, false otherwise. if (button) { From 744da710a313a18ad80fe16b5cd0b79f5a093ab4 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Tue, 16 Jan 2018 01:41:23 +0300 Subject: [PATCH 33/58] move VSYNC into 'PreviewDisabledReasons' enum --- libraries/ui/src/DesktopPreviewProvider.cpp | 4 ++++ libraries/ui/src/DesktopPreviewProvider.h | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/libraries/ui/src/DesktopPreviewProvider.cpp b/libraries/ui/src/DesktopPreviewProvider.cpp index 4eeb058a57..92a8d9e055 100644 --- a/libraries/ui/src/DesktopPreviewProvider.cpp +++ b/libraries/ui/src/DesktopPreviewProvider.cpp @@ -27,6 +27,10 @@ QImage DesktopPreviewProvider::getPreviewDisabledImage(bool vsyncEnabled) const } void DesktopPreviewProvider::setPreviewDisabledReason(PreviewDisabledReasons reason) { + if (reason == VSYNC) { + return; // Not settable via this interface, as VSYNC is controlled by HMD plugin.. + } + m_previewDisabledReason = reason; } diff --git a/libraries/ui/src/DesktopPreviewProvider.h b/libraries/ui/src/DesktopPreviewProvider.h index 2204f26378..40a4640f85 100644 --- a/libraries/ui/src/DesktopPreviewProvider.h +++ b/libraries/ui/src/DesktopPreviewProvider.h @@ -18,12 +18,11 @@ class DesktopPreviewProvider : public QObject, public Dependency { public: enum PreviewDisabledReasons { USER = 0, - SECURE_SCREEN + SECURE_SCREEN, + VSYNC // Not settable via this interface, as VSYNC is controlled by HMD plugin.. }; Q_ENUM(PreviewDisabledReasons) - int VSYNC { 2 }; - static QSharedPointer DesktopPreviewProvider::getInstance(); QImage getPreviewDisabledImage(bool vsyncEnabled) const; From 576ae227a86a9644962d82588028349116aa6b44 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Tue, 16 Jan 2018 01:51:46 +0300 Subject: [PATCH 34/58] move imagePaths into .h so simplify matching enum values to image files --- libraries/ui/src/DesktopPreviewProvider.cpp | 6 ------ libraries/ui/src/DesktopPreviewProvider.h | 7 +++++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/libraries/ui/src/DesktopPreviewProvider.cpp b/libraries/ui/src/DesktopPreviewProvider.cpp index 92a8d9e055..d86e1f951f 100644 --- a/libraries/ui/src/DesktopPreviewProvider.cpp +++ b/libraries/ui/src/DesktopPreviewProvider.cpp @@ -17,12 +17,6 @@ QImage DesktopPreviewProvider::getPreviewDisabledImage(bool vsyncEnabled) const auto imageIndex = vsyncEnabled ? VSYNC : m_previewDisabledReason; assert(imageIndex >= 0 && imageIndex <= VSYNC); - static const QString imagePaths[] = { - "images/preview-disabled.png", // USER - "images/preview-privacy.png", // WALLET - "images/preview.png", // VSYNC - }; - return !m_previewDisabled[imageIndex].isNull() ? m_previewDisabled[imageIndex] : loadPreviewImage(m_previewDisabled[imageIndex], PathUtils::resourcesPath() + imagePaths[imageIndex]); } diff --git a/libraries/ui/src/DesktopPreviewProvider.h b/libraries/ui/src/DesktopPreviewProvider.h index 40a4640f85..db87009205 100644 --- a/libraries/ui/src/DesktopPreviewProvider.h +++ b/libraries/ui/src/DesktopPreviewProvider.h @@ -15,6 +15,13 @@ class DesktopPreviewProvider : public QObject, public Dependency { DesktopPreviewProvider(); DesktopPreviewProvider(const DesktopPreviewProvider& other) = delete; + + constexpr static char* imagePaths[] = { + "images/preview-disabled.png", // USER + "images/preview-privacy.png", // SECURE_SCREEN + "images/preview.png", // VSYNC + }; + public: enum PreviewDisabledReasons { USER = 0, From be3789e70a67e90f1fd921bdea1b88903948a506 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Tue, 16 Jan 2018 03:18:38 +0300 Subject: [PATCH 35/58] show warning on attempt to force 'VSYNC' preview reason --- libraries/ui/src/DesktopPreviewProvider.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/ui/src/DesktopPreviewProvider.cpp b/libraries/ui/src/DesktopPreviewProvider.cpp index d86e1f951f..4ae70a1eb0 100644 --- a/libraries/ui/src/DesktopPreviewProvider.cpp +++ b/libraries/ui/src/DesktopPreviewProvider.cpp @@ -22,6 +22,7 @@ QImage DesktopPreviewProvider::getPreviewDisabledImage(bool vsyncEnabled) const void DesktopPreviewProvider::setPreviewDisabledReason(PreviewDisabledReasons reason) { if (reason == VSYNC) { + qDebug() << "Preview disabled reason can't be forced to " << QMetaEnum::fromType().valueToKey(reason); return; // Not settable via this interface, as VSYNC is controlled by HMD plugin.. } From 29625953f6ec662e93585f014e2dfe68d5ef0616 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Wed, 17 Jan 2018 23:06:55 +0300 Subject: [PATCH 36/58] update privacy-specific disabled preview image --- .../resources/images/preview-privacy.png | Bin 51845 -> 52759 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/interface/resources/images/preview-privacy.png b/interface/resources/images/preview-privacy.png index 1a556b37f25f02cf0f3e1c1ec294725daef6cea1..1a718de23357eec659b05e590af7d1c2165d531c 100644 GIT binary patch literal 52759 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 literal 51845 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 From 50217726c0d026ce4412a5fcb8706562e91851cf Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Thu, 18 Jan 2018 22:29:07 +0300 Subject: [PATCH 37/58] remove extra qualification from DesktopPreviewProvider::getInstance() in .h --- libraries/ui/src/DesktopPreviewProvider.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/ui/src/DesktopPreviewProvider.h b/libraries/ui/src/DesktopPreviewProvider.h index db87009205..07d39a6cdf 100644 --- a/libraries/ui/src/DesktopPreviewProvider.h +++ b/libraries/ui/src/DesktopPreviewProvider.h @@ -30,7 +30,7 @@ public: }; Q_ENUM(PreviewDisabledReasons) - static QSharedPointer DesktopPreviewProvider::getInstance(); + static QSharedPointer getInstance(); QImage getPreviewDisabledImage(bool vsyncEnabled) const; void setPreviewDisabledReason(PreviewDisabledReasons reason); From c8aded068814b238348a409516e2fd46a224edcf Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 18 Jan 2018 11:54:54 -0800 Subject: [PATCH 38/58] watch for attachment texture loading --- .../src/avatars-renderer/Avatar.cpp | 19 +++++++++++++++++-- .../src/avatars-renderer/Avatar.h | 1 + 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 95a8a9f1fd..500a24763d 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -791,10 +791,19 @@ bool Avatar::shouldRenderHead(const RenderArgs* renderArgs) const { // virtual void Avatar::simulateAttachments(float deltaTime) { + assert(_attachmentModels.size() == _attachmentModelsTexturesLoaded.size()); PerformanceTimer perfTimer("attachments"); for (int i = 0; i < (int)_attachmentModels.size(); i++) { const AttachmentData& attachment = _attachmentData.at(i); auto& model = _attachmentModels.at(i); + bool texturesLoaded = _attachmentModelsTexturesLoaded.at(i); + + // Watch for texture loading + if (!texturesLoaded && model->getGeometry() && model->getGeometry()->areTexturesLoaded()) { + _attachmentModelsTexturesLoaded[i] = true; + model->updateRenderItems(); + } + int jointIndex = getJointIndex(attachment.jointName); glm::vec3 jointPosition; glm::quat jointRotation; @@ -1319,6 +1328,7 @@ void Avatar::setAttachmentData(const QVector& attachmentData) { while ((int)_attachmentModels.size() > attachmentData.size()) { auto attachmentModel = _attachmentModels.back(); _attachmentModels.pop_back(); + _attachmentModelsTexturesLoaded.pop_back(); _attachmentsToRemove.push_back(attachmentModel); } @@ -1326,11 +1336,16 @@ void Avatar::setAttachmentData(const QVector& attachmentData) { if (i == (int)_attachmentModels.size()) { // if number of attachments has been increased, we need to allocate a new model _attachmentModels.push_back(allocateAttachmentModel(attachmentData[i].isSoft, _skeletonModel->getRig(), isMyAvatar())); - } - else if (i < oldAttachmentData.size() && oldAttachmentData[i].isSoft != attachmentData[i].isSoft) { + _attachmentModelsTexturesLoaded.push_back(false); + } else if (i < oldAttachmentData.size() && oldAttachmentData[i].isSoft != attachmentData[i].isSoft) { // if the attachment has changed type, we need to re-allocate a new one. _attachmentsToRemove.push_back(_attachmentModels[i]); _attachmentModels[i] = allocateAttachmentModel(attachmentData[i].isSoft, _skeletonModel->getRig(), isMyAvatar()); + _attachmentModelsTexturesLoaded[i] = false; + } + // If the model URL has changd, we need to wait for the textures to load + if (_attachmentModels[i]->getURL() != attachmentData[i].modelURL) { + _attachmentModelsTexturesLoaded[i] = false; } _attachmentModels[i]->setURL(attachmentData[i].modelURL); } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 3fe14f980f..c2b404a925 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -306,6 +306,7 @@ protected: glm::vec3 _skeletonOffset; std::vector> _attachmentModels; + std::vector _attachmentModelsTexturesLoaded; std::vector> _attachmentsToRemove; std::vector> _attachmentsToDelete; From 9a523ead317c68d4fff028f0bbd61e7dcaa9cb31 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 18 Jan 2018 12:44:45 -0800 Subject: [PATCH 39/58] fix link --- interface/src/commerce/Ledger.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 0533b26e5a..959f309ff3 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -144,7 +144,7 @@ QString userLink(const QString& username, const QString& placename) { if (placename.isEmpty()) { return QString("someone"); } else { - return QString("someone in ").arg(PLACE_PAGE_BASE_URL, placename); + return QString("someone nearby").arg(PLACE_PAGE_BASE_URL, placename); } } return QString("%2").arg(USER_PAGE_BASE_URL, username); From 961ed88fb094515e95afd656ba2e920073cabfa5 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 18 Jan 2018 12:56:24 -0800 Subject: [PATCH 40/58] fixed coding standards in RenderableModelEntityItem.cpp --- .../entities-renderer/src/RenderableModelEntityItem.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 8b5a23b787..fc1688974c 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1397,9 +1397,8 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce if (!jointsMapped()) { mapJoints(entity, model->getJointNames()); - } - //else the joints have been mapped before but we have new animation to load - else if (_animation && (_animation->getURL().toString() != entity->getAnimationURL())) { + //else the joint have been mapped before but we have a new animation to load + } else if (_animation && (_animation->getURL().toString() != entity->getAnimationURL())) { _animation = DependencyManager::get()->getAnimation(entity->getAnimationURL()); _jointMappingCompleted = false; mapJoints(entity, model->getJointNames()); From 13bbd1fcbfa3e4b6501c661300977fef8f9cd71d Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 19 Jan 2018 10:10:33 +1300 Subject: [PATCH 41/58] Update places feed to use new function --- interface/resources/qml/hifi/Feed.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 8576e26fcd..a9629c0e4e 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -102,7 +102,7 @@ Column { 'include_actions=' + actions, 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), 'require_online=true', - 'protocol=' + encodeURIComponent(AddressManager.protocolVersion()), + 'protocol=' + encodeURIComponent(Window.protocolSignature()), 'page=' + pageNumber ]; var url = metaverseBase + 'user_stories?' + options.join('&'); From 21cc6ee8dd52391e76ededba69f27b3230a6ab7a Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 18 Jan 2018 14:28:25 -0800 Subject: [PATCH 42/58] fix marketplace txn display (memo only) --- interface/src/commerce/Ledger.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index e65546542d..b702b49367 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -163,7 +163,7 @@ QString transactionString(const QJsonObject& valueObject) { QDateTime createdAt(QDateTime::fromSecsSinceEpoch(dateInteger, Qt::UTC)); QString result; - if (sentCerts <= 0 && receivedCerts <= 0) { + if (sentCerts <= 0 && receivedCerts <= 0 && !KNOWN_USERS.contains(valueObject["sender_name"].toString())) { // this is an hfc transfer. if (sent > 0) { QString recipient = userLink(valueObject["recipient_name"].toString(), valueObject["place_name"].toString()); From 844ec6a8636f3b2fc228738ac76493ed4471bf03 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 18 Jan 2018 16:02:40 -0800 Subject: [PATCH 43/58] eliminate dash in history, replace with 0 HFC --- .../resources/qml/hifi/commerce/wallet/WalletHome.qml | 1 - interface/src/commerce/Ledger.cpp | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 81d38bc0dd..b980c13e3c 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -392,7 +392,6 @@ Item { width: 118; height: paintedHeight; wrapMode: Text.WordWrap; - font.bold: true; // Alignment horizontalAlignment: Text.AlignRight; } diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index b702b49367..10ddd4c110 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -123,17 +123,17 @@ QString hfcString(const QJsonValue& sentValue, const QJsonValue& receivedValue) int sent = sentValue.toInt(); int received = receivedValue.toInt(); if (sent <= 0 && received <= 0) { - return QString("-"); + return QString("0 HFC"); } QString result; if (sent > 0) { - result += QString("-%1 HFC").arg(sent); + result += QString("-%1 HFC").arg(sent); if (received > 0) { result += QString("
"); } } if (received > 0) { - result += QString("%1 HFC").arg(received); + result += QString("%1 HFC").arg(received); } return result; } @@ -178,8 +178,8 @@ QString transactionString(const QJsonObject& valueObject) { } else { result += valueObject["message"].toString(); } + // no matter what we append a smaller date to the bottom of this... - result += QString("
%1").arg(createdAt.toLocalTime().toString(Qt::DefaultLocaleShortDate)); return result; } From a97b4b010dbc2c32089000d153fea86ac0209e47 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 18 Jan 2018 16:51:28 -0800 Subject: [PATCH 44/58] suppress Interface notification for DS decryption error --- scripts/system/notifications.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 11c083dacc..728760c1e7 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -539,8 +539,14 @@ return startingUp; } - function onDomainConnectionRefused(reason) { - createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); + function onDomainConnectionRefused(reason, reasonCode) { + // the "login error" reason means that the DS couldn't decrypt the username signature + // since this eventually resolves itself for good actors we don't need to show a notification for it + var LOGIN_ERROR_REASON_CODE = 2; + + if (reasonCode != LOGIN_ERROR_REASON_CODE) { + createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); + } } function onEditError(msg) { From 497e8c1d694cb8a4a011dcefa836a89bccc3d11a Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 18 Jan 2018 15:14:26 -0800 Subject: [PATCH 45/58] Fix BUILD_LINUX_CHEATSHEET.md formatting --- BUILD_LINUX_CHEATSHEET.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/BUILD_LINUX_CHEATSHEET.md b/BUILD_LINUX_CHEATSHEET.md index 7d77f5d685..9e7534418a 100644 --- a/BUILD_LINUX_CHEATSHEET.md +++ b/BUILD_LINUX_CHEATSHEET.md @@ -1,5 +1,7 @@ - # this guide is specific to Ubuntu 16.04. - # deb packages of High Fidelity domain server and assignment client are stored on debian.highfidelity.com +## This guide is specific to Ubuntu 16.04. +Deb packages of High Fidelity domain server and assignment client are stored on debian.highfidelity.com + +``` sudo su - apt-get -y update apt-get install -y software-properties-common @@ -8,20 +10,27 @@ add-apt-repository "deb http://debian.highfidelity.com stable main" apt-get -y update apt-get install -y hifi-domain-server apt-get install -y hifi-assignment-client +``` - # When installing master/dev builds, the packages are slightly different and you just need to change the last 2 steps to: +When installing master/dev builds, the packages are slightly different and you just need to change the last 2 steps to: +``` apt-get install -y hifi-dev-domain-server apt-get install -y hifi-dev-assignment-client +``` - # domain server and assignment clients should already be running. The processes are controlled via: +Domain server and assignment clients should already be running. The processes are controlled via: +``` systemctl start hifi-domain-server systemctl stop hifi-domain-server +``` - # Once the machine is setup and processes are running you should ensure that your firewall exposes port 40100 on TCP and all UDP ports. This will get your domain up and running and you could connect to it (for now) by using High Fidelity Interface and typing in the IP for the place name. (further customizations can be done via http://IPAddress:40100). - - # The server always depends on both hifi-domain-server and hifi-assignment-client running at the same time. - # As an additional step, you should ensure that your packages are automatically updated when a new version goes out. You could, for example, set the automatic update checks to happen every hour (though this could potentially result in the domain being unreachable for a whole hour by new clients when they are released - adjust the update checks accordingly). +Once the machine is setup and processes are running, you should ensure that your firewall exposes port 40100 on TCP and all UDP ports. This will get your domain up and running and you could connect to it (for now) by using High Fidelity Interface and typing in the IP for the place name. (Further customizations can be done via http://IPAddress:40100). + +The server always depends on both hifi-domain-server and hifi-assignment-client running at the same time. +As an additional step, you should ensure that your packages are automatically updated when a new version goes out. You could, for example, set the automatic update checks to happen every hour (though this could potentially result in the domain being unreachable for a whole hour by new clients when they are released - adjust the update checks accordingly). To do this you can modify /etc/crontab by adding the following lines +``` 0 */1 * * * root apt-get update 1 */1 * * * root apt-get install --only-upgrade -y hifi-domain-server 2 */1 * * * root apt-get install --only-upgrade -y hifi-assignment-client +``` From 3d3bfcf7a315a682972d741125007260c461af60 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Tue, 16 Jan 2018 12:35:03 -0800 Subject: [PATCH 46/58] check dirty flags when harvesting physics results --- libraries/entities/src/EntityItem.cpp | 47 +++++++++++++++++++++ libraries/entities/src/EntityItem.h | 8 ++++ libraries/physics/src/EntityMotionState.cpp | 33 ++++----------- libraries/shared/src/GLMHelpers.h | 1 + libraries/shared/src/SpatiallyNestable.cpp | 30 +++++++++++++ libraries/shared/src/SpatiallyNestable.h | 1 + 6 files changed, 95 insertions(+), 25 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index fe5213baa8..8ea1de7666 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1610,6 +1610,37 @@ void EntityItem::setPosition(const glm::vec3& value) { } } +void EntityItem::setWorldTransformAndVelocitiesUnlessDirtyFlags( + const glm::vec3& position, + const glm::quat& orientation, + const glm::vec3& linearVelocity, + const glm::vec3& angularVelocity) { + // only ever call this for harvesting results of physics simulation + // if a dirty bit is set then an update arrived (via script or network) overriding the physics simulation + uint32_t flags = _dirtyFlags & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES); + if (!flags) { + // flags are clear + setWorldTransform(position, orientation); + setWorldVelocity(linearVelocity); + setWorldAngularVelocity(angularVelocity); + setLastSimulated(usecTimestampNow()); + } else { + // only set properties NOT flagged + if (!(flags & Simulation::DIRTY_TRANSFORM)) { + setWorldTransform(position, orientation); + } + if (!(flags & Simulation::DIRTY_LINEAR_VELOCITY)) { + setWorldVelocity(linearVelocity); + } + if (!(flags & Simulation::DIRTY_ANGULAR_VELOCITY)) { + setWorldAngularVelocity(angularVelocity); + } + if (flags != (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES)) { + setLastSimulated(usecTimestampNow()); + } + } +} + void EntityItem::setParentID(const QUuid& value) { QUuid oldParentID = getParentID(); if (oldParentID != value) { @@ -1739,6 +1770,22 @@ void EntityItem::setVelocity(const glm::vec3& value) { } } +void EntityItem::zeroAllVelocitiesUnlessDirtyFlags() { + uint32_t flags = _dirtyFlags & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES); + if (!flags) { + setWorldVelocity(glm::vec3(0.0f)); + setWorldAngularVelocity(glm::vec3(0.0f)); + } else { + if (!(flags & Simulation::DIRTY_LINEAR_VELOCITY)) { + setWorldVelocity(glm::vec3(0.0f)); + } + if (!(flags & Simulation::DIRTY_ANGULAR_VELOCITY)) { + setWorldAngularVelocity(glm::vec3(0.0f)); + } + } + _acceleration = glm::vec3(0.0f); +} + void EntityItem::setDamping(float value) { auto clampedDamping = glm::clamp(value, 0.0f, 1.0f); withWriteLock([&] { diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index ecfb7b5dcd..db2e7f7641 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -355,6 +355,7 @@ public: void setRotation(glm::quat orientation); void setVelocity(const glm::vec3& velocity); + void zeroAllVelocitiesUnlessDirtyFlags(); uint32_t getDirtyFlags() const; void markDirtyFlags(uint32_t mask); @@ -368,6 +369,13 @@ public: void* getPhysicsInfo() const { return _physicsInfo; } void setPhysicsInfo(void* data) { _physicsInfo = data; } + + void setWorldTransformAndVelocitiesUnlessDirtyFlags( + const glm::vec3& position, + const glm::quat& orientation, + const glm::vec3& linearVelocity, + const glm::vec3& angularVelocity); + EntityTreeElementPointer getElement() const { return _element; } EntityTreePointer getTree() const; virtual SpatialParentTree* getParentTree() const override; diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 33ac887f4f..fe3e242a70 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -256,25 +256,12 @@ void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { assert(_entity); assert(entityTreeIsLocked()); measureBodyAcceleration(); - bool positionSuccess; - _entity->setWorldPosition(bulletToGLM(worldTrans.getOrigin()) + ObjectMotionState::getWorldOffset(), positionSuccess, false); - if (!positionSuccess) { - static QString repeatedMessage = - LogHandler::getInstance().addRepeatedMessageRegex("EntityMotionState::setWorldTransform " - "setPosition failed.*"); - qCDebug(physics) << "EntityMotionState::setWorldTransform setPosition failed" << _entity->getID(); - } - bool orientationSuccess; - _entity->setWorldOrientation(bulletToGLM(worldTrans.getRotation()), orientationSuccess, false); - if (!orientationSuccess) { - static QString repeatedMessage = - LogHandler::getInstance().addRepeatedMessageRegex("EntityMotionState::setWorldTransform " - "setOrientation failed.*"); - qCDebug(physics) << "EntityMotionState::setWorldTransform setOrientation failed" << _entity->getID(); - } - _entity->setVelocity(getBodyLinearVelocity()); - _entity->setAngularVelocity(getBodyAngularVelocity()); - _entity->setLastSimulated(usecTimestampNow()); + + _entity->setWorldTransformAndVelocitiesUnlessDirtyFlags( + bulletToGLM(worldTrans.getOrigin()), + bulletToGLM(worldTrans.getRotation()), + getBodyLinearVelocity(), + getBodyAngularVelocity()); if (_entity->getSimulatorID().isNull()) { _loopsWithoutOwner++; @@ -530,9 +517,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ if (!_body->isActive()) { // make sure all derivatives are zero - _entity->setVelocity(Vectors::ZERO); - _entity->setAngularVelocity(Vectors::ZERO); - _entity->setAcceleration(Vectors::ZERO); + _entity->zeroAllVelocitiesUnlessDirtyFlags(); _numInactiveUpdates++; } else { glm::vec3 gravity = _entity->getGravity(); @@ -559,9 +544,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ if (movingSlowly) { // velocities might not be zero, but we'll fake them as such, which will hopefully help convince // other simulating observers to deactivate their own copies - glm::vec3 zero(0.0f); - _entity->setVelocity(zero); - _entity->setAngularVelocity(zero); + _entity->zeroAllVelocitiesUnlessDirtyFlags(); } } _numInactiveUpdates = 0; diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index 973998b927..4f761a4aac 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -253,6 +253,7 @@ glm::vec2 getFacingDir2D(const glm::mat4& m); inline bool isNaN(const glm::vec3& value) { return isNaN(value.x) || isNaN(value.y) || isNaN(value.z); } inline bool isNaN(const glm::quat& value) { return isNaN(value.w) || isNaN(value.x) || isNaN(value.y) || isNaN(value.z); } +inline bool isNaN(const glm::mat3& value) { return isNaN(value * glm::vec3(1.0f)); } glm::mat4 orthoInverse(const glm::mat4& m); diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 20a5a76b73..324cee3417 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -464,6 +464,36 @@ glm::vec3 SpatiallyNestable::localToWorldDimensions(const glm::vec3& dimensions, return dimensions; } +void SpatiallyNestable::setWorldTransform(const glm::vec3& position, const glm::quat& orientation) { + // guard against introducing NaN into the transform + if (isNaN(orientation) || isNaN(position)) { + return; + } + + bool changed = false; + bool success = true; + Transform parentTransform = getParentTransform(success); + _transformLock.withWriteLock([&] { + Transform myWorldTransform; + Transform::mult(myWorldTransform, parentTransform, _transform); + if (myWorldTransform.getRotation() != orientation) { + changed = true; + myWorldTransform.setRotation(orientation); + } + if (myWorldTransform.getTranslation() != position) { + changed = true; + myWorldTransform.setTranslation(position); + } + if (changed) { + Transform::inverseMult(_transform, parentTransform, myWorldTransform); + _translationChanged = usecTimestampNow(); + } + }); + if (success && changed) { + locationChanged(false); + } +} + glm::vec3 SpatiallyNestable::getWorldPosition(bool& success) const { return getTransform(success).getTranslation(); } diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index 2a315e9230..090ca4c266 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -87,6 +87,7 @@ public: virtual Transform getParentTransform(bool& success, int depth = 0) const; + void setWorldTransform(const glm::vec3& position, const glm::quat& orientation); virtual glm::vec3 getWorldPosition(bool& success) const; virtual glm::vec3 getWorldPosition() const; virtual void setWorldPosition(const glm::vec3& position, bool& success, bool tellPhysics = true); From 75b5635d2fbf9c0fad8ee4a041061deeefc284d3 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 17 Jan 2018 13:51:07 -0800 Subject: [PATCH 47/58] less word salad --- libraries/entities/src/EntityItem.cpp | 47 ------------------ libraries/entities/src/EntityItem.h | 7 --- libraries/physics/src/EntityMotionState.cpp | 53 ++++++++++++++++++--- libraries/physics/src/EntityMotionState.h | 1 + 4 files changed, 47 insertions(+), 61 deletions(-) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 8ea1de7666..fe5213baa8 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1610,37 +1610,6 @@ void EntityItem::setPosition(const glm::vec3& value) { } } -void EntityItem::setWorldTransformAndVelocitiesUnlessDirtyFlags( - const glm::vec3& position, - const glm::quat& orientation, - const glm::vec3& linearVelocity, - const glm::vec3& angularVelocity) { - // only ever call this for harvesting results of physics simulation - // if a dirty bit is set then an update arrived (via script or network) overriding the physics simulation - uint32_t flags = _dirtyFlags & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES); - if (!flags) { - // flags are clear - setWorldTransform(position, orientation); - setWorldVelocity(linearVelocity); - setWorldAngularVelocity(angularVelocity); - setLastSimulated(usecTimestampNow()); - } else { - // only set properties NOT flagged - if (!(flags & Simulation::DIRTY_TRANSFORM)) { - setWorldTransform(position, orientation); - } - if (!(flags & Simulation::DIRTY_LINEAR_VELOCITY)) { - setWorldVelocity(linearVelocity); - } - if (!(flags & Simulation::DIRTY_ANGULAR_VELOCITY)) { - setWorldAngularVelocity(angularVelocity); - } - if (flags != (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES)) { - setLastSimulated(usecTimestampNow()); - } - } -} - void EntityItem::setParentID(const QUuid& value) { QUuid oldParentID = getParentID(); if (oldParentID != value) { @@ -1770,22 +1739,6 @@ void EntityItem::setVelocity(const glm::vec3& value) { } } -void EntityItem::zeroAllVelocitiesUnlessDirtyFlags() { - uint32_t flags = _dirtyFlags & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES); - if (!flags) { - setWorldVelocity(glm::vec3(0.0f)); - setWorldAngularVelocity(glm::vec3(0.0f)); - } else { - if (!(flags & Simulation::DIRTY_LINEAR_VELOCITY)) { - setWorldVelocity(glm::vec3(0.0f)); - } - if (!(flags & Simulation::DIRTY_ANGULAR_VELOCITY)) { - setWorldAngularVelocity(glm::vec3(0.0f)); - } - } - _acceleration = glm::vec3(0.0f); -} - void EntityItem::setDamping(float value) { auto clampedDamping = glm::clamp(value, 0.0f, 1.0f); withWriteLock([&] { diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index db2e7f7641..f9559a375b 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -355,7 +355,6 @@ public: void setRotation(glm::quat orientation); void setVelocity(const glm::vec3& velocity); - void zeroAllVelocitiesUnlessDirtyFlags(); uint32_t getDirtyFlags() const; void markDirtyFlags(uint32_t mask); @@ -370,12 +369,6 @@ public: void setPhysicsInfo(void* data) { _physicsInfo = data; } - void setWorldTransformAndVelocitiesUnlessDirtyFlags( - const glm::vec3& position, - const glm::quat& orientation, - const glm::vec3& linearVelocity, - const glm::vec3& angularVelocity); - EntityTreeElementPointer getElement() const { return _element; } EntityTreePointer getTree() const; virtual SpatialParentTree* getParentTree() const override; diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index fe3e242a70..420da5a1e0 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -257,11 +257,31 @@ void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { assert(entityTreeIsLocked()); measureBodyAcceleration(); - _entity->setWorldTransformAndVelocitiesUnlessDirtyFlags( - bulletToGLM(worldTrans.getOrigin()), - bulletToGLM(worldTrans.getRotation()), - getBodyLinearVelocity(), - getBodyAngularVelocity()); + // If transform or velocities are flagged as dirty it means a network or scripted change + // occured between the beginning and end of the stepSimulation() and we DON'T want to apply + // these physics simulation results. + uint32_t flags = _entity->getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES); + if (!flags) { + // flags are clear + _entity->setWorldTransform(bulletToGLM(worldTrans.getOrigin()), bulletToGLM(worldTrans.getRotation())); + _entity->setWorldVelocity(getBodyLinearVelocity()); + _entity->setWorldAngularVelocity(getBodyAngularVelocity()); + _entity->setLastSimulated(usecTimestampNow()); + } else { + // only set properties NOT flagged + if (!(flags & Simulation::DIRTY_TRANSFORM)) { + _entity->setWorldTransform(bulletToGLM(worldTrans.getOrigin()), bulletToGLM(worldTrans.getRotation())); + } + if (!(flags & Simulation::DIRTY_LINEAR_VELOCITY)) { + _entity->setWorldVelocity(getBodyLinearVelocity()); + } + if (!(flags & Simulation::DIRTY_ANGULAR_VELOCITY)) { + _entity->setWorldAngularVelocity(getBodyAngularVelocity()); + } + if (flags != (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES)) { + _entity->setLastSimulated(usecTimestampNow()); + } + } if (_entity->getSimulatorID().isNull()) { _loopsWithoutOwner++; @@ -517,7 +537,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ if (!_body->isActive()) { // make sure all derivatives are zero - _entity->zeroAllVelocitiesUnlessDirtyFlags(); + zeroCleanObjectVelocities(); _numInactiveUpdates++; } else { glm::vec3 gravity = _entity->getGravity(); @@ -544,7 +564,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ if (movingSlowly) { // velocities might not be zero, but we'll fake them as such, which will hopefully help convince // other simulating observers to deactivate their own copies - _entity->zeroAllVelocitiesUnlessDirtyFlags(); + zeroCleanObjectVelocities(); } } _numInactiveUpdates = 0; @@ -801,3 +821,22 @@ bool EntityMotionState::shouldBeLocallyOwned() const { void EntityMotionState::upgradeOutgoingPriority(uint8_t priority) { _outgoingPriority = glm::max(_outgoingPriority, priority); } + +void EntityMotionState::zeroCleanObjectVelocities() const { + // If transform or velocities are flagged as dirty it means a network or scripted change + // occured between the beginning and end of the stepSimulation() and we DON'T want to apply + // these physics simulation results. + uint32_t flags = _entity->getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES); + if (!flags) { + _entity->setWorldVelocity(glm::vec3(0.0f)); + _entity->setWorldAngularVelocity(glm::vec3(0.0f)); + } else { + if (!(flags & Simulation::DIRTY_LINEAR_VELOCITY)) { + _entity->setWorldVelocity(glm::vec3(0.0f)); + } + if (!(flags & Simulation::DIRTY_ANGULAR_VELOCITY)) { + _entity->setWorldAngularVelocity(glm::vec3(0.0f)); + } + } + _entity->setAcceleration(glm::vec3(0.0f)); +} diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index ddfd7e1e4c..784273d600 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -87,6 +87,7 @@ public: protected: // changes _outgoingPriority only if priority is larger void upgradeOutgoingPriority(uint8_t priority); + void zeroCleanObjectVelocities() const; #ifdef WANT_DEBUG_ENTITY_TREE_LOCKS bool entityTreeIsLocked() const; From 67e816756cab36d3253e99e50ec9a2ee826f2ef7 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Fri, 19 Jan 2018 01:51:32 +0000 Subject: [PATCH 48/58] make gcc happy --- libraries/ui/src/DesktopPreviewProvider.cpp | 4 +++- libraries/ui/src/DesktopPreviewProvider.h | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/ui/src/DesktopPreviewProvider.cpp b/libraries/ui/src/DesktopPreviewProvider.cpp index 4ae70a1eb0..31aa7a22e2 100644 --- a/libraries/ui/src/DesktopPreviewProvider.cpp +++ b/libraries/ui/src/DesktopPreviewProvider.cpp @@ -7,6 +7,8 @@ DesktopPreviewProvider::DesktopPreviewProvider() { } +constexpr const char* DesktopPreviewProvider::imagePaths[]; + QSharedPointer DesktopPreviewProvider::getInstance() { static QSharedPointer instance = DependencyManager::get(); return instance; @@ -41,4 +43,4 @@ void DesktopPreviewProvider::setPreviewDisabledReason(const QString& reasonStrin QImage& DesktopPreviewProvider::loadPreviewImage(QImage& image, const QString& path) const { return image = QImage(path).mirrored().convertToFormat(QImage::Format_RGBA8888); -} \ No newline at end of file +} diff --git a/libraries/ui/src/DesktopPreviewProvider.h b/libraries/ui/src/DesktopPreviewProvider.h index 07d39a6cdf..449c3723e1 100644 --- a/libraries/ui/src/DesktopPreviewProvider.h +++ b/libraries/ui/src/DesktopPreviewProvider.h @@ -16,7 +16,7 @@ class DesktopPreviewProvider : public QObject, public Dependency { DesktopPreviewProvider(); DesktopPreviewProvider(const DesktopPreviewProvider& other) = delete; - constexpr static char* imagePaths[] = { + constexpr static const char* imagePaths[] = { "images/preview-disabled.png", // USER "images/preview-privacy.png", // SECURE_SCREEN "images/preview.png", // VSYNC @@ -44,4 +44,4 @@ private: PreviewDisabledReasons m_previewDisabledReason = { USER }; mutable QImage m_previewDisabled[3]; -}; \ No newline at end of file +}; From e0e6fc711bba14648fd54711cb5e408cf5a67816 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 19 Jan 2018 16:05:09 +1300 Subject: [PATCH 49/58] Replace Pointers API function setNonHoverItems() with setDoesHover() --- .../src/raypick/PointerScriptingInterface.cpp | 10 +-- .../src/raypick/PointerScriptingInterface.h | 16 ++-- libraries/pointers/src/Pointer.cpp | 21 +++-- libraries/pointers/src/Pointer.h | 7 +- libraries/pointers/src/PointerManager.cpp | 14 ++-- libraries/pointers/src/PointerManager.h | 2 +- .../controllers/controllerDispatcher.js | 76 +++++++++---------- .../controllerModules/webSurfaceLaserInput.js | 14 ++-- 8 files changed, 77 insertions(+), 83 deletions(-) diff --git a/interface/src/raypick/PointerScriptingInterface.cpp b/interface/src/raypick/PointerScriptingInterface.cpp index 8e50b1d629..a334834979 100644 --- a/interface/src/raypick/PointerScriptingInterface.cpp +++ b/interface/src/raypick/PointerScriptingInterface.cpp @@ -24,10 +24,6 @@ void PointerScriptingInterface::setIncludeItems(unsigned int uid, const QScriptV DependencyManager::get()->setIncludeItems(uid, qVectorQUuidFromScriptValue(includeItems)); } -void PointerScriptingInterface::setNonHoverItems(unsigned int uid, const QScriptValue& nonHoverItems) const { - DependencyManager::get()->setNonHoverItems(uid, qVectorQUuidFromScriptValue(nonHoverItems)); -} - unsigned int PointerScriptingInterface::createPointer(const PickQuery::PickType& type, const QVariant& properties) { // Interaction with managers should always happen on the main thread if (QThread::currentThread() != qApp->thread()) { @@ -179,4 +175,8 @@ QVariantMap PointerScriptingInterface::getPrevPickResult(unsigned int uid) const result = pickResult->toVariantMap(); } return result; -} \ No newline at end of file +} + +void PointerScriptingInterface::setDoesHover(unsigned int uid, bool hover) const { + DependencyManager::get()->setDoesHover(uid, hover); +} diff --git a/interface/src/raypick/PointerScriptingInterface.h b/interface/src/raypick/PointerScriptingInterface.h index 4791fd802e..451c132769 100644 --- a/interface/src/raypick/PointerScriptingInterface.h +++ b/interface/src/raypick/PointerScriptingInterface.h @@ -191,14 +191,6 @@ public: */ Q_INVOKABLE void setIncludeItems(unsigned int uid, const QScriptValue& includeEntities) const; - /**jsdoc - * Sets a list of Entity IDs, Overlay IDs, and/or Avatar IDs that the pointer should not send hover events to. - * @function Pointers.setNonHoverItems - * @param {number} uid - The ID of the Pointer, as returned by {@link Pointers.createPointer}. - * @param {Uuid[]} nonHoverItems - A list of IDs to that hover events should not be sent to. - */ - Q_INVOKABLE void setNonHoverItems(unsigned int uid, const QScriptValue& nonHoverItems) const; - /**jsdoc * Lock a Pointer onto a specific object (overlay, entity, or avatar). Optionally, provide an offset in object-space, otherwise the Pointer will lock on to the center of the object. * Not used by Stylus Pointers. @@ -210,6 +202,14 @@ public: */ Q_INVOKABLE void setLockEndUUID(unsigned int uid, const QUuid& objectID, bool isOverlay, const glm::mat4& offsetMat = glm::mat4()) const { DependencyManager::get()->setLockEndUUID(uid, objectID, isOverlay, offsetMat); } + /**jsdoc + * Sets whether or not a pointer should generate hover events. + * @function Pointers.setDoesHover + * @param {boolean} uid - The ID of the Pointer, as returned by {@link Pointers.createPointer}. + * @param {boolean} hover - If true then the pointer generates hover events, otherwise it does not. + */ + Q_INVOKABLE void setDoesHover(unsigned int uid, bool hove) const; + /**jsdoc * Check if a Pointer is associated with the left hand. * @function Pointers.isLeftHand diff --git a/libraries/pointers/src/Pointer.cpp b/libraries/pointers/src/Pointer.cpp index ead3c22687..0e542c47da 100644 --- a/libraries/pointers/src/Pointer.cpp +++ b/libraries/pointers/src/Pointer.cpp @@ -52,12 +52,6 @@ void Pointer::setIncludeItems(const QVector& includeItems) const { DependencyManager::get()->setIncludeItems(_pickUID, includeItems); } -void Pointer::setNonHoverItems(const QVector& nonHoverItems) { - withWriteLock([&] { - _nonHoverItems = nonHoverItems; - }); -} - bool Pointer::isLeftHand() const { return DependencyManager::get()->isLeftHand(_pickUID); } @@ -70,6 +64,12 @@ bool Pointer::isMouse() const { return DependencyManager::get()->isMouse(_pickUID); } +void Pointer::setDoesHover(bool doesHover) { + withWriteLock([&] { + _hover = doesHover; + }); +} + void Pointer::update(unsigned int pointerID) { // This only needs to be a read lock because update won't change any of the properties that can be modified from scripts withReadLock([&] { @@ -101,11 +101,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin } // Hover events - bool doHover = shouldHover(pickResult); - - auto pickResultMap = pickResult->toVariantMap(); - auto uuid = QUuid(pickResultMap.value("objectID", "").toString()); - doHover = doHover && !_nonHoverItems.contains(uuid); + bool doHover = _hover && shouldHover(pickResult); Pointer::PickedObject hoveredObject = getHoveredObject(pickResult); PointerEvent hoveredEvent = buildPointerEvent(hoveredObject, pickResult); @@ -240,7 +236,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin } // if we disable the pointer or disable hovering, send hoverEnd events after triggerEnd - if (_hover && ((!_enabled && _prevEnabled) || (!doHover && _prevDoHover))) { + if ((_hover || _prevHover) && ((!_enabled && _prevEnabled) || (!doHover && _prevDoHover))) { if (_prevHoveredObject.type == ENTITY) { emit pointerManager->hoverEndEntity(_prevHoveredObject.objectID, hoveredEvent); } else if (_prevHoveredObject.type == OVERLAY) { @@ -253,6 +249,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin _prevHoveredObject = hoveredObject; _prevButtons = buttons; _prevEnabled = _enabled; + _prevHover = _hover; _prevDoHover = doHover; } diff --git a/libraries/pointers/src/Pointer.h b/libraries/pointers/src/Pointer.h index 6cb366e92a..ef0048cf1e 100644 --- a/libraries/pointers/src/Pointer.h +++ b/libraries/pointers/src/Pointer.h @@ -54,8 +54,6 @@ public: virtual void setIgnoreItems(const QVector& ignoreItems) const; virtual void setIncludeItems(const QVector& includeItems) const; - void setNonHoverItems(const QVector& nonHoverItems); - bool isLeftHand() const; bool isRightHand() const; bool isMouse() const; @@ -64,6 +62,8 @@ public: virtual void setLength(float length) {} virtual void setLockEndUUID(const QUuid& objectID, bool isOverlay, const glm::mat4& offsetMat = glm::mat4()) {} + void setDoesHover(bool hover); + void update(unsigned int pointerID); virtual void updateVisuals(const PickResultPointer& pickResult) = 0; void generatePointerEvents(unsigned int pointerID, const PickResultPointer& pickResult); @@ -99,12 +99,11 @@ private: PickedObject _prevHoveredObject; Buttons _prevButtons; bool _prevEnabled { false }; + bool _prevHover { false }; bool _prevDoHover { false }; std::unordered_map _triggeredObjects; PointerEvent::Button chooseButton(const std::string& button); - - QVector _nonHoverItems; }; #endif // hifi_Pick_h diff --git a/libraries/pointers/src/PointerManager.cpp b/libraries/pointers/src/PointerManager.cpp index f2953ce8c8..13b38457b6 100644 --- a/libraries/pointers/src/PointerManager.cpp +++ b/libraries/pointers/src/PointerManager.cpp @@ -108,13 +108,6 @@ void PointerManager::setIncludeItems(unsigned int uid, const QVector& inc } } -void PointerManager::setNonHoverItems(unsigned int uid, const QVector& nonHoverItems) const { - auto pointer = find(uid); - if (pointer) { - pointer->setNonHoverItems(nonHoverItems); - } -} - void PointerManager::setLength(unsigned int uid, float length) const { auto pointer = find(uid); if (pointer) { @@ -129,6 +122,13 @@ void PointerManager::setLockEndUUID(unsigned int uid, const QUuid& objectID, boo } } +void PointerManager::setDoesHover(unsigned int uid, bool hover) const { + auto pointer = find(uid); + if (pointer) { + pointer->setDoesHover(hover); + } +} + bool PointerManager::isLeftHand(unsigned int uid) { auto pointer = find(uid); if (pointer) { diff --git a/libraries/pointers/src/PointerManager.h b/libraries/pointers/src/PointerManager.h index a2a1e9dcd2..2c9a37e129 100644 --- a/libraries/pointers/src/PointerManager.h +++ b/libraries/pointers/src/PointerManager.h @@ -34,10 +34,10 @@ public: void setPrecisionPicking(unsigned int uid, bool precisionPicking) const; void setIgnoreItems(unsigned int uid, const QVector& ignoreEntities) const; void setIncludeItems(unsigned int uid, const QVector& includeEntities) const; - void setNonHoverItems(unsigned int uid, const QVector& nonHoverItems) const; void setLength(unsigned int uid, float length) const; void setLockEndUUID(unsigned int uid, const QUuid& objectID, bool isOverlay, const glm::mat4& offsetMat = glm::mat4()) const; + void setDoesHover(unsigned int uid, bool hover) const; void update(); diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 15b025a7ea..fe18ed25f8 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -46,10 +46,10 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.tabletID = null; this.TABLET_UI_UUIDS = []; this.blacklist = []; - this.leftPointerNonHoverItem = null; - this.leftPointerNonHoverItemChanged = false; - this.rightPointerNonHoverItem = null; - this.rightPointerNonHoverItemChanged = false; + this.leftPointerDoesHover = true; + this.leftPointerDoesHoverChanged = false; + this.rightPointerDoesHover = true; + this.rightPointerDoesHoverChanged = false; this.pointerManager = new PointerManager(); // a module can occupy one or more "activity" slots while it's running. If all the required slots for a module are @@ -163,26 +163,14 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } }; - this.setNonHoverItems = function () { - if (_this.leftPointerNonHoverItemChanged) { - if (_this.leftPointerNonHoverItem === null) { - Pointers.setNonHoverItems(_this.leftPointer, []); - } else if (_this.isTabletID(_this.leftPointerNonHoverItem)) { - Pointers.setNonHoverItems(_this.leftPointer, _this.TABLET_UI_UUIDS); - } else { - Pointers.setNonHoverItems(_this.leftPointer, [_this.leftPointerNonHoverItem]); - } - _this.leftPointerNonHoverItemChanged = false; + this.updateHovering = function () { + if (_this.leftPointerDoesHoverChanged) { + Pointers.setDoesHover(_this.leftPointer, _this.leftPointerDoesHover); + _this.leftPointerDoesHoverChanged = false; } - if (_this.rightPointerNonHoverItemChanged) { - if (_this.rightPointerNonHoverItem === null) { - Pointers.setNonHoverItems(_this.rightPointer, []); - } else if (_this.isTabletID(_this.rightPointerNonHoverItem)) { - Pointers.setNonHoverItems(_this.rightPointer, _this.TABLET_UI_UUIDS); - } else { - Pointers.setNonHoverItems(_this.rightPointer, [_this.rightPointerNonHoverItem]); - } - _this.rightPointerNonHoverItemChanged = false; + if (_this.rightPointerDoesHoverChanged) { + Pointers.setDoesHover(_this.rightPointer, _this.rightPointerDoesHover); + _this.rightPointerDoesHoverChanged = false; } }; @@ -358,15 +346,15 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); _this.markSlots(candidatePlugin, orderedPluginName); _this.pointerManager.makePointerVisible(candidatePlugin.parameters.handLaser); - if (candidatePlugin.parameters.handLaser.nonHoverItem !== undefined) { + if (candidatePlugin.parameters.handLaser.doesHover !== undefined) { if (candidatePlugin.parameters.handLaser.hand === LEFT_HAND - && _this.leftPointerNonHoverItem !== candidatePlugin.parameters.handLaser.nonHoverItem) { - _this.leftPointerNonHoverItem = candidatePlugin.parameters.handLaser.nonHoverItem; - _this.leftPointerNonHoverItemChanged = true; + && _this.leftPointerDoesHover !== candidatePlugin.parameters.handLaser.doesHover) { + _this.leftPointerDoesHover = candidatePlugin.parameters.handLaser.doesHover; + _this.leftPointerDoesHoverChanged = true; } else if (candidatePlugin.parameters.handLaser.hand === RIGHT_HAND - && _this.rightPointerNonHoverItem !== candidatePlugin.parameters.handLaser.nonHoverItem) { - _this.rightPointerNonHoverItem = candidatePlugin.parameters.handLaser.nonHoverItem; - _this.rightPointerNonHoverItemChanged = true; + && _this.rightPointerDoesHover !== candidatePlugin.parameters.handLaser.doesHover) { + _this.rightPointerDoesHover = candidatePlugin.parameters.handLaser.doesHover; + _this.rightPointerDoesHoverChanged = true; } } @@ -402,15 +390,15 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); var runningness = plugin.run(controllerData, deltaTime); if (runningness.active) { - if (plugin.parameters.handLaser.nonHoverItem !== undefined) { + if (plugin.parameters.handLaser.doesHover !== undefined) { if (plugin.parameters.handLaser.hand === LEFT_HAND - && _this.leftPointerNonHoverItem !== plugin.parameters.handLaser.nonHoverItem) { - _this.leftPointerNonHoverItem = plugin.parameters.handLaser.nonHoverItem; - _this.leftPointerNonHoverItemChanged = true; + && _this.leftPointerDoesHover !== plugin.parameters.handLaser.doesHover) { + _this.leftPointerDoesHover = plugin.parameters.handLaser.doesHover; + _this.leftPointerDoesHoverChanged = true; } else if (plugin.parameters.handLaser.hand === RIGHT_HAND - && _this.rightPointerNonHoverItem !== plugin.parameters.handLaser.nonHoverItem) { - _this.rightPointerNonHoverItem = plugin.parameters.handLaser.nonHoverItem; - _this.rightPointerNonHoverItemChanged = true; + && _this.rightPointerDoesHover !== plugin.parameters.handLaser.doesHover) { + _this.rightPointerDoesHover = plugin.parameters.handLaser.doesHover; + _this.rightPointerDoesHoverChanged = true; } } } @@ -421,6 +409,17 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); delete _this.runningPluginNames[runningPluginName]; _this.markSlots(plugin, false); _this.pointerManager.makePointerInvisible(plugin.parameters.handLaser); + + if (plugin.parameters.handLaser.doesHover !== undefined) { + if (plugin.parameters.handLaser.hand === LEFT_HAND && !_this.leftPointerDoesHover) { + _this.leftPointerDoesHover = true; + _this.leftPointerDoesHoverChanged = true; + } else if (plugin.parameters.handLaser.hand === RIGHT_HAND && !_this.rightPointerDoesHover) { + _this.rightPointerDoesHover = true; + _this.rightPointerDoesHoverChanged = true; + } + } + if (DEBUG) { print("controllerDispatcher stopping " + runningPluginName); } @@ -433,8 +432,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } } _this.pointerManager.updatePointersRenderState(controllerData.triggerClicks, controllerData.triggerValues); - - _this.setNonHoverItems(); + _this.updateHovering(); if (PROFILE) { Script.endProfileRange("dispatch.run"); diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js index 5b9afa3a2a..282dd36d17 100644 --- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js +++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -90,7 +90,7 @@ Script.include("/~/system/libraries/controllers.js"); this.hoverItem = null; this.isTabletID = function (uuid) { - return [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID, HMD.homeButtonHightlightID].indexOf(uuid) !== -1; + return [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID, HMD.homeButtonHighlightID].indexOf(uuid) !== -1; }; this.isReady = function(controllerData) { @@ -105,19 +105,19 @@ Script.include("/~/system/libraries/controllers.js"); } if (pointingAt !== this.getOtherModule().hoverItem) { + this.parameters.handLaser.doesHover = true; this.hoverItem = pointingAt; - this.getOtherModule().parameters.handLaser.nonHoverItem = pointingAt; } else { + this.parameters.handLaser.doesHover = false; this.hoverItem = null; - this.getOtherModule().parameters.handLaser.nonHoverItem = null; } return makeRunningValues(true, [], []); } } + this.parameters.handLaser.doesHover = false; this.hoverItem = null; - this.getOtherModule().parameters.handLaser.nonHoverItem = null; return makeRunningValues(false, [], []); }; @@ -135,11 +135,11 @@ Script.include("/~/system/libraries/controllers.js"); } if (pointingAt !== this.getOtherModule().hoverItem || isTriggerPressed) { + this.parameters.handLaser.doesHover = true; this.hoverItem = pointingAt; - this.getOtherModule().parameters.handLaser.nonHoverItem = pointingAt; } else { + this.parameters.handLaser.doesHover = false; this.hoverItem = null; - this.getOtherModule().parameters.handLaser.nonHoverItem = null; } return makeRunningValues(true, [], []); @@ -147,8 +147,8 @@ Script.include("/~/system/libraries/controllers.js"); this.deleteContextOverlay(); this.running = false; + this.parameters.handLaser.doesHover = false; this.hoverItem = null; - this.getOtherModule().parameters.handLaser.nonHoverItem = null; return makeRunningValues(false, [], []); }; From 45bc6b8dabf371d25b6841901be446dae7d6c57f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 19 Jan 2018 16:41:23 +1300 Subject: [PATCH 50/58] Dominant hand gets the highlight if both start pointing simultaneously --- .../controllerModules/webSurfaceLaserInput.js | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js index 282dd36d17..cc8378af84 100644 --- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js +++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -87,6 +87,23 @@ Script.include("/~/system/libraries/controllers.js"); return MyAvatar.getDominantHand() === "right" ? 1 : 0; }; + this.letOtherHandRunFirst = function (controllerData, pointingAt) { + // If both hands are ready to run, let the other hand run first if it is the dominant hand so that it gets the + // highlight. + var isOtherTriggerPressed = controllerData.triggerValues[this.otherHand] > TRIGGER_OFF_VALUE; + var isLetOtherHandRunFirst = !this.getOtherModule().running + && this.getDominantHand() === this.otherHand + && (this.parameters.handLaser.allwaysOn || isOtherTriggerPressed); + if (isLetOtherHandRunFirst) { + var otherHandPointingAt = controllerData.rayPicks[this.otherHand].objectID; + if (this.isTabletID(otherHandPointingAt)) { + otherHandPointingAt = HMD.tabletID; + } + isLetOtherHandRunFirst = pointingAt === otherHandPointingAt; + } + return isLetOtherHandRunFirst; + }; + this.hoverItem = null; this.isTabletID = function (uuid) { @@ -94,25 +111,28 @@ Script.include("/~/system/libraries/controllers.js"); }; this.isReady = function(controllerData) { - var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE - && controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE; if (this.isPointingAtOverlay(controllerData) || this.isPointingAtWebEntity(controllerData)) { this.updateAllwaysOn(); + + var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE; if (this.parameters.handLaser.allwaysOn || isTriggerPressed) { var pointingAt = controllerData.rayPicks[this.hand].objectID; if (this.isTabletID(pointingAt)) { pointingAt = HMD.tabletID; } - if (pointingAt !== this.getOtherModule().hoverItem) { - this.parameters.handLaser.doesHover = true; - this.hoverItem = pointingAt; - } else { - this.parameters.handLaser.doesHover = false; - this.hoverItem = null; - } + if (!this.letOtherHandRunFirst(controllerData, pointingAt)) { - return makeRunningValues(true, [], []); + if (pointingAt !== this.getOtherModule().hoverItem) { + this.parameters.handLaser.doesHover = true; + this.hoverItem = pointingAt; + } else { + this.parameters.handLaser.doesHover = false; + this.hoverItem = null; + } + + return makeRunningValues(true, [], []); + } } } From 12c48a38f75a6be2db0a545bb4ae057438016edb Mon Sep 17 00:00:00 2001 From: Olivier Prat Date: Fri, 19 Jan 2018 13:58:35 +0100 Subject: [PATCH 51/58] Normalized diffuse & specular of directional, point and spot lights such as a light intensity of 1 gives a perpendicular diffuse lighting of the same color as the albedo for dielectric materials. --- libraries/render-utils/src/LightingModel.slh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index 7d08fdabaf..be8330f198 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -186,7 +186,7 @@ float specularDistribution(SurfaceData surface) { // Add geometric factors G1(n,l) and G1(n,v) float smithInvG1NdotL = evalSmithInvG1(surface.roughness4, surface.ndotl); denom *= surface.smithInvG1NdotV * smithInvG1NdotL; - // Don't divide by PI as it will be done later + // Don't divide by PI as this is part of the light normalization factor float power = surface.roughness4 / denom; return power; } @@ -202,12 +202,11 @@ vec4 evalPBRShading(float metallic, vec3 fresnel, SurfaceData surface) { vec3 specular = fresnelColor * power * angleAttenuation; float diffuse = (1.0 - metallic) * angleAttenuation * (1.0 - fresnelColor.x); - diffuse /= 3.1415926; - // Diffuse is divided by PI but specular isn't because an infinitesimal volume light source - // has a multiplier of PI, says Naty Hoffman. + // We don't divided by PI, as the "normalized" equations state we should, because we decide, as Naty Hoffman, that + // we wish to have a similar color as raw albedo on a perfectly diffuse surface perpendicularly lit + // by a white light of intensity 1. But this is an arbitrary normalization of what light intensity "means". // (see http://blog.selfshadow.com/publications/s2013-shading-course/hoffman/s2013_pbs_physics_math_notes.pdf // page 23 paragraph "Punctual light sources") - return vec4(specular, diffuse); } @@ -222,9 +221,9 @@ vec4 evalPBRShadingDielectric(SurfaceData surface, float fresnel) { vec3 specular = vec3(fresnelScalar) * power * angleAttenuation; float diffuse = angleAttenuation * (1.0 - fresnelScalar); - diffuse /= 3.1415926; - // Diffuse is divided by PI but specular isn't because an infinitesimal volume light source - // has a multiplier of PI, says Naty Hoffman. + // We don't divided by PI, as the "normalized" equations state we should, because we decide, as Naty Hoffman, that + // we wish to have a similar color as raw albedo on a perfectly diffuse surface perpendicularly lit + // by a white light of intensity 1. But this is an arbitrary normalization of what light intensity "means". // (see http://blog.selfshadow.com/publications/s2013-shading-course/hoffman/s2013_pbs_physics_math_notes.pdf // page 23 paragraph "Punctual light sources") return vec4(specular, diffuse); @@ -239,8 +238,9 @@ vec4 evalPBRShadingMetallic(SurfaceData surface, vec3 fresnel) { float power = specularDistribution(surface); vec3 specular = fresnelColor * power * angleAttenuation; - // Specular isn't divided by PI because an infinitesimal volume light source - // has a multiplier of PI, says Naty Hoffman. + // We don't divided by PI, as the "normalized" equations state we should, because we decide, as Naty Hoffman, that + // we wish to have a similar color as raw albedo on a perfectly diffuse surface perpendicularly lit + // by a white light of intensity 1. But this is an arbitrary normalization of what light intensity "means". // (see http://blog.selfshadow.com/publications/s2013-shading-course/hoffman/s2013_pbs_physics_math_notes.pdf // page 23 paragraph "Punctual light sources") return vec4(specular, 0.f); From 27ea74f5bf6af58298626fa0ab34b382f2acd127 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 19 Jan 2018 11:22:37 -0700 Subject: [PATCH 52/58] Fix sortOrder param on tablet buttons --- .../ui/src/ui/TabletScriptingInterface.cpp | 31 +++++++++++++++++-- .../ui/src/ui/TabletScriptingInterface.h | 1 + .../developer/tests/dynamics/dynamicsTests.js | 3 +- .../utilities/render/debugHighlight.js | 3 +- scripts/developer/utilities/render/lod.js | 3 +- scripts/developer/utilities/render/luci.js | 3 +- scripts/system/chat.js | 3 +- scripts/system/generalSettings.js | 3 +- scripts/system/goto.js | 3 +- scripts/system/run.js | 3 +- scripts/system/tablet-users.js | 3 +- .../marketplace/clap/clapApp.js | 3 +- .../skyboxChanger/skyboxchanger.js | 3 +- 13 files changed, 40 insertions(+), 25 deletions(-) diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index c69ec1ce84..33ff9b06f7 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -31,6 +31,8 @@ const QString SYSTEM_TOOLBAR = "com.highfidelity.interface.toolbar.system"; const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; const QString TabletScriptingInterface::QML = "hifi/tablet/TabletRoot.qml"; +const QString BUTTON_SORT_ORDER_KEY = "sortOrder"; +const int DEFAULT_BUTTON_SORT_ORDER = 100; static QString getUsername() { QString username = "Unknown user"; @@ -74,11 +76,20 @@ QVariant TabletButtonListModel::data(const QModelIndex& index, int role) const { } TabletButtonProxy* TabletButtonListModel::addButton(const QVariant& properties) { - auto tabletButtonProxy = QSharedPointer(new TabletButtonProxy(properties.toMap())); + QVariantMap newTabletButtonProperties = properties.toMap(); + if (newTabletButtonProperties.find(BUTTON_SORT_ORDER_KEY) == newTabletButtonProperties.end()) { + newTabletButtonProperties[BUTTON_SORT_ORDER_KEY] = DEFAULT_BUTTON_SORT_ORDER; + } + int insertButtonUsingIndex = computeNewButtonIndex(newTabletButtonProperties); + auto newTabletButtonProxy = QSharedPointer(new TabletButtonProxy(newTabletButtonProperties)); beginResetModel(); - _buttons.push_back(tabletButtonProxy); + if (insertButtonUsingIndex < _buttons.size()) { + _buttons.insert(_buttons.begin() + insertButtonUsingIndex, newTabletButtonProxy); + } else { + _buttons.push_back(newTabletButtonProxy); + } endResetModel(); - return tabletButtonProxy.data(); + return newTabletButtonProxy.data(); } void TabletButtonListModel::removeButton(TabletButtonProxy* button) { @@ -92,6 +103,20 @@ void TabletButtonListModel::removeButton(TabletButtonProxy* button) { endResetModel(); } +int TabletButtonListModel::computeNewButtonIndex(const QVariantMap& newButtonProperties) { + int buttonCount = (int)_buttons.size(); + int newButtonSortOrder = newButtonProperties[BUTTON_SORT_ORDER_KEY].toInt(); + if (newButtonSortOrder == DEFAULT_BUTTON_SORT_ORDER) return buttonCount; + for (int i = 0; i < buttonCount; i++) { + QVariantMap tabletButtonProperties = _buttons[i]->getProperties(); + int tabletButtonSortOrder = tabletButtonProperties[BUTTON_SORT_ORDER_KEY].toInt(); + if (newButtonSortOrder <= tabletButtonSortOrder) { + return i; + } + } + return buttonCount; +} + TabletButtonsProxyModel::TabletButtonsProxyModel(QObject *parent) : QSortFilterProxyModel(parent) { } diff --git a/libraries/ui/src/ui/TabletScriptingInterface.h b/libraries/ui/src/ui/TabletScriptingInterface.h index 56e3ae257b..34827117f0 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.h +++ b/libraries/ui/src/ui/TabletScriptingInterface.h @@ -115,6 +115,7 @@ protected: friend class TabletProxy; TabletButtonProxy* addButton(const QVariant& properties); void removeButton(TabletButtonProxy* button); + int computeNewButtonIndex(const QVariantMap& newButtonProperties); using List = std::list>; static QHash _roles; static Qt::ItemFlags _flags; diff --git a/scripts/developer/tests/dynamics/dynamicsTests.js b/scripts/developer/tests/dynamics/dynamicsTests.js index c0b001eab3..e9262c9308 100644 --- a/scripts/developer/tests/dynamics/dynamicsTests.js +++ b/scripts/developer/tests/dynamics/dynamicsTests.js @@ -22,8 +22,7 @@ var button = tablet.addButton({ icon: Script.resolvePath("dynamicsTests.svg"), - text: "Dynamics", - sortOrder: 15 + text: "Dynamics" }); diff --git a/scripts/developer/utilities/render/debugHighlight.js b/scripts/developer/utilities/render/debugHighlight.js index e70565cec2..c2173f6e2a 100644 --- a/scripts/developer/utilities/render/debugHighlight.js +++ b/scripts/developer/utilities/render/debugHighlight.js @@ -32,8 +32,7 @@ var button = tablet.addButton({ text: TABLET_BUTTON_NAME, icon: ICON_URL, - activeIcon: ACTIVE_ICON_URL, - sortOrder: 1 + activeIcon: ACTIVE_ICON_URL }); var hasEventBridge = false; diff --git a/scripts/developer/utilities/render/lod.js b/scripts/developer/utilities/render/lod.js index dc0b99edc2..307e509d39 100644 --- a/scripts/developer/utilities/render/lod.js +++ b/scripts/developer/utilities/render/lod.js @@ -30,8 +30,7 @@ var button = tablet.addButton({ text: TABLET_BUTTON_NAME, icon: ICON_URL, - activeIcon: ACTIVE_ICON_URL, - sortOrder: 1 + activeIcon: ACTIVE_ICON_URL }); var hasEventBridge = false; diff --git a/scripts/developer/utilities/render/luci.js b/scripts/developer/utilities/render/luci.js index 1e2ac1261f..6482c884ff 100644 --- a/scripts/developer/utilities/render/luci.js +++ b/scripts/developer/utilities/render/luci.js @@ -31,8 +31,7 @@ var button = tablet.addButton({ text: TABLET_BUTTON_NAME, icon: ICON_URL, - activeIcon: ACTIVE_ICON_URL, - sortOrder: 1 + activeIcon: ACTIVE_ICON_URL }); var hasEventBridge = false; diff --git a/scripts/system/chat.js b/scripts/system/chat.js index 0cb414e23c..b0a2e114a3 100644 --- a/scripts/system/chat.js +++ b/scripts/system/chat.js @@ -945,8 +945,7 @@ tabletButton = tablet.addButton({ icon: tabletButtonIcon, activeIcon: tabletButtonActiveIcon, - text: tabletButtonName, - sortOrder: 0 + text: tabletButtonName }); Messages.subscribe(channelName); diff --git a/scripts/system/generalSettings.js b/scripts/system/generalSettings.js index 082528ffc5..d3848da7d0 100644 --- a/scripts/system/generalSettings.js +++ b/scripts/system/generalSettings.js @@ -37,8 +37,7 @@ tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ icon: "icons/tablet-icons/goto-i.svg", - text: buttonName, - sortOrder: 8 + text: buttonName }); } diff --git a/scripts/system/goto.js b/scripts/system/goto.js index d364bf579e..5cc5bad844 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -41,8 +41,7 @@ if (Settings.getValue("HUDUIEnabled")) { button = tablet.addButton({ icon: "icons/tablet-icons/goto-i.svg", activeIcon: "icons/tablet-icons/goto-a.svg", - text: buttonName, - sortOrder: 8 + text: buttonName }); } diff --git a/scripts/system/run.js b/scripts/system/run.js index 054cca6d9c..c34271b18d 100644 --- a/scripts/system/run.js +++ b/scripts/system/run.js @@ -10,8 +10,7 @@ var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ icon: Script.resolvePath("assets/images/run.svg"), - text: "RUN", - sortOrder: 15 + text: "RUN" }); function onClicked() { diff --git a/scripts/system/tablet-users.js b/scripts/system/tablet-users.js index 6f37cd55eb..6181173818 100644 --- a/scripts/system/tablet-users.js +++ b/scripts/system/tablet-users.js @@ -37,8 +37,7 @@ var button = tablet.addButton({ icon: "icons/tablet-icons/users-i.svg", activeIcon: "icons/tablet-icons/users-a.svg", - text: "USERS", - sortOrder: 11 + text: "USERS" }); var onUsersScreen = false; diff --git a/unpublishedScripts/marketplace/clap/clapApp.js b/unpublishedScripts/marketplace/clap/clapApp.js index b2d8ce55db..7118b3623c 100644 --- a/unpublishedScripts/marketplace/clap/clapApp.js +++ b/unpublishedScripts/marketplace/clap/clapApp.js @@ -31,8 +31,7 @@ var activeButton = tablet.addButton({ icon: whiteIcon, activeIcon: blackIcon, text: APP_NAME, - isActive: isActive, - sortOrder: 11 + isActive: isActive }); if (isActive) { diff --git a/unpublishedScripts/marketplace/skyboxChanger/skyboxchanger.js b/unpublishedScripts/marketplace/skyboxChanger/skyboxchanger.js index 7bc65722cd..0bd23552e4 100644 --- a/unpublishedScripts/marketplace/skyboxChanger/skyboxchanger.js +++ b/unpublishedScripts/marketplace/skyboxChanger/skyboxchanger.js @@ -32,8 +32,7 @@ var button = tablet.addButton({ icon: ICONS.icon, activeIcon: ICONS.activeIcon, - text: TABLET_BUTTON_NAME, - sortOrder: 1 + text: TABLET_BUTTON_NAME }); var hasEventBridge = false; From 94e6a68d4197e84b80a0de1b9396d6a7b7bf749e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 20 Jan 2018 09:50:24 +1300 Subject: [PATCH 53/58] Code review --- libraries/pointers/src/Pointer.cpp | 4 +- libraries/pointers/src/Pointer.h | 2 +- .../controllers/controllerDispatcher.js | 55 ++++++------------- 3 files changed, 19 insertions(+), 42 deletions(-) diff --git a/libraries/pointers/src/Pointer.cpp b/libraries/pointers/src/Pointer.cpp index 0e542c47da..8691ab8823 100644 --- a/libraries/pointers/src/Pointer.cpp +++ b/libraries/pointers/src/Pointer.cpp @@ -111,7 +111,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin hoveredEvent.setMoveOnHoverLeave(moveOnHoverLeave); // if shouldHover && !_prevDoHover, only send hoverBegin - if (_enabled && _hover && doHover && !_prevDoHover) { + if (_enabled && doHover && !_prevDoHover) { if (hoveredObject.type == ENTITY) { emit pointerManager->hoverBeginEntity(hoveredObject.objectID, hoveredEvent); } else if (hoveredObject.type == OVERLAY) { @@ -119,7 +119,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin } else if (hoveredObject.type == HUD) { emit pointerManager->hoverBeginHUD(hoveredEvent); } - } else if (_enabled && _hover && doHover) { + } else if (_enabled && doHover) { if (hoveredObject.type == OVERLAY) { if (_prevHoveredObject.type == OVERLAY) { if (hoveredObject.objectID == _prevHoveredObject.objectID) { diff --git a/libraries/pointers/src/Pointer.h b/libraries/pointers/src/Pointer.h index ef0048cf1e..62683cb6e7 100644 --- a/libraries/pointers/src/Pointer.h +++ b/libraries/pointers/src/Pointer.h @@ -62,7 +62,7 @@ public: virtual void setLength(float length) {} virtual void setLockEndUUID(const QUuid& objectID, bool isOverlay, const glm::mat4& offsetMat = glm::mat4()) {} - void setDoesHover(bool hover); + virtual void setDoesHover(bool hover); void update(unsigned int pointerID); virtual void updateVisuals(const PickResultPointer& pickResult) = 0; diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index fe18ed25f8..4946a6525e 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -163,6 +163,20 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } }; + this.updateDoesHover = function(handLaser) { + if (handLaser.doesHover !== undefined) { + if (handLaser.hand === LEFT_HAND + && _this.leftPointerDoesHover !== handLaser.doesHover) { + _this.leftPointerDoesHover = handLaser.doesHover; + _this.leftPointerDoesHoverChanged = true; + } else if (handLaser.hand === RIGHT_HAND + && _this.rightPointerDoesHover !== handLaser.doesHover) { + _this.rightPointerDoesHover = handLaser.doesHover; + _this.rightPointerDoesHoverChanged = true; + } + } + } + this.updateHovering = function () { if (_this.leftPointerDoesHoverChanged) { Pointers.setDoesHover(_this.leftPointer, _this.leftPointerDoesHover); @@ -345,19 +359,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); _this.runningPluginNames[orderedPluginName] = true; _this.markSlots(candidatePlugin, orderedPluginName); _this.pointerManager.makePointerVisible(candidatePlugin.parameters.handLaser); - - if (candidatePlugin.parameters.handLaser.doesHover !== undefined) { - if (candidatePlugin.parameters.handLaser.hand === LEFT_HAND - && _this.leftPointerDoesHover !== candidatePlugin.parameters.handLaser.doesHover) { - _this.leftPointerDoesHover = candidatePlugin.parameters.handLaser.doesHover; - _this.leftPointerDoesHoverChanged = true; - } else if (candidatePlugin.parameters.handLaser.hand === RIGHT_HAND - && _this.rightPointerDoesHover !== candidatePlugin.parameters.handLaser.doesHover) { - _this.rightPointerDoesHover = candidatePlugin.parameters.handLaser.doesHover; - _this.rightPointerDoesHoverChanged = true; - } - } - + _this.updateDoesHover(candidatePlugin.parameters.handLaser); if (DEBUG) { print("controllerDispatcher running " + orderedPluginName); } @@ -387,39 +389,14 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); if (PROFILE) { Script.beginProfileRange("dispatch.run." + runningPluginName); } + _this.updateDoesHover(plugin.parameters.handLaser); var runningness = plugin.run(controllerData, deltaTime); - - if (runningness.active) { - if (plugin.parameters.handLaser.doesHover !== undefined) { - if (plugin.parameters.handLaser.hand === LEFT_HAND - && _this.leftPointerDoesHover !== plugin.parameters.handLaser.doesHover) { - _this.leftPointerDoesHover = plugin.parameters.handLaser.doesHover; - _this.leftPointerDoesHoverChanged = true; - } else if (plugin.parameters.handLaser.hand === RIGHT_HAND - && _this.rightPointerDoesHover !== plugin.parameters.handLaser.doesHover) { - _this.rightPointerDoesHover = plugin.parameters.handLaser.doesHover; - _this.rightPointerDoesHoverChanged = true; - } - } - } - if (!runningness.active) { // plugin is finished running, for now. remove it from the list // of running plugins and mark its activity-slots as "not in use" delete _this.runningPluginNames[runningPluginName]; _this.markSlots(plugin, false); _this.pointerManager.makePointerInvisible(plugin.parameters.handLaser); - - if (plugin.parameters.handLaser.doesHover !== undefined) { - if (plugin.parameters.handLaser.hand === LEFT_HAND && !_this.leftPointerDoesHover) { - _this.leftPointerDoesHover = true; - _this.leftPointerDoesHoverChanged = true; - } else if (plugin.parameters.handLaser.hand === RIGHT_HAND && !_this.rightPointerDoesHover) { - _this.rightPointerDoesHover = true; - _this.rightPointerDoesHoverChanged = true; - } - } - if (DEBUG) { print("controllerDispatcher stopping " + runningPluginName); } From 24dcc8788cd671e99ff8ecdd05f2c4eb087d6ae8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 20 Jan 2018 10:07:23 +1300 Subject: [PATCH 54/58] Code review --- libraries/pointers/src/Pointer.cpp | 3 +-- libraries/pointers/src/Pointer.h | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/pointers/src/Pointer.cpp b/libraries/pointers/src/Pointer.cpp index 8691ab8823..287d5a3c97 100644 --- a/libraries/pointers/src/Pointer.cpp +++ b/libraries/pointers/src/Pointer.cpp @@ -236,7 +236,7 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin } // if we disable the pointer or disable hovering, send hoverEnd events after triggerEnd - if ((_hover || _prevHover) && ((!_enabled && _prevEnabled) || (!doHover && _prevDoHover))) { + if ((!_enabled && _prevEnabled) || (!doHover && _prevDoHover)) { if (_prevHoveredObject.type == ENTITY) { emit pointerManager->hoverEndEntity(_prevHoveredObject.objectID, hoveredEvent); } else if (_prevHoveredObject.type == OVERLAY) { @@ -249,7 +249,6 @@ void Pointer::generatePointerEvents(unsigned int pointerID, const PickResultPoin _prevHoveredObject = hoveredObject; _prevButtons = buttons; _prevEnabled = _enabled; - _prevHover = _hover; _prevDoHover = doHover; } diff --git a/libraries/pointers/src/Pointer.h b/libraries/pointers/src/Pointer.h index 62683cb6e7..9fd434fb15 100644 --- a/libraries/pointers/src/Pointer.h +++ b/libraries/pointers/src/Pointer.h @@ -99,7 +99,6 @@ private: PickedObject _prevHoveredObject; Buttons _prevButtons; bool _prevEnabled { false }; - bool _prevHover { false }; bool _prevDoHover { false }; std::unordered_map _triggeredObjects; From 7898aa2c4b0f6ef879b02ac285dc5afe3d0cecf8 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 20 Jan 2018 10:32:42 +1300 Subject: [PATCH 55/58] Code review --- .../controllers/controllerDispatcher.js | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js index 4946a6525e..a8658933e7 100644 --- a/scripts/system/controllers/controllerDispatcher.js +++ b/scripts/system/controllers/controllerDispatcher.js @@ -163,15 +163,13 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); } }; - this.updateDoesHover = function(handLaser) { + this.updateDoesHover = function(handLaser, doesHover) { if (handLaser.doesHover !== undefined) { - if (handLaser.hand === LEFT_HAND - && _this.leftPointerDoesHover !== handLaser.doesHover) { - _this.leftPointerDoesHover = handLaser.doesHover; + if (handLaser.hand === LEFT_HAND && _this.leftPointerDoesHover !== doesHover) { + _this.leftPointerDoesHover = doesHover; _this.leftPointerDoesHoverChanged = true; - } else if (handLaser.hand === RIGHT_HAND - && _this.rightPointerDoesHover !== handLaser.doesHover) { - _this.rightPointerDoesHover = handLaser.doesHover; + } else if (handLaser.hand === RIGHT_HAND && _this.rightPointerDoesHover !== doesHover) { + _this.rightPointerDoesHover = doesHover; _this.rightPointerDoesHoverChanged = true; } } @@ -359,7 +357,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); _this.runningPluginNames[orderedPluginName] = true; _this.markSlots(candidatePlugin, orderedPluginName); _this.pointerManager.makePointerVisible(candidatePlugin.parameters.handLaser); - _this.updateDoesHover(candidatePlugin.parameters.handLaser); + _this.updateDoesHover(candidatePlugin.parameters.handLaser, + candidatePlugin.parameters.handLaser.doesHover); if (DEBUG) { print("controllerDispatcher running " + orderedPluginName); } @@ -389,14 +388,16 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); if (PROFILE) { Script.beginProfileRange("dispatch.run." + runningPluginName); } - _this.updateDoesHover(plugin.parameters.handLaser); var runningness = plugin.run(controllerData, deltaTime); - if (!runningness.active) { + if (runningness.active) { + _this.updateDoesHover(plugin.parameters.handLaser, plugin.parameters.handLaser.doesHover); + } else { // plugin is finished running, for now. remove it from the list // of running plugins and mark its activity-slots as "not in use" delete _this.runningPluginNames[runningPluginName]; _this.markSlots(plugin, false); _this.pointerManager.makePointerInvisible(plugin.parameters.handLaser); + _this.updateDoesHover(plugin.parameters.handLaser, true); if (DEBUG) { print("controllerDispatcher stopping " + runningPluginName); } From 41f3b792b62af366b47c35049f8b72f531c4fdb0 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 19 Jan 2018 14:34:52 -0700 Subject: [PATCH 56/58] Fix warnings --- libraries/ui/src/ui/TabletScriptingInterface.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 33ff9b06f7..2b89045246 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -83,7 +83,8 @@ TabletButtonProxy* TabletButtonListModel::addButton(const QVariant& properties) int insertButtonUsingIndex = computeNewButtonIndex(newTabletButtonProperties); auto newTabletButtonProxy = QSharedPointer(new TabletButtonProxy(newTabletButtonProperties)); beginResetModel(); - if (insertButtonUsingIndex < _buttons.size()) { + int buttonCount = (int)_buttons.size(); + if (insertButtonUsingIndex < buttonCount) { _buttons.insert(_buttons.begin() + insertButtonUsingIndex, newTabletButtonProxy); } else { _buttons.push_back(newTabletButtonProxy); From f6062ebfccb5a3653844fd69d0b8afdf820524cb Mon Sep 17 00:00:00 2001 From: ZappoMan Date: Fri, 19 Jan 2018 16:00:08 -0800 Subject: [PATCH 57/58] new icon and button layout --- .../icons/tablet-icons/EmoteAppIcon.svg | 21 ++++++++++++++++++ scripts/system/emote.js | 2 +- scripts/system/html/EmoteApp.html | 22 +++++++++---------- 3 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 interface/resources/icons/tablet-icons/EmoteAppIcon.svg diff --git a/interface/resources/icons/tablet-icons/EmoteAppIcon.svg b/interface/resources/icons/tablet-icons/EmoteAppIcon.svg new file mode 100644 index 0000000000..340f0fcd2f --- /dev/null +++ b/interface/resources/icons/tablet-icons/EmoteAppIcon.svg @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/scripts/system/emote.js b/scripts/system/emote.js index f1f739c126..139870fd63 100644 --- a/scripts/system/emote.js +++ b/scripts/system/emote.js @@ -43,7 +43,7 @@ var activeTimer = false; // used to cancel active timer if a user plays an amima var activeEmote = false; // to keep track of the currently playing emote button = tablet.addButton({ - //icon: "icons/tablet-icons/emote.svg", // TODO - we need graphics for this + icon: "icons/tablet-icons/EmoteAppIcon.svg", text: EMOTE_LABEL, sortOrder: EMOTE_APP_SORT_ORDER }); diff --git a/scripts/system/html/EmoteApp.html b/scripts/system/html/EmoteApp.html index 30ef3e17a1..0a423b9b6c 100644 --- a/scripts/system/html/EmoteApp.html +++ b/scripts/system/html/EmoteApp.html @@ -44,11 +44,11 @@ input[type=button] { font-family: 'Raleway'; font-weight: bold; - font-size: 13px; + font-size: 20px; text-transform: uppercase; vertical-align: top; - height: 28px; - min-width: 120px; + height: 105px; + min-width: 190px; padding: 0px 18px; margin-right: 6px; border-radius: 5px; @@ -98,14 +98,14 @@

Click an emotion to Emote:

-

-

-

-

-

-

-

-

+

+

+

+

+

+

+

+

From b0f21c6931b2213032a1d1cf087e33095599bd71 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Sat, 20 Jan 2018 07:09:16 -0700 Subject: [PATCH 58/58] sorter variable names --- .../ui/src/ui/TabletScriptingInterface.cpp | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 2b89045246..0bc9676a2b 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -76,21 +76,21 @@ QVariant TabletButtonListModel::data(const QModelIndex& index, int role) const { } TabletButtonProxy* TabletButtonListModel::addButton(const QVariant& properties) { - QVariantMap newTabletButtonProperties = properties.toMap(); - if (newTabletButtonProperties.find(BUTTON_SORT_ORDER_KEY) == newTabletButtonProperties.end()) { - newTabletButtonProperties[BUTTON_SORT_ORDER_KEY] = DEFAULT_BUTTON_SORT_ORDER; + QVariantMap newProperties = properties.toMap(); + if (newProperties.find(BUTTON_SORT_ORDER_KEY) == newProperties.end()) { + newProperties[BUTTON_SORT_ORDER_KEY] = DEFAULT_BUTTON_SORT_ORDER; } - int insertButtonUsingIndex = computeNewButtonIndex(newTabletButtonProperties); - auto newTabletButtonProxy = QSharedPointer(new TabletButtonProxy(newTabletButtonProperties)); + int index = computeNewButtonIndex(newProperties); + auto button = QSharedPointer(new TabletButtonProxy(newProperties)); beginResetModel(); - int buttonCount = (int)_buttons.size(); - if (insertButtonUsingIndex < buttonCount) { - _buttons.insert(_buttons.begin() + insertButtonUsingIndex, newTabletButtonProxy); + int numButtons = (int)_buttons.size(); + if (index < numButtons) { + _buttons.insert(_buttons.begin() + index, button); } else { - _buttons.push_back(newTabletButtonProxy); + _buttons.push_back(button); } endResetModel(); - return newTabletButtonProxy.data(); + return button.data(); } void TabletButtonListModel::removeButton(TabletButtonProxy* button) { @@ -105,17 +105,17 @@ void TabletButtonListModel::removeButton(TabletButtonProxy* button) { } int TabletButtonListModel::computeNewButtonIndex(const QVariantMap& newButtonProperties) { - int buttonCount = (int)_buttons.size(); + int numButtons = (int)_buttons.size(); int newButtonSortOrder = newButtonProperties[BUTTON_SORT_ORDER_KEY].toInt(); - if (newButtonSortOrder == DEFAULT_BUTTON_SORT_ORDER) return buttonCount; - for (int i = 0; i < buttonCount; i++) { + if (newButtonSortOrder == DEFAULT_BUTTON_SORT_ORDER) return numButtons; + for (int i = 0; i < numButtons; i++) { QVariantMap tabletButtonProperties = _buttons[i]->getProperties(); int tabletButtonSortOrder = tabletButtonProperties[BUTTON_SORT_ORDER_KEY].toInt(); if (newButtonSortOrder <= tabletButtonSortOrder) { return i; } } - return buttonCount; + return numButtons; } TabletButtonsProxyModel::TabletButtonsProxyModel(QObject *parent)