diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..e866713b11 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,72 @@ +module.exports = { + "root": true, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 5 + }, + "globals": { + "Account": false, + "AnimationCache": false, + "Assets": false, + "Audio": false, + "AudioDevice": false, + "AudioEffectOptions": false, + "AvatarList": false, + "AvatarManager": false, + "Camera": false, + "Clipboard": false, + "Controller": false, + "DialogsManager": false, + "Entities": false, + "FaceTracker": false, + "GlobalServices": false, + "HMD": false, + "LODManager": false, + "Mat4": false, + "Menu": false, + "Messages": false, + "ModelCache": false, + "MyAvatar": false, + "Overlays": false, + "Paths": false, + "Quat": false, + "Rates": false, + "Recording": false, + "Reticle": false, + "Scene": false, + "Script": false, + "ScriptDiscoveryService": false, + "Settings": false, + "SoundCache": false, + "Stats": false, + "TextureCache": false, + "Uuid": false, + "UndoStack": false, + "Vec3": false, + "WebSocket": false, + "WebWindow": false, + "Window": false, + "XMLHttpRequest": false, + "location": false, + "print": false + }, + "rules": { + "brace-style": ["error", "1tbs", { "allowSingleLine": false }], + "comma-dangle": ["error", "only-multiline"], + "camelcase": ["error"], + "curly": ["error", "all"], + "indent": ["error", 4, { "SwitchCase": 1 }], + "keyword-spacing": ["error", { "before": true, "after": true }], + "max-len": ["error", 128, 4], + "new-cap": ["error"], + //"no-magic-numbers": ["error", { "ignore": [0, 1], "ignoreArrayIndexes": true }], + "no-multiple-empty-lines": ["error"], + "no-multi-spaces": ["error"], + "no-unused-vars": ["error", { "args": "none", "vars": "local" }], + "semi": ["error", "always"], + "spaced-comment": ["error", "always", { + "line": { "markers": ["/"] } + }], + "space-before-function-paren": ["error", "never"] + } +}; diff --git a/BUILD_OSX.md b/BUILD_OSX.md index 44f27d3d02..1c9c5a9796 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -13,13 +13,13 @@ We no longer require install of qt5 via our [homebrew formulas repository](https Assuming you've installed OpenSSL or Qt 5 using the homebrew instructions above, you'll need to set OPENSSL_ROOT_DIR and QT_CMAKE_PREFIX_PATH so CMake can find your installations. For OpenSSL installed via homebrew, set OPENSSL_ROOT_DIR: - export OPENSSL_ROOT_DIR=/usr/local/Cellar/openssl/1.0.2d_1 + export OPENSSL_ROOT_DIR=/usr/local/Cellar/openssl/1.0.2h_1/ For Qt 5.5.1 installed via homebrew, set QT_CMAKE_PREFIX_PATH as follows. - export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.5.1_2/lib/cmake + export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt55/5.5.1/lib/cmake -Not that these use the versions from homebrew formulae at the time of this writing, and the version in the path will likely change. +Note that these use the versions from homebrew formulae at the time of this writing, and the version in the path will likely change. ###Xcode If Xcode is your editor of choice, you can ask CMake to generate Xcode project files instead of Unix Makefiles. diff --git a/cmake/externals/neuron/CMakeLists.txt b/cmake/externals/neuron/CMakeLists.txt index 6936725571..76dda8f8c5 100644 --- a/cmake/externals/neuron/CMakeLists.txt +++ b/cmake/externals/neuron/CMakeLists.txt @@ -4,8 +4,8 @@ set(EXTERNAL_NAME neuron) string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) -set(NEURON_URL "https://s3.amazonaws.com/hifi-public/dependencies/neuron_datareader_b.12.zip") -set(NEURON_URL_MD5 "0ab54ca04c9cc8094e0fa046c226e574") +set(NEURON_URL "https://s3.amazonaws.com/hifi-public/dependencies/neuron_datareader_b.12.2.zip") +set(NEURON_URL_MD5 "84273ad2200bf86a9279d1f412a822ca") ExternalProject_Add(${EXTERNAL_NAME} URL ${NEURON_URL} diff --git a/cmake/externals/sixense/CMakeLists.txt b/cmake/externals/sixense/CMakeLists.txt index 16f2850449..bd0d042c0b 100644 --- a/cmake/externals/sixense/CMakeLists.txt +++ b/cmake/externals/sixense/CMakeLists.txt @@ -57,30 +57,7 @@ if (WIN32) elseif(APPLE) - set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${SOURCE_DIR}/lib/osx_x64/release_dll/libsixense_x64.dylib CACHE TYPE INTERNAL) - set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${SOURCE_DIR}/lib/osx_x64/debug_dll/libsixensed_x64.dylib CACHE TYPE INTERNAL) - - set(_SIXENSE_LIB_DIR "${SOURCE_DIR}/lib/osx_x64") - ExternalProject_Add_Step( - ${EXTERNAL_NAME} - change-install-name-release - COMMENT "Calling install_name_tool on libraries to fix install name for dylib linking" - COMMAND ${CMAKE_COMMAND} -DINSTALL_NAME_LIBRARY_DIR=${_SIXENSE_LIB_DIR}/release_dll -P ${EXTERNAL_PROJECT_DIR}/OSXInstallNameChange.cmake - DEPENDEES install - WORKING_DIRECTORY - LOG 1 - ) - - set(_SIXENSE_LIB_DIR "${SOURCE_DIR}/lib/osx_x64") - ExternalProject_Add_Step( - ${EXTERNAL_NAME} - change-install-name-debug - COMMENT "Calling install_name_tool on libraries to fix install name for dylib linking" - COMMAND ${CMAKE_COMMAND} -DINSTALL_NAME_LIBRARY_DIR=${_SIXENSE_LIB_DIR}/debug_dll -P ${EXTERNAL_PROJECT_DIR}/OSXInstallNameChange.cmake - DEPENDEES install - WORKING_DIRECTORY - LOG 1 - ) + # We no longer support Sixense on Macs due to bugs in the Sixense DLL elseif(NOT ANDROID) diff --git a/cmake/macros/OptionalWinExecutableSigning.cmake b/cmake/macros/OptionalWinExecutableSigning.cmake index 784aae716f..41ca5762dc 100644 --- a/cmake/macros/OptionalWinExecutableSigning.cmake +++ b/cmake/macros/OptionalWinExecutableSigning.cmake @@ -22,7 +22,7 @@ macro(optional_win_executable_signing) # setup a post build command to sign the executable add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${SIGNTOOL_EXECUTABLE} sign /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr http://tsa.starfieldtech.com /td SHA256 ${EXECUTABLE_PATH} + COMMAND ${SIGNTOOL_EXECUTABLE} sign /fd sha256 /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr http://tsa.starfieldtech.com /td SHA256 ${EXECUTABLE_PATH} ) else () message(FATAL_ERROR "HF_PFX_PASSPHRASE must be set for executables to be signed.") diff --git a/cmake/macros/TargetSixense.cmake b/cmake/macros/TargetSixense.cmake index 6fd9cede1f..07dcfe67e4 100644 --- a/cmake/macros/TargetSixense.cmake +++ b/cmake/macros/TargetSixense.cmake @@ -6,9 +6,11 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # macro(TARGET_SIXENSE) - add_dependency_external_projects(sixense) - find_package(Sixense REQUIRED) - target_include_directories(${TARGET_NAME} PRIVATE ${SIXENSE_INCLUDE_DIRS}) - target_link_libraries(${TARGET_NAME} ${SIXENSE_LIBRARIES}) - add_definitions(-DHAVE_SIXENSE) + if(NOT APPLE) + add_dependency_external_projects(sixense) + find_package(Sixense REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE ${SIXENSE_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${SIXENSE_LIBRARIES}) + add_definitions(-DHAVE_SIXENSE) + endif() endmacro() diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 0ea1199c09..4786b12743 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -64,7 +64,7 @@ ; The Inner invocation has written an uninstaller binary for us. ; We need to sign it if it's a production or PR build. !if @PRODUCTION_BUILD@ == 1 - !system '"@SIGNTOOL_EXECUTABLE@" sign /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr http://tsa.starfieldtech.com /td SHA256 $%TEMP%\@UNINSTALLER_NAME@' = 0 + !system '"@SIGNTOOL_EXECUTABLE@" sign /fd sha256 /f %HF_PFX_FILE% /p %HF_PFX_PASSPHRASE% /tr http://tsa.starfieldtech.com /td SHA256 $%TEMP%\@UNINSTALLER_NAME@' = 0 !endif ; Good. Now we can carry on writing the real installer. diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index bad24dd3a1..7375a0f650 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 1.4, + "version": 1.5, "settings": [ { "name": "metaverse", @@ -116,6 +116,7 @@ "name": "hosts", "label": "Hosts", "type": "table", + "can_add_new_rows": true, "help": "Usernames of hosts who can reliably show your domain to new visitors.", "numbered": false, "columns": [ @@ -130,6 +131,7 @@ "name": "tags", "label": "Tags", "type": "table", + "can_add_new_rows": true, "help": "Common categories under which your domain falls.", "numbered": false, "columns": [ @@ -139,6 +141,218 @@ "can_set": true } ] + }, + { + "label": "Operating Hours", + "help": "\"Open\" domains can be searched using their operating hours. Hours are entered in the local timezone, selected below.", + + "name": "weekday_hours", + "caption": "Weekday Hours (Monday-Friday)", + "type": "table", + "can_add_new_rows": false, + "columns": [ + { + "name": "open", + "label": "Opening Time", + "type": "time", + "default": "00:00", + "editable": true + }, + { + "name": "close", + "label": "Closing Time", + "type": "time", + "default": "23:59", + "editable": true + } + ] + }, + { + "name": "weekend_hours", + "label": "Weekend Hours (Saturday/Sunday)", + "type": "table", + "can_add_new_rows": false, + "columns": [ + { + "name": "open", + "label": "Opening Time", + "type": "time", + "default": "00:00", + "editable": true + }, + { + "name": "close", + "label": "Closing Time", + "type": "time", + "default": "23:59", + "editable": true + } + ] + }, + { + "label": "Time Zone", + "name": "utc_offset", + "caption": "Time Zone", + "help": "This server's time zone. Used to define your server's operating hours.", + "type": "select", + "options": [ + { + "value": "-12", + "label": "UTC-12:00" + }, + { + "value": "-11", + "label": "UTC-11:00" + }, + { + "value": "-10", + "label": "UTC-10:00" + }, + { + "value": "-9.5", + "label": "UTC-09:30" + }, + { + "value": "-9", + "label": "UTC-09:00" + }, + { + "value": "-8", + "label": "UTC-08:00" + }, + { + "value": "-7", + "label": "UTC-07:00" + }, + { + "value": "-6", + "label": "UTC-06:00" + }, + { + "value": "-5", + "label": "UTC-05:00" + }, + { + "value": "-4", + "label": "UTC-04:00" + }, + { + "value": "-3.5", + "label": "UTC-03:30" + }, + { + "value": "-3", + "label": "UTC-03:00" + }, + { + "value": "-2", + "label": "UTC-02:00" + }, + { + "value": "-1", + "label": "UTC-01:00" + }, + { + "value": "", + "label": "UTC±00:00" + }, + { + "value": "1", + "label": "UTC+01:00" + }, + { + "value": "2", + "label": "UTC+02:00" + }, + { + "value": "3", + "label": "UTC+03:00" + }, + { + "value": "3.5", + "label": "UTC+03:30" + }, + { + "value": "4", + "label": "UTC+04:00" + }, + { + "value": "4.5", + "label": "UTC+04:30" + }, + { + "value": "5", + "label": "UTC+05:00" + }, + { + "value": "5.5", + "label": "UTC+05:30" + }, + { + "value": "5.75", + "label": "UTC+05:45" + }, + { + "value": "6", + "label": "UTC+06:00" + }, + { + "value": "6.5", + "label": "UTC+06:30" + }, + { + "value": "7", + "label": "UTC+07:00" + }, + { + "value": "8", + "label": "UTC+08:00" + }, + { + "value": "8.5", + "label": "UTC+08:30" + }, + { + "value": "8.75", + "label": "UTC+08:45" + }, + { + "value": "9", + "label": "UTC+09:00" + }, + { + "value": "9.5", + "label": "UTC+09:30" + }, + { + "value": "10", + "label": "UTC+10:00" + }, + { + "value": "10.5", + "label": "UTC+10:30" + }, + { + "value": "11", + "label": "UTC+11:00" + }, + { + "value": "12", + "label": "UTC+12:00" + }, + { + "value": "12.75", + "label": "UTC+12:45" + }, + { + "value": "13", + "label": "UTC+13:00" + }, + { + "value": "14", + "label": "UTC+14:00" + } + ] } ] }, @@ -170,7 +384,7 @@ "name": "standard_permissions", "type": "table", "label": "Domain-Wide User Permissions", - "help": "Indicate which users or groups can have which domain-wide permissions.", + "help": "Indicate which users or groups can have which domain-wide permissions.", "caption": "Standard Permissions", "can_add_new_rows": false, @@ -180,7 +394,7 @@ "span": 1 }, { - "label": "Permissions ?", + "label": "Permissions ?", "span": 6 } ], @@ -249,7 +463,7 @@ "span": 1 }, { - "label": "Permissions ?", + "label": "Permissions ?", "span": 6 } ], diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index aecc48b31f..4f153d6190 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -243,6 +243,16 @@ $(document).ready(function(){ } }); + $('#' + Settings.FORM_ID).on('change', 'input.table-time', function() { + // Bootstrap switches in table: set the changed data attribute for all rows in table. + var row = $(this).closest('tr'); + if (row.hasClass("value-row")) { // Don't set attribute on input row switches prior to it being added to table. + row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true); + updateDataChangedForSiblingRows(row, true); + badgeSidebarForDifferences($(this)); + } + }); + $('.advanced-toggle').click(function(){ Settings.showAdvanced = !Settings.showAdvanced var advancedSelector = $('.' + Settings.ADVANCED_CLASS) @@ -447,6 +457,8 @@ function disonnectHighFidelityAccount() { }, function(){ // we need to post to settings to clear the access-token $(Settings.ACCESS_TOKEN_SELECTOR).val('').change(); + // reset the domain id to get a new temporary name + $(Settings.DOMAIN_ID_SELECTOR).val('').change(); saveSettings(); }); } @@ -545,7 +557,7 @@ function createNewDomainID(description, justConnected) { // get the JSON object ready that we'll use to create a new domain var domainJSON = { "domain": { - "description": description + "private_description": description }, "access_token": $(Settings.ACCESS_TOKEN_SELECTOR).val() } @@ -738,8 +750,8 @@ function chooseFromHighFidelityDomains(clickedButton) { _.each(data.data.domains, function(domain){ var domainString = ""; - if (domain.description) { - domainString += '"' + domain.description + '" - '; + if (domain.private_description) { + domainString += '"' + domain.private_description + '" - '; } domainString += domain.id; @@ -987,7 +999,7 @@ function makeTable(setting, keypath, setting_value, isLocked) { html += "" + rowIndexOrName + "" } - var isNonDeletableRow = false; + var isNonDeletableRow = !setting.can_add_new_rows; _.each(setting.columns, function(col) { @@ -1007,6 +1019,10 @@ function makeTable(setting, keypath, setting_value, isLocked) { html += "" + ""; + } else if (isArray && col.type === "time" && col.editable) { + html += "" + + ""; } else { // Use a hidden input so that the values are posted. html += "" @@ -1196,15 +1212,21 @@ function addTableRow(add_glyphicon) { // Hide inputs var input = $(element).find("input") var isCheckbox = false; + var isTime = false; if (input.hasClass("table-checkbox")) { input = $(input).parent(); isCheckbox = true; + } else if (input.hasClass("table-time")) { + input = $(input).parent(); + isTime = true; } var val = input.val(); if (isCheckbox) { - val = $(input).find("input").is(':checked'); // don't hide the checkbox + val = $(input).find("input").is(':checked'); + } else if (isTime) { + // don't hide the time } else { input.attr("type", "hidden") } diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index 26d2bb87ce..6a17bff4c0 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -10,16 +10,18 @@ #include "DomainMetadata.h" -#include +#include #include +#include #include +#include "DomainServer.h" #include "DomainServerNodeData.h" const QString DomainMetadata::USERS = "users"; -const QString DomainMetadata::USERS_NUM_TOTAL = "num_users"; -const QString DomainMetadata::USERS_NUM_ANON = "num_anon_users"; -const QString DomainMetadata::USERS_HOSTNAMES = "user_hostnames"; +const QString DomainMetadata::Users::NUM_TOTAL = "num_users"; +const QString DomainMetadata::Users::NUM_ANON = "num_anon_users"; +const QString DomainMetadata::Users::HOSTNAMES = "user_hostnames"; // users metadata will appear as (JSON): // { "num_users": Number, // "num_anon_users": Number, @@ -27,26 +29,30 @@ const QString DomainMetadata::USERS_HOSTNAMES = "user_hostnames"; // } const QString DomainMetadata::DESCRIPTORS = "descriptors"; -const QString DomainMetadata::DESCRIPTORS_DESCRIPTION = "description"; -const QString DomainMetadata::DESCRIPTORS_CAPACITY = "capacity"; // parsed from security -const QString DomainMetadata::DESCRIPTORS_RESTRICTION = "restriction"; // parsed from ACL -const QString DomainMetadata::DESCRIPTORS_MATURITY = "maturity"; -const QString DomainMetadata::DESCRIPTORS_HOSTS = "hosts"; -const QString DomainMetadata::DESCRIPTORS_TAGS = "tags"; +const QString DomainMetadata::Descriptors::DESCRIPTION = "description"; +const QString DomainMetadata::Descriptors::CAPACITY = "capacity"; // parsed from security +const QString DomainMetadata::Descriptors::RESTRICTION = "restriction"; // parsed from ACL +const QString DomainMetadata::Descriptors::MATURITY = "maturity"; +const QString DomainMetadata::Descriptors::HOSTS = "hosts"; +const QString DomainMetadata::Descriptors::TAGS = "tags"; +const QString DomainMetadata::Descriptors::HOURS = "hours"; +const QString DomainMetadata::Descriptors::Hours::WEEKDAY = "weekday"; +const QString DomainMetadata::Descriptors::Hours::WEEKEND = "weekend"; +const QString DomainMetadata::Descriptors::Hours::UTC_OFFSET = "utc_offset"; +const QString DomainMetadata::Descriptors::Hours::OPEN = "open"; +const QString DomainMetadata::Descriptors::Hours::CLOSE = "close"; // descriptors metadata will appear as (JSON): -// { "capacity": Number, -// TODO: "hours": String, // UTF-8 representation of the week, split into 15" segments +// { "description": String, // capped description +// "capacity": Number, // "restriction": String, // enum of either open, hifi, or acl // "maturity": String, // enum corresponding to ESRB ratings // "hosts": [ String ], // capped list of usernames -// "description": String, // capped description -// TODO: "img": { -// "src": String, -// "type": String, -// "size": Number, -// "updated_at": Number, -// }, // "tags": [ String ], // capped list of tags +// "hours": { +// "utc_offset": Number, +// "weekday": [ { "open": Time, "close": Time } ], +// "weekend": [ { "open": Time, "close": Time } ], +// } // } // metadata will appear as (JSON): @@ -54,36 +60,178 @@ const QString DomainMetadata::DESCRIPTORS_TAGS = "tags"; // // it is meant to be sent to and consumed by an external API -DomainMetadata::DomainMetadata() { - _metadata[USERS] = {}; - _metadata[DESCRIPTORS] = {}; +// merge delta into target +// target should be of the form [ OpenTime, CloseTime ], +// delta should be of the form [ { open: Time, close: Time } ] +void parseHours(QVariant delta, QVariant& target) { + using Hours = DomainMetadata::Descriptors::Hours; + + assert(target.canConvert()); + auto& targetList = *static_cast(target.data()); + + // if/when multiple ranges are allowed, this list will need to be iterated + assert(targetList[0].canConvert()); + auto& hours = *static_cast(targetList[0].data()); + + if (!delta.canConvert()) { + return; + } + + auto& deltaList = *static_cast(delta.data()); + if (deltaList.isEmpty()) { + return; + } + + auto& deltaHours = *static_cast(deltaList.first().data()); + if (deltaHours.isEmpty()) { + return; + } + + // merge delta into base + static const int OPEN_INDEX = 0; + static const int CLOSE_INDEX = 1; + auto open = deltaHours.find(Hours::OPEN); + if (open != deltaHours.end()) { + hours[OPEN_INDEX] = open.value(); + } + assert(hours[OPEN_INDEX].canConvert()); + + auto close = deltaHours.find(Hours::CLOSE); + if (close != deltaHours.end()) { + hours[CLOSE_INDEX] = close.value(); + } + assert(hours[CLOSE_INDEX].canConvert()); + } -void DomainMetadata::setDescriptors(QVariantMap& settings) { - const QString CAPACITY = "security.maximum_user_capacity"; +DomainMetadata::DomainMetadata(QObject* domainServer) : QObject(domainServer) { + // set up the structure necessary for casting during parsing (see parseHours, esp.) + _metadata[USERS] = QVariantMap {}; + _metadata[DESCRIPTORS] = QVariantMap { { + Descriptors::HOURS, QVariantMap { + { Descriptors::Hours::WEEKDAY, QVariantList { + QVariantList{ QVariant{}, QVariant{} } } + }, + { Descriptors::Hours::WEEKEND, QVariantList { + QVariantList{ QVariant{}, QVariant{} } } + } + } + } }; + + assert(dynamic_cast(domainServer)); + DomainServer* server = static_cast(domainServer); + + // update the metadata when a user (dis)connects + connect(server, &DomainServer::userConnected, this, &DomainMetadata::usersChanged); + connect(server, &DomainServer::userDisconnected, this, &DomainMetadata::usersChanged); + + // update the metadata when security changes + connect(&server->_settingsManager, &DomainServerSettingsManager::updateNodePermissions, + this, static_cast(&DomainMetadata::securityChanged)); + + // initialize the descriptors + securityChanged(false); + descriptorsChanged(); +} + +QJsonObject DomainMetadata::get() { + maybeUpdateUsers(); + return QJsonObject::fromVariantMap(_metadata); +} + +QJsonObject DomainMetadata::get(const QString& group) { + maybeUpdateUsers(); + return QJsonObject::fromVariantMap(_metadata[group].toMap()); +} + +void DomainMetadata::descriptorsChanged() { + // get descriptors + assert(_metadata[DESCRIPTORS].canConvert()); + auto& state = *static_cast(_metadata[DESCRIPTORS].data()); + auto& settings = static_cast(parent())->_settingsManager.getSettingsMap(); + auto& descriptors = static_cast(parent())->_settingsManager.getDescriptorsMap(); + + // copy simple descriptors (description/maturity) + state[Descriptors::DESCRIPTION] = descriptors[Descriptors::DESCRIPTION]; + state[Descriptors::MATURITY] = descriptors[Descriptors::MATURITY]; + + // copy array descriptors (hosts/tags) + state[Descriptors::HOSTS] = descriptors[Descriptors::HOSTS].toList(); + state[Descriptors::TAGS] = descriptors[Descriptors::TAGS].toList(); + + // parse capacity + static const QString CAPACITY = "security.maximum_user_capacity"; const QVariant* capacityVariant = valueForKeyPath(settings, CAPACITY); unsigned int capacity = capacityVariant ? capacityVariant->toUInt() : 0; + state[Descriptors::CAPACITY] = capacity; - // TODO: Keep parity with ACL development. - const QString RESTRICTION = "security.restricted_access"; - const QString RESTRICTION_OPEN = "open"; - // const QString RESTRICTION_HIFI = "hifi"; - const QString RESTRICTION_ACL = "acl"; - const QVariant* isRestrictedVariant = valueForKeyPath(settings, RESTRICTION); - bool isRestricted = isRestrictedVariant ? isRestrictedVariant->toBool() : false; - QString restriction = isRestricted ? RESTRICTION_ACL : RESTRICTION_OPEN; - - QVariantMap descriptors = settings[DESCRIPTORS].toMap(); - descriptors[DESCRIPTORS_CAPACITY] = capacity; - descriptors[DESCRIPTORS_RESTRICTION] = restriction; - _metadata[DESCRIPTORS] = descriptors; + // parse operating hours + static const QString WEEKDAY_HOURS = "weekday_hours"; + static const QString WEEKEND_HOURS = "weekend_hours"; + static const QString UTC_OFFSET = "utc_offset"; + assert(state[Descriptors::HOURS].canConvert()); + auto& hours = *static_cast(state[Descriptors::HOURS].data()); + hours[Descriptors::Hours::UTC_OFFSET] = descriptors.take(UTC_OFFSET); + parseHours(descriptors[WEEKDAY_HOURS], hours[Descriptors::Hours::WEEKDAY]); + parseHours(descriptors[WEEKEND_HOURS], hours[Descriptors::Hours::WEEKEND]); #if DEV_BUILD || PR_BUILD - qDebug() << "Domain metadata descriptors set:" << descriptors; + qDebug() << "Domain metadata descriptors set:" << QJsonObject::fromVariantMap(_metadata[DESCRIPTORS].toMap()); +#endif + + sendDescriptors(); +} + +void DomainMetadata::securityChanged(bool send) { + // get descriptors + assert(_metadata[DESCRIPTORS].canConvert()); + auto& state = *static_cast(_metadata[DESCRIPTORS].data()); + + const QString RESTRICTION_OPEN = "open"; + const QString RESTRICTION_ANON = "anon"; + const QString RESTRICTION_HIFI = "hifi"; + const QString RESTRICTION_ACL = "acl"; + + QString restriction; + + const auto& settingsManager = static_cast(parent())->_settingsManager; + bool hasAnonymousAccess = + settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous).canConnectToDomain; + bool hasHifiAccess = + settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn).canConnectToDomain; + if (hasAnonymousAccess) { + restriction = hasHifiAccess ? RESTRICTION_OPEN : RESTRICTION_ANON; + } else if (hasHifiAccess) { + restriction = RESTRICTION_HIFI; + } else { + restriction = RESTRICTION_ACL; + } + + state[Descriptors::RESTRICTION] = restriction; + +#if DEV_BUILD || PR_BUILD + qDebug() << "Domain metadata restriction set:" << restriction; +#endif + + if (send) { + sendDescriptors(); + } +} + +void DomainMetadata::usersChanged() { + ++_tic; + +#if DEV_BUILD || PR_BUILD + qDebug() << "Domain metadata users change detected"; #endif } -void DomainMetadata::updateUsers() { +void DomainMetadata::maybeUpdateUsers() { + if (_lastTic == _tic) { + return; + } + _lastTic = _tic; + static const QString DEFAULT_HOSTNAME = "*"; auto nodeList = DependencyManager::get(); @@ -112,21 +260,32 @@ void DomainMetadata::updateUsers() { } }); - QVariantMap users = { - { USERS_NUM_TOTAL, numConnected }, - { USERS_NUM_ANON, numConnectedAnonymously }, - { USERS_HOSTNAMES, userHostnames }}; - _metadata[USERS] = users; + assert(_metadata[USERS].canConvert()); + auto& users = *static_cast(_metadata[USERS].data()); + users[Users::NUM_TOTAL] = numConnected; + users[Users::NUM_ANON] = numConnectedAnonymously; + users[Users::HOSTNAMES] = userHostnames; #if DEV_BUILD || PR_BUILD - qDebug() << "Domain metadata users updated:" << users; + qDebug() << "Domain metadata users set:" << QJsonObject::fromVariantMap(_metadata[USERS].toMap()); #endif } -void DomainMetadata::usersChanged() { - ++_tic; +void DomainMetadata::sendDescriptors() { + QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(get(DESCRIPTORS)).toJson(QJsonDocument::Compact))); + const QUuid& domainID = DependencyManager::get()->getSessionUUID(); + if (!domainID.isNull()) { + static const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; + QString path { DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)) }; + DependencyManager::get()->sendRequest(path, + AccountManagerAuth::Required, + QNetworkAccessManager::PutOperation, + JSONCallbackParameters(), + domainUpdateJSON.toUtf8()); #if DEV_BUILD || PR_BUILD - qDebug() << "Domain metadata users change detected"; + qDebug() << "Domain metadata sent to" << path; + qDebug() << "Domain metadata update:" << domainUpdateJSON; #endif + } } diff --git a/domain-server/src/DomainMetadata.h b/domain-server/src/DomainMetadata.h index 7d58d43182..41f3a60832 100644 --- a/domain-server/src/DomainMetadata.h +++ b/domain-server/src/DomainMetadata.h @@ -19,46 +19,56 @@ class DomainMetadata : public QObject { Q_OBJECT +public: + using Tic = uint32_t; + static const QString USERS; - static const QString USERS_NUM_TOTAL; - static const QString USERS_NUM_ANON; - static const QString USERS_HOSTNAMES; + class Users { + public: + static const QString NUM_TOTAL; + static const QString NUM_ANON; + static const QString HOSTNAMES; + }; static const QString DESCRIPTORS; - static const QString DESCRIPTORS_DESCRIPTION; - static const QString DESCRIPTORS_CAPACITY; - static const QString DESCRIPTORS_HOURS; - static const QString DESCRIPTORS_RESTRICTION; - static const QString DESCRIPTORS_MATURITY; - static const QString DESCRIPTORS_HOSTS; - static const QString DESCRIPTORS_TAGS; - static const QString DESCRIPTORS_IMG; - static const QString DESCRIPTORS_IMG_SRC; - static const QString DESCRIPTORS_IMG_TYPE; - static const QString DESCRIPTORS_IMG_SIZE; - static const QString DESCRIPTORS_IMG_UPDATED_AT; + class Descriptors { + public: + static const QString DESCRIPTION; + static const QString CAPACITY; + static const QString RESTRICTION; + static const QString MATURITY; + static const QString HOSTS; + static const QString TAGS; + static const QString HOURS; + class Hours { + public: + static const QString WEEKDAY; + static const QString WEEKEND; + static const QString UTC_OFFSET; + static const QString OPEN; + static const QString CLOSE; + }; + }; -public: - DomainMetadata(); + DomainMetadata(QObject* domainServer); + DomainMetadata() = delete; - // Returns the last set metadata - // If connected users have changed, metadata may need to be updated - // this should be checked by storing tic = getTic() between calls - // and testing it for equality before the next get (tic == getTic()) - QJsonObject get() { return QJsonObject::fromVariantMap(_metadata); } - QJsonObject getUsers() { return QJsonObject::fromVariantMap(_metadata[USERS].toMap()); } - QJsonObject getDescriptors() { return QJsonObject::fromVariantMap(_metadata[DESCRIPTORS].toMap()); } - - uint32_t getTic() { return _tic; } - - void setDescriptors(QVariantMap& settings); - void updateUsers(); + // Get cached metadata + QJsonObject get(); + QJsonObject get(const QString& group); public slots: + void descriptorsChanged(); + void securityChanged(bool send); + void securityChanged() { securityChanged(true); } void usersChanged(); protected: + void maybeUpdateUsers(); + void sendDescriptors(); + QVariantMap _metadata; + uint32_t _lastTic{ (uint32_t)-1 }; uint32_t _tic{ 0 }; }; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 1f666455c6..d75a2c3245 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -76,6 +76,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : setApplicationVersion(BuildInfo::VERSION); QSettings::setDefaultFormat(QSettings::IniFormat); + qDebug() << "Setting up domain-server"; + // make sure we have a fresh AccountManager instance // (need this since domain-server can restart itself and maintain static variables) DependencyManager::set(); @@ -94,10 +96,6 @@ DomainServer::DomainServer(int argc, char* argv[]) : qRegisterMetaType("DomainServerWebSessionData"); qRegisterMetaTypeStreamOperators("DomainServerWebSessionData"); - // update the metadata when a user (dis)connects - connect(this, &DomainServer::userConnected, &_metadata, &DomainMetadata::usersChanged); - connect(this, &DomainServer::userDisconnected, &_metadata, &DomainMetadata::usersChanged); - // make sure we hear about newly connected nodes from our gatekeeper connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode); @@ -108,34 +106,31 @@ DomainServer::DomainServer(int argc, char* argv[]) : connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions, &_gatekeeper, &DomainGatekeeper::updateNodePermissions); - // update the metadata with current descriptors - _metadata.setDescriptors(_settingsManager.getSettingsMap()); - - if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) { - // we either read a certificate and private key or were not passed one - // and completed login or did not need to - - qDebug() << "Setting up LimitedNodeList and assignments."; - setupNodeListAndAssignments(); - - // setup automatic networking settings with data server - setupAutomaticNetworking(); - - // preload some user public keys so they can connect on first request - _gatekeeper.preloadAllowedUserPublicKeys(); - - optionallyGetTemporaryName(args); - - // send metadata descriptors - QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(_metadata.getDescriptors()).toJson(QJsonDocument::Compact))); - const QUuid& domainID = DependencyManager::get()->getSessionUUID(); - static const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; - DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), - AccountManagerAuth::Required, - QNetworkAccessManager::PutOperation, - JSONCallbackParameters(), - domainUpdateJSON.toUtf8()); + // if we were given a certificate/private key or oauth credentials they must succeed + if (!(optionallyReadX509KeyAndCertificate() && optionallySetupOAuth())) { + return; } + + setupNodeListAndAssignments(); + setupAutomaticNetworking(); + if (!getID().isNull()) { + setupHeartbeatToMetaverse(); + // send the first heartbeat immediately + sendHeartbeatToMetaverse(); + } + + // check for the temporary name parameter + const QString GET_TEMPORARY_NAME_SWITCH = "--get-temp-name"; + if (args.contains(GET_TEMPORARY_NAME_SWITCH)) { + getTemporaryName(); + } + + _gatekeeper.preloadAllowedUserPublicKeys(); // so they can connect on first request + + _metadata = new DomainMetadata(this); + + + qDebug() << "domain-server is running"; } DomainServer::~DomainServer() { @@ -163,6 +158,10 @@ void DomainServer::restart() { exit(DomainServer::EXIT_CODE_REBOOT); } +const QUuid& DomainServer::getID() { + return DependencyManager::get()->getSessionUUID(); +} + bool DomainServer::optionallyReadX509KeyAndCertificate() { const QString X509_CERTIFICATE_OPTION = "cert"; const QString X509_PRIVATE_KEY_OPTION = "key"; @@ -248,34 +247,26 @@ bool DomainServer::optionallySetupOAuth() { static const QString METAVERSE_DOMAIN_ID_KEY_PATH = "metaverse.id"; -void DomainServer::optionallyGetTemporaryName(const QStringList& arguments) { - // check for the temporary name parameter - const QString GET_TEMPORARY_NAME_SWITCH = "--get-temp-name"; +void DomainServer::getTemporaryName(bool force) { + // check if we already have a domain ID + const QVariant* idValueVariant = valueForKeyPath(_settingsManager.getSettingsMap(), METAVERSE_DOMAIN_ID_KEY_PATH); - if (arguments.contains(GET_TEMPORARY_NAME_SWITCH)) { - - // make sure we don't already have a domain ID - const QVariant* idValueVariant = valueForKeyPath(_settingsManager.getSettingsMap(), METAVERSE_DOMAIN_ID_KEY_PATH); - if (idValueVariant) { - qWarning() << "Temporary domain name requested but a domain ID is already present in domain-server settings." - << "Will not request temporary name."; + qInfo() << "Requesting temporary domain name"; + if (idValueVariant) { + qDebug() << "A domain ID is already present in domain-server settings:" << idValueVariant->toString(); + if (force) { + qDebug() << "Requesting temporary domain name to replace current ID:" << getID(); + } else { + qInfo() << "Abandoning request of temporary domain name."; return; } - - // we've been asked to grab a temporary name from the API - // so fire off that request now - auto accountManager = DependencyManager::get(); - - // get callbacks for temporary domain result - JSONCallbackParameters callbackParameters; - callbackParameters.jsonCallbackReceiver = this; - callbackParameters.jsonCallbackMethod = "handleTempDomainSuccess"; - callbackParameters.errorCallbackReceiver = this; - callbackParameters.errorCallbackMethod = "handleTempDomainError"; - - accountManager->sendRequest("/api/v1/domains/temporary", AccountManagerAuth::None, - QNetworkAccessManager::PostOperation, callbackParameters); } + + // request a temporary name from the metaverse + auto accountManager = DependencyManager::get(); + JSONCallbackParameters callbackParameters { this, "handleTempDomainSuccess", this, "handleTempDomainError" }; + accountManager->sendRequest("/api/v1/domains/temporary", AccountManagerAuth::None, + QNetworkAccessManager::PostOperation, callbackParameters); } void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) { @@ -286,11 +277,13 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) { static const QString DOMAIN_KEY = "domain"; static const QString ID_KEY = "id"; static const QString NAME_KEY = "name"; + static const QString KEY_KEY = "api_key"; auto domainObject = jsonObject[DATA_KEY].toObject()[DOMAIN_KEY].toObject(); if (!domainObject.isEmpty()) { auto id = domainObject[ID_KEY].toString(); auto name = domainObject[NAME_KEY].toString(); + auto key = domainObject[KEY_KEY].toString(); qInfo() << "Received new temporary domain name" << name; qDebug() << "The temporary domain ID is" << id; @@ -306,9 +299,13 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) { // change our domain ID immediately DependencyManager::get()->setSessionUUID(QUuid { id }); - // change our automatic networking settings so that we're communicating with the ICE server - setupICEHeartbeatForFullNetworking(); + // store the new token to the account info + auto accountManager = DependencyManager::get(); + accountManager->setTemporaryDomain(id, key); + // update our heartbeats to use the correct id + setupICEHeartbeatForFullNetworking(); + setupHeartbeatToMetaverse(); } else { qWarning() << "There were problems parsing the API response containing a temporary domain name. Please try again" << "via domain-server relaunch or from the domain-server settings."; @@ -347,8 +344,7 @@ bool DomainServer::packetVersionMatch(const udt::Packet& packet) { } -void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) { - +void DomainServer::setupNodeListAndAssignments() { const QString CUSTOM_LOCAL_PORT_OPTION = "metaverse.local_port"; QVariant localPortValue = _settingsManager.valueOrDefaultValueForKeyPath(CUSTOM_LOCAL_PORT_OPTION); @@ -473,29 +469,23 @@ bool DomainServer::resetAccountManagerAccessToken() { } void DomainServer::setupAutomaticNetworking() { - auto nodeList = DependencyManager::get(); + qDebug() << "Updating automatic networking setting in domain-server to" << _automaticNetworkingSetting; + + resetAccountManagerAccessToken(); _automaticNetworkingSetting = _settingsManager.valueOrDefaultValueForKeyPath(METAVERSE_AUTOMATIC_NETWORKING_KEY_PATH).toString(); + auto nodeList = DependencyManager::get(); + const QUuid& domainID = getID(); + if (_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) { setupICEHeartbeatForFullNetworking(); } - _hasAccessToken = resetAccountManagerAccessToken(); - - if (!_hasAccessToken) { - qDebug() << "Will not send heartbeat to Metaverse API without an access token."; - qDebug() << "If this is not a temporary domain add an access token to your config file or via the web interface."; - - return; - } - if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE || _automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) { - const QUuid& domainID = nodeList->getSessionUUID(); - if (!domainID.isNull()) { qDebug() << "domain-server" << _automaticNetworkingSetting << "automatic networking enabled for ID" << uuidStringWithoutCurlyBraces(domainID) << "via" << _oauthProviderURL.toString(); @@ -507,9 +497,6 @@ void DomainServer::setupAutomaticNetworking() { // have the LNL enable public socket updating via STUN nodeList->startSTUNPublicSocketUpdate(); - } else { - // send our heartbeat to data server so it knows what our network settings are - sendHeartbeatToMetaverse(); } } else { qDebug() << "Cannot enable domain-server automatic networking without a domain ID." @@ -517,18 +504,20 @@ void DomainServer::setupAutomaticNetworking() { return; } - } else { - sendHeartbeatToMetaverse(); } +} - qDebug() << "Updating automatic networking setting in domain-server to" << _automaticNetworkingSetting; - - // no matter the auto networking settings we should heartbeat to the data-server every 15s +void DomainServer::setupHeartbeatToMetaverse() { + // heartbeat to the data-server every 15s const int DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS = 15 * 1000; - QTimer* dataHeartbeatTimer = new QTimer(this); - connect(dataHeartbeatTimer, SIGNAL(timeout()), this, SLOT(sendHeartbeatToMetaverse())); - dataHeartbeatTimer->start(DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS); + if (!_metaverseHeartbeatTimer) { + // setup a timer to heartbeat with the metaverse-server + _metaverseHeartbeatTimer = new QTimer { this }; + connect(_metaverseHeartbeatTimer, SIGNAL(timeout()), this, SLOT(sendHeartbeatToMetaverse())); + // do not send a heartbeat immediately - this avoids flooding if the heartbeat fails with a 401 + _metaverseHeartbeatTimer->start(DOMAIN_SERVER_DATA_WEB_HEARTBEAT_MSECS); + } } void DomainServer::setupICEHeartbeatForFullNetworking() { @@ -547,22 +536,21 @@ void DomainServer::setupICEHeartbeatForFullNetworking() { limitedNodeList->startSTUNPublicSocketUpdate(); // to send ICE heartbeats we'd better have a private key locally with an uploaded public key - auto accountManager = DependencyManager::get(); - auto domainID = accountManager->getAccountInfo().getDomainID(); - // if we have an access token and we don't have a private key or the current domain ID has changed // we should generate a new keypair - if (!accountManager->getAccountInfo().hasPrivateKey() || domainID != limitedNodeList->getSessionUUID()) { - accountManager->generateNewDomainKeypair(limitedNodeList->getSessionUUID()); + auto accountManager = DependencyManager::get(); + if (!accountManager->getAccountInfo().hasPrivateKey() || accountManager->getAccountInfo().getDomainID() != getID()) { + accountManager->generateNewDomainKeypair(getID()); } // hookup to the signal from account manager that tells us when keypair is available connect(accountManager.data(), &AccountManager::newKeypair, this, &DomainServer::handleKeypairChange); if (!_iceHeartbeatTimer) { - // setup a timer to heartbeat with the ice-server every so often + // setup a timer to heartbeat with the ice-server _iceHeartbeatTimer = new QTimer { this }; connect(_iceHeartbeatTimer, &QTimer::timeout, this, &DomainServer::sendHeartbeatToIceServer); + sendHeartbeatToIceServer(); _iceHeartbeatTimer->start(ICE_HEARBEAT_INTERVAL_MSECS); } } @@ -1090,12 +1078,14 @@ void DomainServer::performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr) } void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { - auto nodeList = DependencyManager::get(); - const QUuid& domainID = nodeList->getSessionUUID(); - // Setup the domain object to send to the data server QJsonObject domainObject; + // add the version + static const QString VERSION_KEY = "version"; + domainObject[VERSION_KEY] = BuildInfo::VERSION; + + // add networking if (!networkAddress.isEmpty()) { static const QString PUBLIC_NETWORK_ADDRESS_KEY = "network_address"; domainObject[PUBLIC_NETWORK_ADDRESS_KEY] = networkAddress; @@ -1104,37 +1094,83 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { static const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking"; domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting; - // add a flag to indicate if this domain uses restricted access - for now that will exclude it from listings - const QString RESTRICTED_ACCESS_FLAG = "restricted"; - // consider the domain to have restricted access if "anonymous" connections can't connect to the domain. + // add access level for anonymous connections + // consider the domain to be "restricted" if anonymous connections are disallowed + static const QString RESTRICTED_ACCESS_FLAG = "restricted"; NodePermissions anonymousPermissions = _settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous); domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.canConnectToDomain; - // Add the metadata to the heartbeat - static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; - auto tic = _metadata.getTic(); - if (_metadataTic != tic) { - _metadataTic = tic; - _metadata.updateUsers(); + const auto& temporaryDomainKey = DependencyManager::get()->getTemporaryDomainKey(getID()); + if (!temporaryDomainKey.isEmpty()) { + // add the temporary domain token + const QString KEY_KEY = "api_key"; + domainObject[KEY_KEY] = temporaryDomainKey; + } + + if (_metadata) { + // Add the metadata to the heartbeat + static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; + domainObject[DOMAIN_HEARTBEAT_KEY] = _metadata->get(DomainMetadata::USERS); } - domainObject[DOMAIN_HEARTBEAT_KEY] = _metadata.getUsers(); QString domainUpdateJSON = QString("{\"domain\":%1}").arg(QString(QJsonDocument(domainObject).toJson(QJsonDocument::Compact))); static const QString DOMAIN_UPDATE = "/api/v1/domains/%1"; - DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), - AccountManagerAuth::Required, + DependencyManager::get()->sendRequest(DOMAIN_UPDATE.arg(uuidStringWithoutCurlyBraces(getID())), + AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, - JSONCallbackParameters(), + JSONCallbackParameters(nullptr, QString(), this, "handleMetaverseHeartbeatError"), domainUpdateJSON.toUtf8()); } +void DomainServer::handleMetaverseHeartbeatError(QNetworkReply& requestReply) { + if (!_metaverseHeartbeatTimer) { + // avoid rehandling errors from the same issue + return; + } + + // check if we need to force a new temporary domain name + switch (requestReply.error()) { + // if we have a temporary domain with a bad token, we get a 401 + case QNetworkReply::NetworkError::AuthenticationRequiredError: { + static const QString DATA_KEY = "data"; + static const QString TOKEN_KEY = "api_key"; + + QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); + auto tokenFailure = jsonObject[DATA_KEY].toObject()[TOKEN_KEY]; + + if (!tokenFailure.isNull()) { + qWarning() << "Temporary domain name lacks a valid API key, and is being reset."; + } + break; + } + // if the domain does not (or no longer) exists, we get a 404 + case QNetworkReply::NetworkError::ContentNotFoundError: + qWarning() << "Domain not found, getting a new temporary domain."; + break; + // otherwise, we erred on something else, and should not force a temporary domain + default: + return; + } + + // halt heartbeats until we have a token + _metaverseHeartbeatTimer->deleteLater(); + _metaverseHeartbeatTimer = nullptr; + + // give up eventually to avoid flooding traffic + static const int MAX_ATTEMPTS = 5; + static int attempt = 0; + if (++attempt < MAX_ATTEMPTS) { + // get a new temporary name and token + getTemporaryName(true); + } else { + qWarning() << "Already attempted too many temporary domain requests. Please set a domain ID manually or restart."; + } +} + void DomainServer::sendICEServerAddressToMetaverseAPI() { if (!_iceServerSocket.isNull()) { - auto nodeList = DependencyManager::get(); - const QUuid& domainID = nodeList->getSessionUUID(); - const QString ICE_SERVER_ADDRESS = "ice_server_address"; QJsonObject domainObject; @@ -1142,6 +1178,13 @@ void DomainServer::sendICEServerAddressToMetaverseAPI() { // we're using full automatic networking and we have a current ice-server socket, use that now domainObject[ICE_SERVER_ADDRESS] = _iceServerSocket.getAddress().toString(); + const auto& temporaryDomainKey = DependencyManager::get()->getTemporaryDomainKey(getID()); + if (!temporaryDomainKey.isEmpty()) { + // add the temporary domain token + const QString KEY_KEY = "api_key"; + domainObject[KEY_KEY] = temporaryDomainKey; + } + QString domainUpdateJSON = QString("{\"domain\": %1 }").arg(QString(QJsonDocument(domainObject).toJson())); // make sure we hear about failure so we can retry @@ -1153,7 +1196,7 @@ void DomainServer::sendICEServerAddressToMetaverseAPI() { static const QString DOMAIN_ICE_ADDRESS_UPDATE = "/api/v1/domains/%1/ice_server_address"; - DependencyManager::get()->sendRequest(DOMAIN_ICE_ADDRESS_UPDATE.arg(uuidStringWithoutCurlyBraces(domainID)), + DependencyManager::get()->sendRequest(DOMAIN_ICE_ADDRESS_UPDATE.arg(uuidStringWithoutCurlyBraces(getID())), AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, callbackParameters, diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index bdcc36c1ac..138cb9ca2d 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -80,6 +80,8 @@ private slots: void handleTempDomainSuccess(QNetworkReply& requestReply); void handleTempDomainError(QNetworkReply& requestReply); + void handleMetaverseHeartbeatError(QNetworkReply& requestReply); + void queuedQuit(QString quitMessage, int exitCode); void handleKeypairChange(); @@ -96,11 +98,13 @@ signals: void userDisconnected(); private: - void setupNodeListAndAssignments(const QUuid& sessionUUID = QUuid::createUuid()); + const QUuid& getID(); + + void setupNodeListAndAssignments(); bool optionallySetupOAuth(); bool optionallyReadX509KeyAndCertificate(); - void optionallyGetTemporaryName(const QStringList& arguments); + void getTemporaryName(bool force = false); static bool packetVersionMatch(const udt::Packet& packet); @@ -108,6 +112,7 @@ private: void setupAutomaticNetworking(); void setupICEHeartbeatForFullNetworking(); + void setupHeartbeatToMetaverse(); void sendHeartbeatToMetaverse(const QString& networkAddress); void randomizeICEServerAddress(bool shouldTriggerHostLookup); @@ -172,13 +177,13 @@ private: DomainServerSettingsManager _settingsManager; - DomainMetadata _metadata; - uint32_t _metadataTic{ 0 }; - HifiSockAddr _iceServerSocket; std::unique_ptr _iceServerHeartbeatPacket; - QTimer* _iceHeartbeatTimer { nullptr }; // this looks like it dangles when created but it's parented to the DomainServer + // These will be parented to this, they are not dangling + DomainMetadata* _metadata { nullptr }; + QTimer* _iceHeartbeatTimer { nullptr }; + QTimer* _metaverseHeartbeatTimer { nullptr }; QList _iceServerAddresses; QSet _failedIceServerAddresses; @@ -187,9 +192,8 @@ private: int _numHeartbeatDenials { 0 }; bool _connectedToICEServer { false }; - bool _hasAccessToken { false }; - friend class DomainGatekeeper; + friend class DomainMetadata; }; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index b03d2c07ae..262cc9d9ee 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -21,10 +21,13 @@ #include #include +#include + #include #include #include #include +#include #include "DomainServerSettingsManager.h" @@ -69,7 +72,7 @@ DomainServerSettingsManager::DomainServerSettingsManager() : } static const QString MISSING_SETTINGS_DESC_MSG = - QString("Did not find settings decription in JSON at %1 - Unable to continue. domain-server will quit.\n%2 at %3") + QString("Did not find settings description in JSON at %1 - Unable to continue. domain-server will quit.\n%2 at %3") .arg(SETTINGS_DESCRIPTION_RELATIVE_PATH).arg(parseError.errorString()).arg(parseError.offset); static const int MISSING_SETTINGS_DESC_ERROR_CODE = 6; @@ -258,6 +261,11 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList _standardAgentPermissions.clear(); _agentPermissions.clear(); } + + if (oldVersion < 1.5) { + // This was prior to operating hours, so add default hours + validateDescriptorsMap(); + } } unpackPermissions(); @@ -266,6 +274,49 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList appSettings.setValue(JSON_SETTINGS_VERSION_KEY, _descriptionVersion); } +QVariantMap& DomainServerSettingsManager::getDescriptorsMap() { + validateDescriptorsMap(); + + static const QString DESCRIPTORS{ "descriptors" }; + return *static_cast(getSettingsMap()[DESCRIPTORS].data()); +} + +void DomainServerSettingsManager::validateDescriptorsMap() { + static const QString WEEKDAY_HOURS{ "descriptors.weekday_hours" }; + static const QString WEEKEND_HOURS{ "descriptors.weekend_hours" }; + static const QString UTC_OFFSET{ "descriptors.utc_offset" }; + + QVariant* weekdayHours = valueForKeyPath(_configMap.getUserConfig(), WEEKDAY_HOURS, true); + QVariant* weekendHours = valueForKeyPath(_configMap.getUserConfig(), WEEKEND_HOURS, true); + QVariant* utcOffset = valueForKeyPath(_configMap.getUserConfig(), UTC_OFFSET, true); + + static const QString OPEN{ "open" }; + static const QString CLOSE{ "close" }; + static const QString DEFAULT_OPEN{ "00:00" }; + static const QString DEFAULT_CLOSE{ "23:59" }; + bool wasMalformed = false; + if (weekdayHours->isNull()) { + *weekdayHours = QVariantList{ QVariantMap{ { OPEN, QVariant(DEFAULT_OPEN) }, { CLOSE, QVariant(DEFAULT_CLOSE) } } }; + wasMalformed = true; + } + if (weekendHours->isNull()) { + *weekendHours = QVariantList{ QVariantMap{ { OPEN, QVariant(DEFAULT_OPEN) }, { CLOSE, QVariant(DEFAULT_CLOSE) } } }; + wasMalformed = true; + } + if (utcOffset->isNull()) { + *utcOffset = QVariant(QTimeZone::systemTimeZone().offsetFromUtc(QDateTime::currentDateTime()) / (float)SECS_PER_HOUR); + wasMalformed = true; + } + + if (wasMalformed) { + // write the new settings to file + persistToFile(); + + // reload the master and user config so the merged config is correct + _configMap.loadMasterAndUserConfig(_argumentList); + } +} + void DomainServerSettingsManager::packPermissionsForMap(QString mapName, NodePermissionsMap& agentPermissions, QString keyPath) { diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index ec1d3b637d..66f1a83500 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -41,6 +41,8 @@ public: QVariantMap& getUserSettingsMap() { return _configMap.getUserConfig(); } QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); } + QVariantMap& getDescriptorsMap(); + bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name); } bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name); } NodePermissions getStandardPermissionsForName(const QString& name) const; @@ -72,6 +74,8 @@ private: friend class DomainServer; + void validateDescriptorsMap(); + void packPermissionsForMap(QString mapName, NodePermissionsMap& agentPermissions, QString keyPath); void packPermissions(); void unpackPermissions(); diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index ae84705da3..cf5a2b60ad 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -139,7 +139,7 @@ link_hifi_libraries(shared octree gpu gl gpu-gl procedural model render recording fbx networking model-networking entities avatars audio audio-client animation script-engine physics render-utils entities-renderer ui auto-updater - controllers plugins display-plugins input-plugins steamworks-wrapper) + controllers plugins ui-plugins display-plugins input-plugins steamworks-wrapper) # include the binary directory of render-utils for shader includes target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/libraries/render-utils") diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index dc3ca3755e..6be672900a 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -1,18 +1,26 @@ { "name": "Vive to Standard", "channels": [ - { "from": "Vive.LY", "when": "Vive.LSOuter", "filters": ["invert"], "to": "Standard.LY" }, - { "from": "Vive.LX", "when": "Vive.LSOuter", "to": "Standard.LX" }, - - { "from": "Vive.LT", "to": "Standard.LT" }, + { "from": "Vive.LY", "when": "Vive.LSY", "filters": ["invert"], "to": "Standard.LY" }, + { "from": "Vive.LX", "when": "Vive.LSX", "to": "Standard.LX" }, + { + "from": "Vive.LT", "to": "Standard.LT", + "filters": [ + { "type": "deadZone", "min": 0.05 } + ] + }, { "from": "Vive.LeftGrip", "to": "Standard.LeftGrip" }, { "from": "Vive.LS", "to": "Standard.LS" }, { "from": "Vive.LSTouch", "to": "Standard.LSTouch" }, - { "from": "Vive.RY", "when": "Vive.RSOuter", "filters": ["invert"], "to": "Standard.RY" }, - { "from": "Vive.RX", "when": "Vive.RSOuter", "to": "Standard.RX" }, - - { "from": "Vive.RT", "to": "Standard.RT" }, + { "from": "Vive.RY", "when": "Vive.RSY", "filters": ["invert"], "to": "Standard.RY" }, + { "from": "Vive.RX", "when": "Vive.RSX", "to": "Standard.RX" }, + { + "from": "Vive.RT", "to": "Standard.RT", + "filters": [ + { "type": "deadZone", "min": 0.05 } + ] + }, { "from": "Vive.RightGrip", "to": "Standard.RightGrip" }, { "from": "Vive.RS", "to": "Standard.RS" }, { "from": "Vive.RSTouch", "to": "Standard.RSTouch" }, diff --git a/interface/resources/icons/hud-01.svg b/interface/resources/icons/hud-01.svg new file mode 100644 index 0000000000..4929389268 --- /dev/null +++ b/interface/resources/icons/hud-01.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 7f107e44e9..a48804faba 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -20,9 +20,10 @@ Window { objectName: "AddressBarDialog" frame: HiddenFrame {} + hideBackground: true - visible: false - destroyOnInvisible: false + shown: false + destroyOnHidden: false resizable: false scale: 1.25 // Make this dialog a little larger than normal @@ -145,14 +146,14 @@ Window { if (addressLine.text !== "") { addressBarDialog.loadAddress(addressLine.text) } - root.visible = false; + root.shown = false; } Keys.onPressed: { switch (event.key) { case Qt.Key_Escape: case Qt.Key_Back: - root.visible = false + root.shown = false event.accepted = true break case Qt.Key_Enter: diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index 6d2e8e7ba0..c9b6305258 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -15,15 +15,15 @@ import Qt.labs.settings 1.0 import "styles-uit" import "controls-uit" as HifiControls -import "windows-uit" +import "windows" import "dialogs" -Window { +ScrollingWindow { id: root objectName: "AssetServer" title: "Asset Browser" resizable: true - destroyOnInvisible: true + destroyOnHidden: true implicitWidth: 384; implicitHeight: 640 minSize: Qt.vector2d(200, 300) diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 89ab333a0d..8c8cf05444 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -2,22 +2,24 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 import QtWebEngine 1.1 -import "controls" -import "styles" +import "controls-uit" +import "styles-uit" import "windows" -Window { +ScrollingWindow { id: root HifiConstants { id: hifi } title: "Browser" resizable: true - destroyOnInvisible: true + destroyOnHidden: true width: 800 height: 600 property alias webView: webview - + x: 100 + y: 100 + Component.onCompleted: { - visible = true + shown = true addressBar.text = webview.url } @@ -30,15 +32,9 @@ Window { Item { id:item - anchors.fill: parent - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: webview.top - color: "white" - } - + width: pane.contentWidth + implicitHeight: pane.scrollHeight + Row { id: buttons spacing: 4 @@ -46,25 +42,37 @@ Window { anchors.topMargin: 8 anchors.left: parent.left anchors.leftMargin: 8 - FontAwesome { - id: back; text: "\uf0a8"; size: 48; enabled: webview.canGoBack; + HiFiGlyphs { + id: back; + enabled: webview.canGoBack; + text: hifi.glyphs.backward color: enabled ? hifi.colors.text : hifi.colors.disabledText + size: 48 MouseArea { anchors.fill: parent; onClicked: webview.goBack() } } - FontAwesome { - id: forward; text: "\uf0a9"; size: 48; enabled: webview.canGoForward; + + HiFiGlyphs { + id: forward; + enabled: webview.canGoForward; + text: hifi.glyphs.forward color: enabled ? hifi.colors.text : hifi.colors.disabledText - MouseArea { anchors.fill: parent; onClicked: webview.goBack() } + size: 48 + MouseArea { anchors.fill: parent; onClicked: webview.goForward() } } - FontAwesome { - id: reload; size: 48; text: webview.loading ? "\uf057" : "\uf021" - MouseArea { anchors.fill: parent; onClicked: webview.loading ? webview.stop() : webview.reload() } + + HiFiGlyphs { + id: reload; + enabled: webview.canGoForward; + text: webview.loading ? hifi.glyphs.close : hifi.glyphs.reload + color: enabled ? hifi.colors.text : hifi.colors.disabledText + size: 48 + MouseArea { anchors.fill: parent; onClicked: webview.goForward() } } } - Border { + Item { + id: border height: 48 - radius: 8 anchors.top: parent.top anchors.topMargin: 8 anchors.right: parent.right @@ -86,15 +94,18 @@ Window { onSourceChanged: console.log("Icon url: " + source) } } - - TextInput { + + TextField { id: addressBar anchors.right: parent.right anchors.rightMargin: 8 anchors.left: barIcon.right anchors.leftMargin: 0 anchors.verticalCenter: parent.verticalCenter - + focus: true + colorScheme: hifi.colorSchemes.dark + placeholderText: "Enter URL" + Component.onCompleted: scriptsModel.filterRegExp = new RegExp("^.*$", "i") Keys.onPressed: { switch(event.key) { case Qt.Key_Enter: @@ -110,7 +121,7 @@ Window { } } - WebView { + WebEngineView { id: webview url: "http://highfidelity.com" anchors.top: buttons.bottom @@ -119,7 +130,7 @@ Window { anchors.left: parent.left anchors.right: parent.right onLoadingChanged: { - if (loadRequest.status == WebEngineView.LoadSucceededStatus) { + if (loadRequest.status === WebEngineView.LoadSucceededStatus) { addressBar.text = loadRequest.url } } @@ -127,7 +138,7 @@ Window { console.log("New icon: " + icon) } - profile: desktop.browserProfile + //profile: desktop.browserProfile } diff --git a/interface/resources/qml/InfoView.qml b/interface/resources/qml/InfoView.qml index c5dba7e1f3..f18969fb2f 100644 --- a/interface/resources/qml/InfoView.qml +++ b/interface/resources/qml/InfoView.qml @@ -12,9 +12,9 @@ import QtQuick 2.5 import Hifi 1.0 as Hifi import "controls-uit" -import "windows-uit" as Windows +import "windows" as Windows -Windows.Window { +Windows.ScrollingWindow { id: root width: 800 height: 800 diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index 1b25b75608..f75e83e36e 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -14,7 +14,7 @@ import "controls" import "styles" import "windows" -Window { +ScrollingWindow { id: root HifiConstants { id: hifi } objectName: "LoginDialog" @@ -22,8 +22,9 @@ Window { width: loginDialog.implicitWidth // FIXME make movable anchors.centerIn: parent - destroyOnInvisible: false - visible: false + destroyOnHidden: false + hideBackground: true + shown: false LoginDialog { id: loginDialog @@ -268,8 +269,8 @@ Window { } } - onVisibleChanged: { - if (!visible) { + onShownChanged: { + if (!shown) { username.text = "" password.text = "" loginDialog.statusText = "" @@ -282,7 +283,7 @@ Window { switch (event.key) { case Qt.Key_Escape: case Qt.Key_Back: - root.visible = false; + root.shown = false; event.accepted = true; break; diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index ae052879db..542b44b95e 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -13,16 +13,16 @@ import QtQuick.Controls 1.4 import QtWebEngine 1.1 import QtWebChannel 1.0 -import "windows-uit" as Windows +import "windows" as Windows import "controls-uit" as Controls import "styles-uit" -Windows.Window { +Windows.ScrollingWindow { id: root HifiConstants { id: hifi } title: "WebWindow" resizable: true - visible: false + shown: false // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false property alias source: webview.url diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index 0420cd2e88..7be747a3ad 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -14,7 +14,7 @@ Windows.Window { HifiConstants { id: hifi } title: "QmlWindow" resizable: true - visible: false + shown: false focus: true property var channel; // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer diff --git a/interface/resources/qml/ToolWindow.qml b/interface/resources/qml/ToolWindow.qml index aaff43b146..bbfc74493d 100644 --- a/interface/resources/qml/ToolWindow.qml +++ b/interface/resources/qml/ToolWindow.qml @@ -15,18 +15,18 @@ import QtWebEngine 1.1 import QtWebChannel 1.0 import Qt.labs.settings 1.0 -import "windows-uit" +import "windows" import "controls-uit" import "styles-uit" -Window { +ScrollingWindow { id: toolWindow resizable: true objectName: "ToolWindow" destroyOnCloseButton: false - destroyOnInvisible: false + destroyOnHidden: false closable: true - visible: false + shown: false title: "Edit" property alias tabView: tabView implicitWidth: 520; implicitHeight: 695 @@ -142,7 +142,7 @@ Window { return; } } - visible = false; + shown = false; } function findIndexForUrl(source) { @@ -172,7 +172,7 @@ Window { var tab = tabView.getTab(index); if (newVisible) { - toolWindow.visible = true + toolWindow.shown = true tab.enabled = true } else { tab.enabled = false; diff --git a/interface/resources/qml/UpdateDialog.qml b/interface/resources/qml/UpdateDialog.qml index 4cb5b206c6..91dc210eda 100644 --- a/interface/resources/qml/UpdateDialog.qml +++ b/interface/resources/qml/UpdateDialog.qml @@ -3,11 +3,11 @@ import QtQuick 2.3 import QtQuick.Controls 1.3 import QtQuick.Controls.Styles 1.3 import QtGraphicalEffects 1.0 -import "controls" -import "styles" +import "controls-uit" +import "styles-uit" import "windows" -Window { +ScrollingWindow { id: root HifiConstants { id: hifi } objectName: "UpdateDialog" diff --git a/interface/resources/qml/controls-uit/AttachmentsTable.qml b/interface/resources/qml/controls-uit/AttachmentsTable.qml index ce93b8f4df..7d0280b72d 100644 --- a/interface/resources/qml/controls-uit/AttachmentsTable.qml +++ b/interface/resources/qml/controls-uit/AttachmentsTable.qml @@ -15,7 +15,7 @@ import QtQuick.XmlListModel 2.0 import "../styles-uit" import "../controls-uit" as HifiControls -import "../windows-uit" +import "../windows" import "../hifi/models" TableView { diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 62a72e3d8c..27fa9692b9 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -24,6 +24,13 @@ FocusScope { readonly property int invalid_position: -9999; property rect recommendedRect: Qt.rect(0,0,0,0); property var expectedChildren; + property bool repositionLocked: true + + onRepositionLockedChanged: { + if (!repositionLocked) { + d.handleSizeChanged(); + } + } onHeightChanged: d.handleSizeChanged(); @@ -56,6 +63,10 @@ FocusScope { id: d function handleSizeChanged() { + if (desktop.repositionLocked) { + return; + } + var oldRecommendedRect = recommendedRect; var newRecommendedRectJS = (typeof Controller === "undefined") ? Qt.rect(0,0,0,0) : Controller.getRecommendedOverlayRect(); var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y, @@ -64,7 +75,7 @@ FocusScope { var oldChildren = expectedChildren; var newChildren = d.getRepositionChildren(); - if (oldRecommendedRect != Qt.rect(0,0,0,0) + if (oldRecommendedRect != Qt.rect(0,0,0,0) && oldRecommendedRect != Qt.rect(0,0,1,1) && (oldRecommendedRect != newRecommendedRect || oldChildren != newChildren) ) { @@ -93,6 +104,17 @@ FocusScope { return item; } + function findMatchingChildren(item, predicate) { + var results = []; + for (var i in item.children) { + var child = item.children[i]; + if (predicate(child)) { + results.push(child); + } + } + return results; + } + function isTopLevelWindow(item) { return item.topLevelWindow; } @@ -106,19 +128,9 @@ FocusScope { } function getTopLevelWindows(predicate) { - var currentWindows = []; - if (!desktop) { - console.log("Could not find desktop for " + item) - return currentWindows; - } - - for (var i = 0; i < desktop.children.length; ++i) { - var child = desktop.children[i]; - if (isTopLevelWindow(child) && (!predicate || predicate(child))) { - currentWindows.push(child) - } - } - return currentWindows; + return findMatchingChildren(desktop, function(child) { + return (isTopLevelWindow(child) && (!predicate || predicate(child))); + }); } function getDesktopWindow(item) { @@ -227,22 +239,16 @@ FocusScope { } function getRepositionChildren(predicate) { - var currentWindows = []; - if (!desktop) { - console.log("Could not find desktop"); - return currentWindows; - } - - for (var i = 0; i < desktop.children.length; ++i) { - var child = desktop.children[i]; - if (child.shouldReposition === true && (!predicate || predicate(child))) { - currentWindows.push(child) - } - } - return currentWindows; + return findMatchingChildren(desktop, function(child) { + return (child.shouldReposition === true && (!predicate || predicate(child))); + }); } function repositionAll() { + if (desktop.repositionLocked) { + return; + } + var oldRecommendedRect = recommendedRect; var oldRecommendedDimmensions = { x: oldRecommendedRect.width, y: oldRecommendedRect.height }; var newRecommendedRect = Controller.getRecommendedOverlayRect(); @@ -265,6 +271,63 @@ FocusScope { } } + property bool pinned: false + property var hiddenChildren: [] + + function togglePinned() { + pinned = !pinned + } + + function setPinned(newPinned) { + pinned = newPinned + } + + property real unpinnedAlpha: 1.0; + + Behavior on unpinnedAlpha { + NumberAnimation { + easing.type: Easing.Linear; + duration: 300 + } + } + + state: "NORMAL" + states: [ + State { + name: "NORMAL" + PropertyChanges { target: desktop; unpinnedAlpha: 1.0 } + }, + State { + name: "PINNED" + PropertyChanges { target: desktop; unpinnedAlpha: 0.0 } + } + ] + + transitions: [ + Transition { + NumberAnimation { properties: "unpinnedAlpha"; duration: 300 } + } + ] + + onPinnedChanged: { + if (pinned) { + nullFocus.focus = true; + nullFocus.forceActiveFocus(); + + // recalculate our non-pinned children + hiddenChildren = d.findMatchingChildren(desktop, function(child){ + return !d.isTopLevelWindow(child) && child.visible && !child.pinned; + }); + + hiddenChildren.forEach(function(child){ + child.opacity = Qt.binding(function(){ return desktop.unpinnedAlpha }); + }); + } + state = pinned ? "PINNED" : "NORMAL" + } + + onShowDesktop: pinned = false + function raise(item) { var targetWindow = d.getDesktopWindow(item); if (!targetWindow) { @@ -422,7 +485,6 @@ FocusScope { event.accepted = false; } - function unfocusWindows() { var windows = d.getTopLevelWindows(); for (var i = 0; i < windows.length; ++i) { @@ -433,6 +495,8 @@ FocusScope { FocusHack { id: focusHack; } + FocusScope { id: nullFocus; } + Rectangle { id: focusDebugger; objectName: "focusDebugger" diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index f57d20de51..5372028da5 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -18,7 +18,7 @@ import QtQuick.Dialogs 1.2 as OriginalDialogs import ".." import "../controls-uit" import "../styles-uit" -import "../windows-uit" +import "../windows" import "fileDialog" @@ -729,7 +729,7 @@ ModalWindow { Action { id: cancelAction text: "Cancel" - onTriggered: { canceled(); root.visible = false; } + onTriggered: { canceled(); root.shown = false; } } } diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index 30f492e36a..d390ea08bf 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -14,7 +14,7 @@ import QtQuick.Dialogs 1.2 as OriginalDialogs import "../controls-uit" import "../styles-uit" -import "../windows-uit" +import "../windows" import "messageDialog" @@ -24,7 +24,7 @@ ModalWindow { implicitWidth: 640 implicitHeight: 320 destroyOnCloseButton: true - destroyOnInvisible: true + destroyOnHidden: true visible: true signal selected(int button); diff --git a/interface/resources/qml/dialogs/PreferencesDialog.qml b/interface/resources/qml/dialogs/PreferencesDialog.qml index 40cc713397..5278118a22 100644 --- a/interface/resources/qml/dialogs/PreferencesDialog.qml +++ b/interface/resources/qml/dialogs/PreferencesDialog.qml @@ -13,14 +13,14 @@ import QtQuick.Controls 1.4 import "../controls-uit" as HifiControls import "../styles-uit" -import "../windows-uit" +import "../windows" import "preferences" -Window { +ScrollingWindow { id: root title: "Preferences" resizable: true - destroyOnInvisible: true + destroyOnHidden: true width: 500 height: 577 property var sections: [] diff --git a/interface/resources/qml/dialogs/QueryDialog.qml b/interface/resources/qml/dialogs/QueryDialog.qml index 0c7772dc94..05cb347169 100644 --- a/interface/resources/qml/dialogs/QueryDialog.qml +++ b/interface/resources/qml/dialogs/QueryDialog.qml @@ -14,7 +14,7 @@ import QtQuick.Dialogs 1.2 as OriginalDialogs import "../controls-uit" import "../styles-uit" -import "../windows-uit" +import "../windows" ModalWindow { id: root diff --git a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml index e5bc9b80ef..16d25b3c4c 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarBrowser.qml @@ -12,7 +12,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtWebEngine 1.1 -import "../../windows-uit" as Windows +import "../../windows" as Windows import "../../controls-uit" as Controls import "../../styles-uit" @@ -23,15 +23,10 @@ Windows.Window { resizable: true modality: Qt.ApplicationModal - Item { - width: pane.contentWidth - implicitHeight: pane.scrollHeight - - Controls.WebView { - id: webview - anchors.fill: parent - url: "https://metaverse.highfidelity.com/marketplace?category=avatars" - focus: true - } + Controls.WebView { + id: webview + anchors.fill: parent + url: "https://metaverse.highfidelity.com/marketplace?category=avatars" + focus: true } } diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 59278a17b4..169542c0f0 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -1,14 +1,18 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtWebEngine 1.1; +import Qt.labs.settings 1.0 -import "../desktop" +import "../desktop" as OriginalDesktop import ".." +import "." +import "./toolbars" -Desktop { +OriginalDesktop.Desktop { id: desktop MouseArea { + id: hoverWatch anchors.fill: parent hoverEnabled: true propagateComposedEvents: true @@ -18,13 +22,6 @@ Desktop { acceptedButtons: Qt.NoButton } - Component.onCompleted: { - WebEngine.settings.javascriptCanOpenWindows = true; - WebEngine.settings.javascriptCanAccessClipboard = false; - WebEngine.settings.spatialNavigationEnabled = false; - WebEngine.settings.localContentCanAccessRemoteUrls = true; - } - // The tool window, one instance property alias toolWindow: toolWindow ToolWindow { id: toolWindow } @@ -47,7 +44,42 @@ Desktop { } } + property var toolbars: ({}) + Component { id: toolbarBuilder; Toolbar { } } + + Component.onCompleted: { + WebEngine.settings.javascriptCanOpenWindows = true; + WebEngine.settings.javascriptCanAccessClipboard = false; + WebEngine.settings.spatialNavigationEnabled = false; + WebEngine.settings.localContentCanAccessRemoteUrls = true; + + var sysToolbar = desktop.getToolbar("com.highfidelity.interface.toolbar.system"); + var toggleHudButton = sysToolbar.addButton({ + objectName: "hudToggle", + imageURL: "../../../icons/hud-01.svg", + visible: true, + pinned: true, + }); + + toggleHudButton.yOffset = Qt.binding(function(){ + return desktop.pinned ? 50 : 0 + }); + toggleHudButton.clicked.connect(function(){ + console.log("Clicked on hud button") + var overlayMenuItem = "Overlays" + MenuInterface.setIsOptionChecked(overlayMenuItem, !MenuInterface.isOptionChecked(overlayMenuItem)); + }); + } + + // Create or fetch a toolbar with the given name + function getToolbar(name) { + var result = toolbars[name]; + if (!result) { + result = toolbars[name] = toolbarBuilder.createObject(desktop, {}); + result.objectName = name; + } + return result; + } } - diff --git a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml index 437e02e149..15467f8021 100755 --- a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml +++ b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml @@ -6,17 +6,17 @@ import QtQuick.Controls.Styles 1.4 import "../../styles-uit" import "../../controls-uit" as HifiControls -import "../../windows-uit" +import "../../windows" import "attachments" -Window { +ScrollingWindow { id: root title: "Attachments" objectName: "AttachmentsDialog" width: 600 height: 600 resizable: true - destroyOnInvisible: true + destroyOnHidden: true minSize: Qt.vector2d(400, 500) HifiConstants { id: hifi } diff --git a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml index b2de108545..aeffb8e4bf 100644 --- a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml +++ b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml @@ -9,9 +9,9 @@ import "../models" import "../../styles-uit" import "../../controls-uit" as HifiControls -import "../../windows-uit" +import "../../windows" -Window { +ScrollingWindow { id: root resizable: true width: 600 diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index 94b8c1905f..5457caccf1 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -15,14 +15,14 @@ import Qt.labs.settings 1.0 import "../../styles-uit" import "../../controls-uit" as HifiControls -import "../../windows-uit" +import "../../windows" -Window { +ScrollingWindow { id: root objectName: "RunningScripts" title: "Running Scripts" resizable: true - destroyOnInvisible: true + destroyOnHidden: true implicitWidth: 424 implicitHeight: isHMD ? 695 : 728 minSize: Qt.vector2d(424, 300) @@ -34,6 +34,9 @@ Window { property var runningScriptsModel: ListModel { } property bool isHMD: false + onVisibleChanged: console.log("Running scripts visible changed to " + visible) + onShownChanged: console.log("Running scripts visible changed to " + visible) + Settings { category: "Overlay.RunningScripts" property alias x: root.x diff --git a/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml b/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml index f99b770a78..3dacb3b39c 100644 --- a/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml +++ b/interface/resources/qml/hifi/dialogs/SnapshotShareDialog.qml @@ -7,7 +7,7 @@ import "../../windows" import "../../js/Utils.js" as Utils import "../models" -Window { +ScrollingWindow { id: root resizable: true width: 516 diff --git a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml index 1277c459ce..04e3934535 100755 --- a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml +++ b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml @@ -8,7 +8,7 @@ import "." import ".." import "../../../styles-uit" import "../../../controls-uit" as HifiControls -import "../../../windows-uit" +import "../../../windows" Item { height: column.height + 2 * 8 diff --git a/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml b/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml index e1d7b6d4a3..3d109cc2a5 100644 --- a/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml +++ b/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml @@ -3,7 +3,7 @@ import QtQuick.Controls 1.4 import "../../../styles-uit" import "../../../controls-uit" as HifiControls -import "../../../windows-uit" +import "../../../windows" Item { id: root diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml new file mode 100644 index 0000000000..75c06e4199 --- /dev/null +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -0,0 +1,151 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 + +import "../../windows" +import "." + +Window { + id: window + frame: ToolFrame { + horizontalSpacers: horizontal + verticalSpacers: !horizontal + } + hideBackground: true + resizable: false + destroyOnCloseButton: false + destroyOnHidden: false + closable: false + shown: true + width: content.width + height: content.height + visible: true + // Disable this window from being able to call 'desktop.raise() and desktop.showDesktop' + activator: Item {} + property bool horizontal: true + property real buttonSize: 50; + property var buttons: [] + property var container: horizontal ? row : column + + Settings { + category: "toolbar/" + window.objectName + property alias x: window.x + property alias y: window.y + } + + onHorizontalChanged: { + var newParent = horizontal ? row : column; + for (var i in buttons) { + var child = buttons[i]; + child.parent = newParent; + if (horizontal) { + child.y = 0 + } else { + child.x = 0 + } + } + } + + Item { + id: content + implicitHeight: horizontal ? row.height : column.height + implicitWidth: horizontal ? row.width : column.width + + Row { + id: row + spacing: 6 + } + + Column { + id: column + spacing: 6 + } + + Component { id: toolbarButtonBuilder; ToolbarButton { } } + + Connections { + target: desktop + onPinnedChanged: { + if (!window.pinned) { + return; + } + var newPinned = desktop.pinned; + for (var i in buttons) { + var child = buttons[i]; + if (desktop.pinned) { + if (!child.pinned) { + child.visible = false; + } + } else { + child.visible = true; + } + } + } + } + } + + + function findButtonIndex(name) { + if (!name) { + return -1; + } + + for (var i in buttons) { + var child = buttons[i]; + if (child.objectName === name) { + return i; + } + } + return -1; + } + + function findButton(name) { + var index = findButtonIndex(name); + if (index < 0) { + return; + } + return buttons[index]; + } + + function addButton(properties) { + properties = properties || {} + + // If a name is specified, then check if there's an existing button with that name + // and return it if so. This will allow multiple clients to listen to a single button, + // and allow scripts to be idempotent so they don't duplicate buttons if they're reloaded + var result = findButton(properties.objectName); + if (result) { + return result; + } + properties.toolbar = this; + properties.opacity = 0; + result = toolbarButtonBuilder.createObject(container, properties); + buttons.push(result); + result.opacity = 1; + updatePinned(); + return result; + } + + function removeButton(name) { + var index = findButtonIndex(name); + if (index < -1) { + console.warn("Tried to remove non-existent button " + name); + return; + } + buttons[index].destroy(); + buttons.splice(index, 1); + updatePinned(); + } + + function updatePinned() { + var newPinned = false; + for (var i in buttons) { + var child = buttons[i]; + if (child.pinned) { + newPinned = true; + break; + } + } + pinned = newPinned; + } +} diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml new file mode 100644 index 0000000000..a3be4533d2 --- /dev/null +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -0,0 +1,65 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Item { + id: button + property alias imageURL: image.source + property alias alpha: button.opacity + property var subImage; + property int yOffset: 0 + property int buttonState: 0 + property var toolbar; + property real size: 50 // toolbar ? toolbar.buttonSize : 50 + width: size; height: size + property bool pinned: false + clip: true + + Behavior on opacity { + NumberAnimation { + duration: 150 + easing.type: Easing.InOutCubic + } + } + + property alias fadeTargetProperty: button.opacity + + onFadeTargetPropertyChanged: { + visible = (fadeTargetProperty !== 0.0); + } + + onVisibleChanged: { + if ((!visible && fadeTargetProperty != 0.0) || (visible && fadeTargetProperty == 0.0)) { + var target = visible; + visible = !visible; + fadeTargetProperty = target ? 1.0 : 0.0; + return; + } + } + + + onButtonStateChanged: { + yOffset = size * buttonState + } + + Component.onCompleted: { + if (subImage) { + if (subImage.y) { + yOffset = subImage.y; + } + } + } + + signal clicked() + + Image { + id: image + y: -button.yOffset; + width: parent.width + } + + MouseArea { + anchors.fill: parent + onClicked: button.clicked(); + } +} + diff --git a/interface/resources/qml/menus/MenuMouseHandler.qml b/interface/resources/qml/menus/MenuMouseHandler.qml index 9ba158cb28..48574d41e5 100644 --- a/interface/resources/qml/menus/MenuMouseHandler.qml +++ b/interface/resources/qml/menus/MenuMouseHandler.qml @@ -39,6 +39,19 @@ Item { onSelected: d.handleSelection(subMenu, currentItem, item) } } + property var delay: Timer { // No setTimeout in QML. + property var menuItem: null; + interval: 0 + repeat: false + running: false + function trigger(item) { // Capture item and schedule asynchronous Timer. + menuItem = item; + start(); + } + onTriggered: { + menuItem.trigger(); // Now trigger the item. + } + } function toModel(items) { var result = modelMaker.createObject(desktop); @@ -128,7 +141,8 @@ Item { case MenuItemType.Item: console.log("Triggering " + item.text) - item.trigger(); + // Don't block waiting for modal dialogs and such that the menu might open. + delay.trigger(item); clearMenus(); break; } diff --git a/interface/resources/qml/windows-uit/DefaultFrame.qml b/interface/resources/qml/windows-uit/DefaultFrame.qml deleted file mode 100644 index 84f435480b..0000000000 --- a/interface/resources/qml/windows-uit/DefaultFrame.qml +++ /dev/null @@ -1,119 +0,0 @@ -// -// DefaultFrame.qml -// -// Created by Bradley Austin Davis on 12 Jan 2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtGraphicalEffects 1.0 - -import "." -import "../styles-uit" - -Frame { - HifiConstants { id: hifi } - - Rectangle { - // Dialog frame - id: frameContent - - readonly property int iconSize: hifi.dimensions.frameIconSize - readonly property int frameMargin: 9 - readonly property int frameMarginLeft: frameMargin - readonly property int frameMarginRight: frameMargin - readonly property int frameMarginTop: 2 * frameMargin + iconSize - readonly property int frameMarginBottom: iconSize + 11 - - anchors { - topMargin: -frameMarginTop - leftMargin: -frameMarginLeft - rightMargin: -frameMarginRight - bottomMargin: -frameMarginBottom - } - anchors.fill: parent - color: hifi.colors.baseGrayHighlight40 - border { - width: hifi.dimensions.borderWidth - color: hifi.colors.faintGray50 - } - radius: hifi.dimensions.borderRadius - - // Enable dragging of the window - MouseArea { - anchors.fill: parent - drag.target: window - } - - Row { - id: controlsRow - anchors { - right: parent.right; - top: parent.top; - topMargin: frameContent.frameMargin + 1 // Move down a little to visually align with the title - rightMargin: frameContent.frameMarginRight; - } - spacing: frameContent.iconSize / 4 - - HiFiGlyphs { - // "Pin" button - visible: false - text: (frame.pinned && !pinClickArea.containsMouse) || (!frame.pinned && pinClickArea.containsMouse) ? hifi.glyphs.pinInverted : hifi.glyphs.pin - color: pinClickArea.containsMouse && !pinClickArea.pressed ? hifi.colors.redHighlight : hifi.colors.white - size: frameContent.iconSize - MouseArea { - id: pinClickArea - anchors.fill: parent - hoverEnabled: true - propagateComposedEvents: true - onClicked: { frame.pin(); mouse.accepted = false; } - } - } - - HiFiGlyphs { - // "Close" button - visible: window ? window.closable : false - text: closeClickArea.containsPress ? hifi.glyphs.closeInverted : hifi.glyphs.close - color: closeClickArea.containsMouse ? hifi.colors.redHighlight : hifi.colors.white - size: frameContent.iconSize - MouseArea { - id: closeClickArea - anchors.fill: parent - hoverEnabled: true - onClicked: window.visible = false; - } - } - } - - RalewayRegular { - // Title - id: titleText - anchors { - left: parent.left - leftMargin: frameContent.frameMarginLeft + hifi.dimensions.contentMargin.x - right: controlsRow.left - rightMargin: frameContent.iconSize - top: parent.top - topMargin: frameContent.frameMargin - } - text: window ? window.title : "" - color: hifi.colors.white - size: hifi.fontSizes.overlayTitle - } - - DropShadow { - source: titleText - anchors.fill: titleText - horizontalOffset: 2 - verticalOffset: 2 - samples: 2 - color: hifi.colors.baseGrayShadow60 - visible: (window && window.focus) - cached: true - } - } -} - diff --git a/interface/resources/qml/windows-uit/Fadable.qml b/interface/resources/qml/windows-uit/Fadable.qml deleted file mode 100644 index 34990c2147..0000000000 --- a/interface/resources/qml/windows-uit/Fadable.qml +++ /dev/null @@ -1,60 +0,0 @@ -// -// Fadable.qml -// -// Created by Bradley Austin Davis on 15 Jan 2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtGraphicalEffects 1.0 - -import "../styles-uit" - -// Enable window visibility transitions -FocusScope { - id: root - HifiConstants { id: hifi } - - Component.onCompleted: { - fadeTargetProperty = visible ? 1.0 : 0.0 - } - - // The target property to animate, usually scale or opacity - property alias fadeTargetProperty: root.opacity - // always start the property at 0 to enable fade in on creation - fadeTargetProperty: 0 - // DO NOT set visible to false or when derived types override it it - // will short circuit the fade in on initial visibility - // visible: false <--- NO - - // Some dialogs should be destroyed when they become - // invisible, so handle that - onVisibleChanged: { - // If someone directly set the visibility to false - // toggle it back on and use the targetVisible flag to transition - // via fading. - if ((!visible && fadeTargetProperty != 0.0) || (visible && fadeTargetProperty == 0.0)) { - var target = visible; - visible = !visible; - fadeTargetProperty = target ? 1.0 : 0.0; - return; - } - } - - // The actual animator - Behavior on fadeTargetProperty { - NumberAnimation { - duration: hifi.effects.fadeInDuration - easing.type: Easing.InOutCubic - } - } - - // Once we're transparent, disable the dialog's visibility - onFadeTargetPropertyChanged: { - visible = (fadeTargetProperty != 0.0); - } -} diff --git a/interface/resources/qml/windows-uit/Frame.qml b/interface/resources/qml/windows-uit/Frame.qml deleted file mode 100644 index 9519a44cf0..0000000000 --- a/interface/resources/qml/windows-uit/Frame.qml +++ /dev/null @@ -1,133 +0,0 @@ -// -// Frame.qml -// -// Created by Bradley Austin Davis on 12 Jan 2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtGraphicalEffects 1.0 - -import "../styles-uit" -import "../js/Utils.js" as Utils - -Item { - id: frame - HifiConstants { id: hifi } - - default property var decoration - - property bool gradientsSupported: desktop.gradientsSupported - - readonly property int frameMarginLeft: frameContent.frameMarginLeft - readonly property int frameMarginRight: frameContent.frameMarginRight - readonly property int frameMarginTop: frameContent.frameMarginTop - readonly property int frameMarginBottom: frameContent.frameMarginBottom - - // Frames always fill their parents, but their decorations may extend - // beyond the window via negative margin sizes - anchors.fill: parent - - children: [ - focusShadow, - decoration, - sizeOutline, - debugZ, - sizeDrag - ] - - Text { - id: debugZ - visible: DebugQML - text: window ? "Z: " + window.z : "" - y: window ? window.height + 4 : 0 - } - - function deltaSize(dx, dy) { - var newSize = Qt.vector2d(window.width + dx, window.height + dy); - newSize = Utils.clampVector(newSize, window.minSize, window.maxSize); - window.width = newSize.x - window.height = newSize.y - } - - RadialGradient { - id: focusShadow - width: 1.66 * window.width - height: 1.66 * window.height - x: (window.width - width) / 2 - y: window.height / 2 - 0.375 * height - visible: gradientsSupported && window && window.focus && pane.visible - gradient: Gradient { - // GradientStop position 0.5 is at full circumference of circle that fits inside the square. - GradientStop { position: 0.0; color: "#ff000000" } // black, 100% opacity - GradientStop { position: 0.333; color: "#1f000000" } // black, 12% opacity - GradientStop { position: 0.5; color: "#00000000" } // black, 0% opacity - GradientStop { position: 1.0; color: "#00000000" } - } - cached: true - } - - Rectangle { - id: sizeOutline - x: -frameMarginLeft - y: -frameMarginTop - width: window ? window.width + frameMarginLeft + frameMarginRight + 2 : 0 - height: window ? window.height + frameMarginTop + frameMarginBottom + 2 : 0 - color: hifi.colors.baseGrayHighlight15 - border.width: 3 - border.color: hifi.colors.white50 - radius: hifi.dimensions.borderRadius - visible: window ? !pane.visible : false - } - - MouseArea { - // Resize handle - id: sizeDrag - width: hifi.dimensions.frameIconSize - height: hifi.dimensions.frameIconSize - enabled: window ? window.resizable : false - hoverEnabled: true - x: window ? window.width + frameMarginRight - hifi.dimensions.frameIconSize : 0 - y: window ? window.height + 4 : 0 - property vector2d pressOrigin - property vector2d sizeOrigin - property bool hid: false - onPressed: { - //console.log("Pressed on size") - pressOrigin = Qt.vector2d(mouseX, mouseY) - sizeOrigin = Qt.vector2d(window.content.width, window.content.height) - hid = false; - } - onReleased: { - if (hid) { - pane.visible = true - frameContent.visible = true - hid = false; - } - } - onPositionChanged: { - if (pressed) { - if (pane.visible) { - pane.visible = false; - frameContent.visible = false - hid = true; - } - var delta = Qt.vector2d(mouseX, mouseY).minus(pressOrigin); - frame.deltaSize(delta.x, delta.y) - } - } - HiFiGlyphs { - visible: sizeDrag.enabled - x: -11 // Move a little to visually align - y: window.modality == Qt.ApplicationModal ? -6 : -4 - text: hifi.glyphs.resizeHandle - size: hifi.dimensions.frameIconSize + 10 - color: sizeDrag.containsMouse || sizeDrag.pressed - ? hifi.colors.white - : (window.colorScheme == hifi.colorSchemes.dark ? hifi.colors.white50 : hifi.colors.lightGrayText80) - } - } -} diff --git a/interface/resources/qml/windows-uit/ModalFrame.qml b/interface/resources/qml/windows-uit/ModalFrame.qml deleted file mode 100644 index 211353b5f3..0000000000 --- a/interface/resources/qml/windows-uit/ModalFrame.qml +++ /dev/null @@ -1,98 +0,0 @@ -// -// ModalFrame.qml -// -// Created by Bradley Austin Davis on 15 Jan 2016 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 - -import "." -import "../controls-uit" -import "../styles-uit" - -Frame { - HifiConstants { id: hifi } - - Rectangle { - id: frameContent - - readonly property bool hasTitle: window.title != "" - - readonly property int frameMarginLeft: hifi.dimensions.modalDialogMargin.x - readonly property int frameMarginRight: hifi.dimensions.modalDialogMargin.x - readonly property int frameMarginTop: hifi.dimensions.modalDialogMargin.y + (frameContent.hasTitle ? hifi.dimensions.modalDialogTitleHeight + 10 : 0) - readonly property int frameMarginBottom: hifi.dimensions.modalDialogMargin.y - - signal frameClicked(); - - anchors { - fill: parent - topMargin: -frameMarginTop - leftMargin: -frameMarginLeft - rightMargin: -frameMarginRight - bottomMargin: -frameMarginBottom - } - - border { - width: hifi.dimensions.borderWidth - color: hifi.colors.lightGrayText80 - } - radius: hifi.dimensions.borderRadius - color: hifi.colors.faintGray - - // Enable dragging of the window - MouseArea { - anchors.fill: parent - drag.target: window - enabled: window.draggable - onClicked: window.frameClicked(); - } - - Item { - visible: frameContent.hasTitle - anchors.fill: parent - anchors { - topMargin: -parent.anchors.topMargin - leftMargin: -parent.anchors.leftMargin - rightMargin: -parent.anchors.rightMargin - } - - Item { - width: title.width + (icon.text !== "" ? icon.width + hifi.dimensions.contentSpacing.x : 0) - x: (parent.width - width) / 2 - - onWidthChanged: window.titleWidth = width - - HiFiGlyphs { - id: icon - text: window.iconText ? window.iconText : "" - size: window.iconSize ? window.iconSize : 30 - color: hifi.colors.lightGray - visible: text != "" - anchors.verticalCenter: title.verticalCenter - anchors.left: parent.left - } - RalewayRegular { - id: title - text: window.title - elide: Text.ElideRight - color: hifi.colors.baseGrayHighlight - size: hifi.fontSizes.overlayTitle - y: -hifi.dimensions.modalDialogTitleHeight - anchors.right: parent.right - } - } - - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - height: 1 - color: hifi.colors.lightGray - } - } - } -} diff --git a/interface/resources/qml/windows-uit/ModalWindow.qml b/interface/resources/qml/windows-uit/ModalWindow.qml deleted file mode 100644 index 144165e4e1..0000000000 --- a/interface/resources/qml/windows-uit/ModalWindow.qml +++ /dev/null @@ -1,28 +0,0 @@ -// -// ModalWindow.qml -// -// Created by Bradley Austin Davis on 22 Jan 2016 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 - -import "." - -Window { - id: window - modality: Qt.ApplicationModal - destroyOnCloseButton: true - destroyOnInvisible: true - frame: ModalFrame { } - - property int colorScheme: hifi.colorSchemes.light - property bool draggable: false - - signal frameClicked(); - - anchors.centerIn: draggable ? undefined : parent -} diff --git a/interface/resources/qml/windows-uit/Window.qml b/interface/resources/qml/windows-uit/Window.qml deleted file mode 100644 index d614b21ce2..0000000000 --- a/interface/resources/qml/windows-uit/Window.qml +++ /dev/null @@ -1,343 +0,0 @@ -// -// Window.qml -// -// Created by Bradley Austin Davis on 12 Jan 2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import QtGraphicalEffects 1.0 - -import "." -import "../styles-uit" - -// FIXME how do I set the initial position of a window without -// overriding places where the a individual client of the window -// might be setting the position with a Settings{} element? - -// FIXME how to I enable dragging without allowing the window to lay outside -// of the desktop? How do I ensure when the desktop resizes all the windows -// are still at least partially visible? -Fadable { - id: window - HifiConstants { id: hifi } - - // The Window size is the size of the content, while the frame - // decorations can extend outside it. - implicitHeight: content ? content.height : 0 - implicitWidth: content ? content.width : 0 - x: desktop.invalid_position; y: desktop.invalid_position; - enabled: visible - - signal windowDestroyed(); - - property int modality: Qt.NonModal - readonly property bool topLevelWindow: true - property string title - // Should the window be closable control? - property bool closable: true - // Should the window try to remain on top of other windows? - property bool alwaysOnTop: false - // Should hitting the close button hide or destroy the window? - property bool destroyOnCloseButton: true - // Should hiding the window destroy it or just hide it? - property bool destroyOnInvisible: false - // FIXME support for pinned / unpinned pending full design - // property bool pinnable: false - // property bool pinned: false - property bool resizable: false - property bool gradientsSupported: desktop.gradientsSupported - property int colorScheme: hifi.colorSchemes.dark - - property vector2d minSize: Qt.vector2d(100, 100) - property vector2d maxSize: Qt.vector2d(1280, 800) - - // The content to place inside the window, determined by the client - default property var content - - property var footer: Item { } // Optional static footer at the bottom of the dialog. - - function setDefaultFocus() {} // Default function; can be overridden by dialogs. - - property var rectifier: Timer { - property bool executing: false; - interval: 100 - repeat: false - running: false - - onTriggered: { - executing = true; - x = Math.floor(x); - y = Math.floor(y); - executing = false; - } - - function begin() { - if (!executing) { - restart(); - } - } - } - onXChanged: rectifier.begin(); - onYChanged: rectifier.begin(); - - // This mouse area serves to raise the window. To function, it must live - // in the window and have a higher Z-order than the content, but follow - // the position and size of frame decoration - property var activator: MouseArea { - width: frame.decoration.width - height: frame.decoration.height - x: frame.decoration.anchors.leftMargin - y: frame.decoration.anchors.topMargin - propagateComposedEvents: true - acceptedButtons: Qt.AllButtons - enabled: window.visible - onPressed: { - //console.log("Pressed on activator area"); - window.raise(); - mouse.accepted = false; - } - } - - // This mouse area serves to swallow mouse events while the mouse is over the window - // to prevent things like mouse wheel events from reaching the application and changing - // the camera if the user is scrolling through a list and gets to the end. - property var swallower: MouseArea { - width: frame.decoration.width - height: frame.decoration.height - x: frame.decoration.anchors.leftMargin - y: frame.decoration.anchors.topMargin - hoverEnabled: true - acceptedButtons: Qt.AllButtons - enabled: window.visible - onClicked: {} - onDoubleClicked: {} - onPressAndHold: {} - onReleased: {} - onWheel: {} - } - - // Default to a standard frame. Can be overriden to provide custom - // frame styles, like a full desktop frame to simulate a modal window - property var frame: DefaultFrame { } - - // Scrollable window content. - property var pane: Item { - property bool isScrolling: scrollView.height < scrollView.contentItem.height - property int contentWidth: scrollView.width - (isScrolling ? 10 : 0) - property int scrollHeight: scrollView.height - - anchors.fill: parent - anchors.rightMargin: isScrolling ? 11 : 0 - - Rectangle { - id: contentBackground - anchors.fill: parent - anchors.rightMargin: parent.isScrolling ? 11 : 0 - color: hifi.colors.baseGray - visible: modality != Qt.ApplicationModal - } - - LinearGradient { - visible: gradientsSupported && modality != Qt.ApplicationModal - anchors.top: contentBackground.bottom - anchors.left: contentBackground.left - width: contentBackground.width - 1 - height: 4 - start: Qt.point(0, 0) - end: Qt.point(0, 4) - gradient: Gradient { - GradientStop { position: 0.0; color: hifi.colors.darkGray } - GradientStop { position: 1.0; color: hifi.colors.darkGray0 } - } - cached: true - } - - ScrollView { - id: scrollView - contentItem: content - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - verticalScrollBarPolicy: Qt.ScrollBarAsNeeded - anchors.fill: parent - anchors.rightMargin: parent.isScrolling ? 1 : 0 - anchors.bottomMargin: footer.height > 0 ? footerPane.height : 0 - - style: ScrollViewStyle { - - padding.right: -7 // Move to right away from content. - - handle: Item { - implicitWidth: 8 - Rectangle { - radius: 4 - color: hifi.colors.white30 - anchors { - fill: parent - leftMargin: 2 // Finesse size and position. - topMargin: 1 - bottomMargin: 1 - } - } - } - - scrollBarBackground: Item { - implicitWidth: 10 - Rectangle { - color: hifi.colors.darkGray30 - radius: 4 - anchors { - fill: parent - topMargin: -1 // Finesse size - bottomMargin: -2 - } - } - } - - incrementControl: Item { - visible: false - } - - decrementControl: Item { - visible: false - } - } - } - - Rectangle { - // Optional non-scrolling footer. - id: footerPane - anchors { - left: parent.left - bottom: parent.bottom - } - width: parent.contentWidth - height: footer.height + 2 * hifi.dimensions.contentSpacing.y + 3 - color: hifi.colors.baseGray - visible: footer.height > 0 - - Item { - // Horizontal rule. - anchors.fill: parent - - Rectangle { - width: parent.width - height: 1 - y: 1 // Stop displaying content just above horizontal rule/=. - color: hifi.colors.baseGrayShadow - } - - Rectangle { - width: parent.width - height: 1 - y: 2 - color: hifi.colors.baseGrayHighlight - } - } - - Item { - anchors.fill: parent - anchors.topMargin: 3 // Horizontal rule. - children: [ footer ] - } - } - } - - children: [ swallower, frame, pane, activator ] - - Component.onCompleted: { - window.parentChanged.connect(raise); - raise(); - setDefaultFocus(); - centerOrReposition(); - } - Component.onDestruction: { - window.parentChanged.disconnect(raise); // Prevent warning on shutdown - windowDestroyed(); - } - - onVisibleChanged: { - if (!visible && destroyOnInvisible) { - destroy(); - return; - } - if (visible) { - raise(); - } - enabled = visible - - if (visible && parent) { - centerOrReposition(); - } - } - - function centerOrReposition() { - if (x == desktop.invalid_position && y == desktop.invalid_position) { - desktop.centerOnVisible(window); - } else { - desktop.repositionOnVisible(window); - } - } - - function raise() { - if (visible && parent) { - desktop.raise(window) - } - } - - function pin() { -// pinned = ! pinned - } - - // our close function performs the same way as the OffscreenUI class: - // don't do anything but manipulate the targetVisible flag and let the other - // mechanisms decide if the window should be destroyed after the close - // animation completes - // FIXME using this close function messes up the visibility signals received by the - // type and it's derived types -// function close() { -// console.log("Closing " + window) -// if (destroyOnCloseButton) { -// destroyOnInvisible = true -// } -// visible = false; -// } - - function framedRect() { - if (!frame || !frame.decoration) { - return Qt.rect(0, 0, window.width, window.height) - } - return Qt.rect(frame.decoration.anchors.leftMargin, frame.decoration.anchors.topMargin, - window.width - frame.decoration.anchors.leftMargin - frame.decoration.anchors.rightMargin, - window.height - frame.decoration.anchors.topMargin - frame.decoration.anchors.bottomMargin) - } - - Keys.onPressed: { - switch(event.key) { - case Qt.Key_Control: - case Qt.Key_Shift: - case Qt.Key_Meta: - case Qt.Key_Alt: - break; - - case Qt.Key_W: - if (window.closable && (event.modifiers === Qt.ControlModifier)) { - visible = false - event.accepted = true - } - // fall through - - default: - // Consume unmodified keyboard entries while the window is focused, to prevent them - // from propagating to the application - if (event.modifiers === Qt.NoModifier) { - event.accepted = true; - } - break; - } - } -} diff --git a/interface/resources/qml/windows/DefaultFrame.qml b/interface/resources/qml/windows/DefaultFrame.qml index c58f9ca545..242209dbe0 100644 --- a/interface/resources/qml/windows/DefaultFrame.qml +++ b/interface/resources/qml/windows/DefaultFrame.qml @@ -1,20 +1,48 @@ +// +// DefaultFrame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 +import QtGraphicalEffects 1.0 import "." -import "../controls" +import "../styles-uit" Frame { - id: frame - - property bool wideTopMargin: (window && (window.closable || window.title)); + HifiConstants { id: hifi } Rectangle { - anchors { margins: -iconSize; topMargin: -iconSize * (wideTopMargin ? 2 : 1); } - anchors.fill: parent; - color: "#7f7f7f7f"; - radius: 3; + // Dialog frame + id: frameContent - // Allow dragging of the window + readonly property int iconSize: hifi.dimensions.frameIconSize + readonly property int frameMargin: 9 + readonly property int frameMarginLeft: frameMargin + readonly property int frameMarginRight: frameMargin + readonly property int frameMarginTop: 2 * frameMargin + iconSize + readonly property int frameMarginBottom: iconSize + 11 + + anchors { + topMargin: -frameMarginTop + leftMargin: -frameMarginLeft + rightMargin: -frameMarginRight + bottomMargin: -frameMarginBottom + } + anchors.fill: parent + color: hifi.colors.baseGrayHighlight40 + border { + width: hifi.dimensions.borderWidth + color: hifi.colors.faintGray50 + } + radius: hifi.dimensions.borderRadius + + // Enable dragging of the window MouseArea { anchors.fill: parent drag.target: window @@ -22,48 +50,70 @@ Frame { Row { id: controlsRow - anchors { right: parent.right; top: parent.top; rightMargin: iconSize; topMargin: iconSize / 2; } - spacing: iconSize / 4 - FontAwesome { - visible: false - text: "\uf08d" - style: Text.Outline; styleColor: "white" - size: frame.iconSize - rotation: !frame.parent ? 90 : frame.parent.pinned ? 0 : 90 - color: frame.pinned ? "red" : "black" + anchors { + right: parent.right; + top: parent.top; + topMargin: frameContent.frameMargin + 1 // Move down a little to visually align with the title + rightMargin: frameContent.frameMarginRight; + } + spacing: frameContent.iconSize / 4 + + HiFiGlyphs { + // "Pin" button + visible: window.pinnable + text: window.pinned ? hifi.glyphs.pinInverted : hifi.glyphs.pin + color: pinClickArea.pressed ? hifi.colors.redHighlight : hifi.colors.white + size: frameContent.iconSize MouseArea { + id: pinClickArea anchors.fill: parent + hoverEnabled: true propagateComposedEvents: true - onClicked: { frame.pin(); mouse.accepted = false; } + onClicked: window.pinned = !window.pinned; } } - FontAwesome { + + HiFiGlyphs { + // "Close" button visible: window ? window.closable : false - text: closeClickArea.containsMouse ? "\uf057" : "\uf05c" - style: Text.Outline; - styleColor: "white" - color: closeClickArea.containsMouse ? "red" : "black" - size: frame.iconSize + text: closeClickArea.containsPress ? hifi.glyphs.closeInverted : hifi.glyphs.close + color: closeClickArea.containsMouse ? hifi.colors.redHighlight : hifi.colors.white + size: frameContent.iconSize MouseArea { id: closeClickArea anchors.fill: parent hoverEnabled: true - onClicked: window.visible = false; + onClicked: window.shown = false; } } } - Text { + RalewayRegular { + // Title id: titleText - anchors { left: parent.left; leftMargin: iconSize; right: controlsRow.left; rightMargin: iconSize; top: parent.top; topMargin: iconSize / 2; } + anchors { + left: parent.left + leftMargin: frameContent.frameMarginLeft + hifi.dimensions.contentMargin.x + right: controlsRow.left + rightMargin: frameContent.iconSize + top: parent.top + topMargin: frameContent.frameMargin + } text: window ? window.title : "" - elide: Text.ElideRight - font.bold: true - color: (window && window.focus) ? "white" : "gray" - style: Text.Outline; - styleColor: "black" + color: hifi.colors.white + size: hifi.fontSizes.overlayTitle + } + + DropShadow { + source: titleText + anchors.fill: titleText + horizontalOffset: 2 + verticalOffset: 2 + samples: 2 + color: hifi.colors.baseGrayShadow60 + visible: (window && window.focus) + cached: true } } - } diff --git a/interface/resources/qml/windows/Fadable.qml b/interface/resources/qml/windows/Fadable.qml index 0352966bd0..38cd4bf1f9 100644 --- a/interface/resources/qml/windows/Fadable.qml +++ b/interface/resources/qml/windows/Fadable.qml @@ -1,8 +1,18 @@ +// +// Fadable.qml +// +// Created by Bradley Austin Davis on 15 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 import QtQuick.Controls 1.4 import QtGraphicalEffects 1.0 -import "." -import "../styles" + +import "../styles-uit" // Enable window visibility transitions FocusScope { @@ -13,6 +23,7 @@ FocusScope { fadeTargetProperty = visible ? 1.0 : 0.0 } + property var completionCallback; // The target property to animate, usually scale or opacity property alias fadeTargetProperty: root.opacity // always start the property at 0 to enable fade in on creation @@ -33,6 +44,13 @@ FocusScope { fadeTargetProperty = target ? 1.0 : 0.0; return; } + + // Now handle completions + if (completionCallback) { + completionCallback(); + completionCallback = undefined; + } + } // The actual animator @@ -43,8 +61,17 @@ FocusScope { } } - // Once we're transparent, disable the dialog's visibility onFadeTargetPropertyChanged: { visible = (fadeTargetProperty != 0.0); } + + function fadeIn(callback) { + completionCallback = callback; + fadeTargetProperty = 1.0; + } + + function fadeOut(callback) { + completionCallback = callback; + fadeTargetProperty = 0.0; + } } diff --git a/interface/resources/qml/windows/Frame.qml b/interface/resources/qml/windows/Frame.qml index 20bf669b9a..88d8c3ad41 100644 --- a/interface/resources/qml/windows/Frame.qml +++ b/interface/resources/qml/windows/Frame.qml @@ -1,24 +1,42 @@ -import QtQuick 2.5 +// +// Frame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// -import "../controls" +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "../styles-uit" import "../js/Utils.js" as Utils Item { id: frame + HifiConstants { id: hifi } + + default property var decoration + + property bool gradientsSupported: desktop.gradientsSupported + + readonly property int frameMarginLeft: frameContent.frameMarginLeft + readonly property int frameMarginRight: frameContent.frameMarginRight + readonly property int frameMarginTop: frameContent.frameMarginTop + readonly property int frameMarginBottom: frameContent.frameMarginBottom + // Frames always fill their parents, but their decorations may extend // beyond the window via negative margin sizes anchors.fill: parent - // Convenience accessor for the window - property alias window: frame.parent - readonly property int iconSize: 24 - default property var decoration; - children: [ + focusShadow, decoration, sizeOutline, debugZ, - sizeDrag, + sizeDrag ] Text { @@ -35,57 +53,81 @@ Item { window.height = newSize.y } + RadialGradient { + id: focusShadow + width: 1.66 * window.width + height: 1.66 * window.height + x: (window.width - width) / 2 + y: window.height / 2 - 0.375 * height + visible: gradientsSupported && window && window.focus && window.content.visible + gradient: Gradient { + // GradientStop position 0.5 is at full circumference of circle that fits inside the square. + GradientStop { position: 0.0; color: "#ff000000" } // black, 100% opacity + GradientStop { position: 0.333; color: "#1f000000" } // black, 12% opacity + GradientStop { position: 0.5; color: "#00000000" } // black, 0% opacity + GradientStop { position: 1.0; color: "#00000000" } + } + cached: true + } + Rectangle { id: sizeOutline - width: window ? window.width : 0 - height: window ? window.height : 0 - color: "#00000000" - border.width: 4 - radius: 10 + x: -frameMarginLeft + y: -frameMarginTop + width: window ? window.width + frameMarginLeft + frameMarginRight + 2 : 0 + height: window ? window.height + frameMarginTop + frameMarginBottom + 2 : 0 + color: hifi.colors.baseGrayHighlight15 + border.width: 3 + border.color: hifi.colors.white50 + radius: hifi.dimensions.borderRadius visible: window ? !window.content.visible : false } MouseArea { + // Resize handle id: sizeDrag - width: iconSize - height: iconSize + width: hifi.dimensions.frameIconSize + height: hifi.dimensions.frameIconSize enabled: window ? window.resizable : false - x: window ? window.width : 0 - y: window ? window.height : 0 + hoverEnabled: true + x: window ? window.width + frameMarginRight - hifi.dimensions.frameIconSize : 0 + y: window ? window.height + 4 : 0 property vector2d pressOrigin property vector2d sizeOrigin property bool hid: false onPressed: { - console.log("Pressed on size") + //console.log("Pressed on size") pressOrigin = Qt.vector2d(mouseX, mouseY) sizeOrigin = Qt.vector2d(window.content.width, window.content.height) hid = false; } onReleased: { if (hid) { - window.content.visible = true + pane.visible = true + frameContent.visible = true hid = false; } } onPositionChanged: { if (pressed) { - if (window.content.visible) { - window.content.visible = false; + if (pane.visible) { + pane.visible = false; + frameContent.visible = false hid = true; } var delta = Qt.vector2d(mouseX, mouseY).minus(pressOrigin); frame.deltaSize(delta.x, delta.y) } } - FontAwesome { + HiFiGlyphs { visible: sizeDrag.enabled - rotation: -45 - anchors { centerIn: parent } - horizontalAlignment: Text.AlignHCenter - text: "\uf07d" - size: iconSize / 3 * 2 - style: Text.Outline; styleColor: "white" + x: -11 // Move a little to visually align + y: window.modality == Qt.ApplicationModal ? -6 : -4 + text: hifi.glyphs.resizeHandle + size: hifi.dimensions.frameIconSize + 10 + color: sizeDrag.containsMouse || sizeDrag.pressed + ? hifi.colors.white + : (window.colorScheme == hifi.colorSchemes.dark ? hifi.colors.white50 : hifi.colors.lightGrayText80) } } - } diff --git a/interface/resources/qml/windows/HiddenFrame.qml b/interface/resources/qml/windows/HiddenFrame.qml index 2621b71eed..3d3fd047e2 100644 --- a/interface/resources/qml/windows/HiddenFrame.qml +++ b/interface/resources/qml/windows/HiddenFrame.qml @@ -2,7 +2,7 @@ import QtQuick 2.5 import "." -Frame { +Item { id: frame Item { anchors.fill: parent } diff --git a/interface/resources/qml/windows/ModalFrame.qml b/interface/resources/qml/windows/ModalFrame.qml index eb4641bc75..211353b5f3 100644 --- a/interface/resources/qml/windows/ModalFrame.qml +++ b/interface/resources/qml/windows/ModalFrame.qml @@ -1,36 +1,98 @@ +// +// ModalFrame.qml +// +// Created by Bradley Austin Davis on 15 Jan 2016 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 import "." -import "../controls" +import "../controls-uit" +import "../styles-uit" Frame { - id: frame + HifiConstants { id: hifi } - Item { - anchors.fill: parent + Rectangle { + id: frameContent - Rectangle { - id: background - anchors.fill: parent - anchors.margins: -4096 - visible: window.visible - color: "#7f7f7f7f"; - radius: 3; + readonly property bool hasTitle: window.title != "" + + readonly property int frameMarginLeft: hifi.dimensions.modalDialogMargin.x + readonly property int frameMarginRight: hifi.dimensions.modalDialogMargin.x + readonly property int frameMarginTop: hifi.dimensions.modalDialogMargin.y + (frameContent.hasTitle ? hifi.dimensions.modalDialogTitleHeight + 10 : 0) + readonly property int frameMarginBottom: hifi.dimensions.modalDialogMargin.y + + signal frameClicked(); + + anchors { + fill: parent + topMargin: -frameMarginTop + leftMargin: -frameMarginLeft + rightMargin: -frameMarginRight + bottomMargin: -frameMarginBottom } - Text { - y: -implicitHeight - iconSize / 2 - text: window.title - elide: Text.ElideRight - font.bold: true - color: window.focus ? "white" : "gray" - style: Text.Outline; - styleColor: "black" + border { + width: hifi.dimensions.borderWidth + color: hifi.colors.lightGrayText80 + } + radius: hifi.dimensions.borderRadius + color: hifi.colors.faintGray + + // Enable dragging of the window + MouseArea { + anchors.fill: parent + drag.target: window + enabled: window.draggable + onClicked: window.frameClicked(); + } + + Item { + visible: frameContent.hasTitle + anchors.fill: parent + anchors { + topMargin: -parent.anchors.topMargin + leftMargin: -parent.anchors.leftMargin + rightMargin: -parent.anchors.rightMargin + } + + Item { + width: title.width + (icon.text !== "" ? icon.width + hifi.dimensions.contentSpacing.x : 0) + x: (parent.width - width) / 2 + + onWidthChanged: window.titleWidth = width + + HiFiGlyphs { + id: icon + text: window.iconText ? window.iconText : "" + size: window.iconSize ? window.iconSize : 30 + color: hifi.colors.lightGray + visible: text != "" + anchors.verticalCenter: title.verticalCenter + anchors.left: parent.left + } + RalewayRegular { + id: title + text: window.title + elide: Text.ElideRight + color: hifi.colors.baseGrayHighlight + size: hifi.fontSizes.overlayTitle + y: -hifi.dimensions.modalDialogTitleHeight + anchors.right: parent.right + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: hifi.colors.lightGray + } } } - - - - } - diff --git a/interface/resources/qml/windows/ModalWindow.qml b/interface/resources/qml/windows/ModalWindow.qml index 32443e70e3..2d56099051 100644 --- a/interface/resources/qml/windows/ModalWindow.qml +++ b/interface/resources/qml/windows/ModalWindow.qml @@ -1,14 +1,28 @@ +// +// ModalWindow.qml +// +// Created by Bradley Austin Davis on 22 Jan 2016 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 import "." -Window { - id: root - anchors.centerIn: parent +ScrollingWindow { + id: window modality: Qt.ApplicationModal destroyOnCloseButton: true - destroyOnInvisible: true - frame: ModalFrame{} + destroyOnHidden: true + frame: ModalFrame { } + + property int colorScheme: hifi.colorSchemes.light + property bool draggable: false + + signal frameClicked(); + + anchors.centerIn: draggable ? undefined : parent } - - diff --git a/interface/resources/qml/windows/ScrollingWindow.qml b/interface/resources/qml/windows/ScrollingWindow.qml new file mode 100644 index 0000000000..f1dc744344 --- /dev/null +++ b/interface/resources/qml/windows/ScrollingWindow.qml @@ -0,0 +1,157 @@ +// +// Window.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtGraphicalEffects 1.0 + +import "." +import "../styles-uit" + +// FIXME how do I set the initial position of a window without +// overriding places where the a individual client of the window +// might be setting the position with a Settings{} element? + +// FIXME how to I enable dragging without allowing the window to lay outside +// of the desktop? How do I ensure when the desktop resizes all the windows +// are still at least partially visible? +Window { + id: window + HifiConstants { id: hifi } + children: [ swallower, frame, pane, activator ] + + property var footer: Item { } // Optional static footer at the bottom of the dialog. + + // Scrollable window content. + // FIXME this should not define any visual content in this type. The base window + // type should only consist of logic sized areas, with nothing drawn (although the + // default value for the frame property does include visual decorations) + property var pane: Item { + property bool isScrolling: scrollView.height < scrollView.contentItem.height + property int contentWidth: scrollView.width - (isScrolling ? 10 : 0) + property int scrollHeight: scrollView.height + + anchors.fill: parent + anchors.rightMargin: isScrolling ? 11 : 0 + + Rectangle { + id: contentBackground + anchors.fill: parent + anchors.rightMargin: parent.isScrolling ? 11 : 0 + color: hifi.colors.baseGray + visible: !window.hideBackground && modality != Qt.ApplicationModal + } + + + LinearGradient { + visible: !window.hideBackground && gradientsSupported && modality != Qt.ApplicationModal + anchors.top: contentBackground.bottom + anchors.left: contentBackground.left + width: contentBackground.width - 1 + height: 4 + start: Qt.point(0, 0) + end: Qt.point(0, 4) + gradient: Gradient { + GradientStop { position: 0.0; color: hifi.colors.darkGray } + GradientStop { position: 1.0; color: hifi.colors.darkGray0 } + } + cached: true + } + + ScrollView { + id: scrollView + contentItem: content + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + verticalScrollBarPolicy: Qt.ScrollBarAsNeeded + anchors.fill: parent + anchors.rightMargin: parent.isScrolling ? 1 : 0 + anchors.bottomMargin: footer.height > 0 ? footerPane.height : 0 + + style: ScrollViewStyle { + + padding.right: -7 // Move to right away from content. + + handle: Item { + implicitWidth: 8 + Rectangle { + radius: 4 + color: hifi.colors.white30 + anchors { + fill: parent + leftMargin: 2 // Finesse size and position. + topMargin: 1 + bottomMargin: 1 + } + } + } + + scrollBarBackground: Item { + implicitWidth: 10 + Rectangle { + color: hifi.colors.darkGray30 + radius: 4 + anchors { + fill: parent + topMargin: -1 // Finesse size + bottomMargin: -2 + } + } + } + + incrementControl: Item { + visible: false + } + + decrementControl: Item { + visible: false + } + } + } + + Rectangle { + // Optional non-scrolling footer. + id: footerPane + anchors { + left: parent.left + bottom: parent.bottom + } + width: parent.contentWidth + height: footer.height + 2 * hifi.dimensions.contentSpacing.y + 3 + color: hifi.colors.baseGray + visible: footer.height > 0 + + Item { + // Horizontal rule. + anchors.fill: parent + + Rectangle { + width: parent.width + height: 1 + y: 1 // Stop displaying content just above horizontal rule/=. + color: hifi.colors.baseGrayShadow + } + + Rectangle { + width: parent.width + height: 1 + y: 2 + color: hifi.colors.baseGrayHighlight + } + } + + Item { + anchors.fill: parent + anchors.topMargin: 3 // Horizontal rule. + children: [ footer ] + } + } + } +} diff --git a/interface/resources/qml/windows/ToolFrame.qml b/interface/resources/qml/windows/ToolFrame.qml new file mode 100644 index 0000000000..eff5fc0377 --- /dev/null +++ b/interface/resources/qml/windows/ToolFrame.qml @@ -0,0 +1,96 @@ +// +// DefaultFrame.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "." +import "../styles-uit" + +Frame { + HifiConstants { id: hifi } + property bool horizontalSpacers: false + property bool verticalSpacers: false + + Rectangle { + // Dialog frame + id: frameContent + readonly property int frameMargin: 6 + readonly property int frameMarginLeft: frameMargin + (horizontalSpacers ? 12 : 0) + readonly property int frameMarginRight: frameMargin + (horizontalSpacers ? 12 : 0) + readonly property int frameMarginTop: frameMargin + (verticalSpacers ? 12 : 0) + readonly property int frameMarginBottom: frameMargin + (verticalSpacers ? 12 : 0) + + Rectangle { + visible: horizontalSpacers + anchors.left: parent.left + anchors.leftMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: 8 + height: window.height + color: "gray"; + radius: 4 + } + + Rectangle { + visible: horizontalSpacers + anchors.right: parent.right + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: 8 + height: window.height + color: "gray"; + radius: 4 + } + + Rectangle { + visible: verticalSpacers + anchors.top: parent.top + anchors.topMargin: 6 + anchors.horizontalCenter: parent.horizontalCenter + height: 8 + width: window.width + color: "gray"; + radius: 4 + } + + Rectangle { + visible: verticalSpacers + anchors.bottom: parent.bottom + anchors.bottomMargin: 6 + anchors.horizontalCenter: parent.horizontalCenter + height: 8 + width: window.width + color: "gray"; + radius: 4 + } + + anchors { + leftMargin: -frameMarginLeft + rightMargin: -frameMarginRight + topMargin: -frameMarginTop + bottomMargin: -frameMarginBottom + } + anchors.fill: parent + color: hifi.colors.baseGrayHighlight40 + border { + width: hifi.dimensions.borderWidth + color: hifi.colors.faintGray50 + } + radius: hifi.dimensions.borderRadius / 2 + + // Enable dragging of the window + MouseArea { + anchors.fill: parent + drag.target: window + } + } +} + diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 3abdbacc64..e3e70c1e74 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -1,9 +1,20 @@ +// +// Window.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 import QtGraphicalEffects 1.0 import "." -import "../styles" +import "../styles-uit" // FIXME how do I set the initial position of a window without // overriding places where the a individual client of the window @@ -15,16 +26,36 @@ import "../styles" Fadable { id: window HifiConstants { id: hifi } + + // + // Signals + // + signal windowDestroyed(); + + // + // Native properties + // + // The Window size is the size of the content, while the frame // decorations can extend outside it. implicitHeight: content ? content.height : 0 implicitWidth: content ? content.width : 0 x: desktop.invalid_position; y: desktop.invalid_position; - enabled: visible + children: [ swallower, frame, content, activator ] - signal windowDestroyed(); + // + // Custom properties + // property int modality: Qt.NonModal + // Corresponds to the window shown / hidden state AS DISTINCT from window visibility. + // Window visibility should NOT be used as a proxy for any other behavior. + property bool shown: true + // FIXME workaround to deal with the face that some visual items are defined here, + // when they should be moved to a frame derived type + property bool hideBackground: false + visible: shown + enabled: visible readonly property bool topLevelWindow: true property string title // Should the window be closable control? @@ -34,17 +65,23 @@ Fadable { // Should hitting the close button hide or destroy the window? property bool destroyOnCloseButton: true // Should hiding the window destroy it or just hide it? - property bool destroyOnInvisible: false - // FIXME support for pinned / unpinned pending full design - // property bool pinnable: false - // property bool pinned: false + property bool destroyOnHidden: false + property bool pinnable: true + property bool pinned: false property bool resizable: false + property bool gradientsSupported: desktop.gradientsSupported + property int colorScheme: hifi.colorSchemes.dark + property vector2d minSize: Qt.vector2d(100, 100) - property vector2d maxSize: Qt.vector2d(1280, 720) + property vector2d maxSize: Qt.vector2d(1280, 800) // The content to place inside the window, determined by the client default property var content + property var footer: Item { } // Optional static footer at the bottom of the dialog. + + function setDefaultFocus() {} // Default function; can be overridden by dialogs. + property var rectifier: Timer { property bool executing: false; interval: 100 @@ -65,20 +102,15 @@ Fadable { } } - - onXChanged: rectifier.begin(); - onYChanged: rectifier.begin(); - // This mouse area serves to raise the window. To function, it must live // in the window and have a higher Z-order than the content, but follow // the position and size of frame decoration property var activator: MouseArea { - width: frame.decoration.width - height: frame.decoration.height - x: frame.decoration.anchors.margins - y: frame.decoration.anchors.topMargin + width: frame.decoration ? frame.decoration.width : window.width + height: frame.decoration ? frame.decoration.height : window.height + x: frame.decoration ? frame.decoration.anchors.leftMargin : 0 + y: frame.decoration ? frame.decoration.anchors.topMargin : 0 propagateComposedEvents: true - hoverEnabled: true acceptedButtons: Qt.AllButtons enabled: window.visible onPressed: { @@ -92,10 +124,10 @@ Fadable { // to prevent things like mouse wheel events from reaching the application and changing // the camera if the user is scrolling through a list and gets to the end. property var swallower: MouseArea { - width: frame.decoration.width - height: frame.decoration.height - x: frame.decoration.anchors.margins - y: frame.decoration.anchors.topMargin + width: frame.decoration ? frame.decoration.width : window.width + height: frame.decoration ? frame.decoration.height : window.height + x: frame.decoration ? frame.decoration.anchors.leftMargin : 0 + y: frame.decoration ? frame.decoration.anchors.topMargin : 0 hoverEnabled: true acceptedButtons: Qt.AllButtons enabled: window.visible @@ -106,71 +138,119 @@ Fadable { onWheel: {} } - // Default to a standard frame. Can be overriden to provide custom // frame styles, like a full desktop frame to simulate a modal window - property var frame: DefaultFrame { } + property var frame: DefaultFrame { + //window: window + } - children: [ swallower, frame, content, activator ] - + // + // Handlers + // Component.onCompleted: { window.parentChanged.connect(raise); - raise(); - centerOrReposition(); + setDefaultFocus(); + d.centerOrReposition(); + d.updateVisibility(shown); } Component.onDestruction: { window.parentChanged.disconnect(raise); // Prevent warning on shutdown windowDestroyed(); } - function centerOrReposition() { - if (x == desktop.invalid_position && y == desktop.invalid_position) { - desktop.centerOnVisible(window); - } else { - desktop.repositionOnVisible(window); - } - } + onXChanged: rectifier.begin(); + onYChanged: rectifier.begin(); + + onShownChanged: d.updateVisibility(shown) onVisibleChanged: { - if (!visible && destroyOnInvisible) { - destroy(); - return; - } - if (visible) { - raise(); - } enabled = visible - if (visible && parent) { - centerOrReposition(); + d.centerOrReposition(); } } + QtObject { + id: d + + readonly property alias pinned: window.pinned + readonly property alias shown: window.shown + readonly property alias modality: window.modality; + + function getTargetVisibility() { + if (!window.shown) { + return false; + } + + if (modality !== Qt.NonModal) { + return true; + } + + if (pinned) { + return true; + } + + if (desktop && !desktop.pinned) { + return true; + } + + return false; + } + + // The force flag causes all windows to fade back in, because a window was shown + readonly property alias visible: window.visible + function updateVisibility(force) { + if (force && !pinned && desktop.pinned) { + // Change the pinned state (which in turn will call us again) + desktop.pinned = false; + return; + } + + var targetVisibility = getTargetVisibility(); + if (targetVisibility === visible) { + return; + } + + if (targetVisibility) { + fadeIn(function() { + if (force) { + window.raise(); + } + }); + } else { + fadeOut(function() { + if (!window.shown && window.destroyOnHidden) { + window.destroy(); + } + }); + } + } + + function centerOrReposition() { + if (x == desktop.invalid_position && y == desktop.invalid_position) { + desktop.centerOnVisible(window); + } else { + desktop.repositionOnVisible(window); + } + } + + } + + // When the desktop pinned state changes, automatically handle the current windows + Connections { target: desktop; onPinnedChanged: d.updateVisibility() } + + function raise() { if (visible && parent) { desktop.raise(window) } } - function pin() { -// pinned = ! pinned + function setPinned() { + pinned = !pinned } - // our close function performs the same way as the OffscreenUI class: - // don't do anything but manipulate the targetVisible flag and let the other - // mechanisms decide if the window should be destroyed after the close - // animation completes - // FIXME using this close function messes up the visibility signals received by the - // type and it's derived types -// function close() { -// console.log("Closing " + window) -// if (destroyOnCloseButton) { -// destroyOnInvisible = true -// } -// visible = false; -// } - function framedRect() { if (!frame || !frame.decoration) { return Qt.rect(0, 0, window.width, window.height) @@ -180,7 +260,6 @@ Fadable { window.height - frame.decoration.anchors.topMargin - frame.decoration.anchors.bottomMargin) } - Keys.onPressed: { switch(event.key) { case Qt.Key_Control: @@ -189,10 +268,9 @@ Fadable { case Qt.Key_Alt: break; - case Qt.Key_W: if (window.closable && (event.modifiers === Qt.ControlModifier)) { - visible = false + shown = false event.accepted = true } // fall through diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9bcd85fd02..5978461dcf 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -68,6 +68,7 @@ #include #include #include +#include #include #include #include @@ -83,7 +84,6 @@ #include #include #include -#include #include #include #include @@ -119,7 +119,6 @@ #include "InterfaceLogging.h" #include "LODManager.h" #include "ModelPackager.h" -#include "PluginContainerProxy.h" #include "scripting/AccountScriptingInterface.h" #include "scripting/AssetMappingsScriptingInterface.h" #include "scripting/AudioDeviceScriptingInterface.h" @@ -133,6 +132,7 @@ #include "scripting/WebWindowClass.h" #include "scripting/WindowScriptingInterface.h" #include "scripting/ControllerScriptingInterface.h" +#include "scripting/ToolbarScriptingInterface.h" #include "scripting/RatesScriptingInterface.h" #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" @@ -151,6 +151,8 @@ #include "InterfaceParentFinder.h" #include "FrameTimingsScriptingInterface.h" +#include +#include // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. @@ -437,7 +439,8 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - + DependencyManager::set(); + DependencyManager::set(); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) DependencyManager::set(); @@ -464,7 +467,6 @@ bool setupEssentials(int& argc, char** argv) { // continuing to overburden Application.cpp Cube3DOverlay* _keyboardFocusHighlight{ nullptr }; int _keyboardFocusHighlightID{ -1 }; -PluginContainer* _pluginContainer; // FIXME hack access to the internal share context for the Chromium helper @@ -504,6 +506,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _maxOctreePPS(maxOctreePacketsPerSecond.get()), _lastFaceTrackerUpdate(0) { + + + PluginContainer* pluginContainer = dynamic_cast(this); // set the container for any plugins that care + PluginManager::getInstance()->setContainer(pluginContainer); + // FIXME this may be excessively conservative. On the other hand // maybe I'm used to having an 8-core machine // Perhaps find the ideal thread count and subtract 2 or 3 @@ -521,7 +528,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _entityClipboard->createRootElement(); - _pluginContainer = new PluginContainerProxy(); #ifdef Q_OS_WIN installNativeEventFilter(&MyNativeEventFilter::getInstance()); #endif @@ -672,10 +678,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : accountManager->setIsAgent(true); accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL); - // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. - // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. - UserActivityLogger::getInstance().launch(applicationVersion(), _previousSessionCrashed, sessionRunTime.get()); - auto addressManager = DependencyManager::get(); // use our MyAvatar position and quat for address manager path @@ -765,6 +767,40 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // Make sure we don't time out during slow operations at startup updateHeartbeat(); + + // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. + // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. + auto gpuIdent = GPUIdent::getInstance(); + auto glContextData = getGLContextData(); + QJsonObject properties = { + { "version", applicationVersion() }, + { "previousSessionCrashed", _previousSessionCrashed }, + { "previousSessionRuntime", sessionRunTime.get() }, + { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, + { "kernel_type", QSysInfo::kernelType() }, + { "kernel_version", QSysInfo::kernelVersion() }, + { "os_type", QSysInfo::productType() }, + { "os_version", QSysInfo::productVersion() }, + { "gpu_name", gpuIdent->getName() }, + { "gpu_driver", gpuIdent->getDriver() }, + { "gpu_memory", static_cast(gpuIdent->getMemory()) }, + { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, + { "gl_version", glContextData["version"] }, + { "gl_vender", glContextData["vendor"] }, + { "gl_sl_version", glContextData["slVersion"] }, + { "gl_renderer", glContextData["renderer"] } + }; + auto macVersion = QSysInfo::macVersion(); + if (macVersion != QSysInfo::MV_None) { + properties["os_osx_version"] = QSysInfo::macVersion(); + } + auto windowsVersion = QSysInfo::windowsVersion(); + if (windowsVersion != QSysInfo::WV_None) { + properties["os_win_version"] = QSysInfo::windowsVersion(); + } + UserActivityLogger::getInstance().logAction("launch", properties); + + // Tell our entity edit sender about our known jurisdictions _entityEditSender.setServerJurisdictions(&_entityServerJurisdictions); _entityEditSender.setMyAvatar(getMyAvatar()); @@ -886,17 +922,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : cycleCamera(); } else if (action == controller::toInt(controller::Action::UI_NAV_SELECT)) { if (!offscreenUi->navigationFocused()) { - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); + toggleMenuUnderReticle(); } } else if (action == controller::toInt(controller::Action::CONTEXT_MENU)) { - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); - } else if (action == controller::toInt(controller::Action::UI_NAV_SELECT)) { - if (!offscreenUi->navigationFocused()) { - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); - } + toggleMenuUnderReticle(); } else if (action == controller::toInt(controller::Action::RETICLE_X)) { auto oldPos = getApplicationCompositor().getReticlePosition(); getApplicationCompositor().setReticlePosition({ oldPos.x + state, oldPos.y }); @@ -935,6 +964,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : updateHeartbeat(); loadSettings(); + + // Now that we've loaded the menu and thus switched to the previous display plugin + // we can unlock the desktop repositioning code, since all the positions will be + // relative to the desktop size for this plugin + auto offscreenUi = DependencyManager::get(); + offscreenUi->getDesktop()->setProperty("repositionLocked", false); + // Make sure we don't time out during slow operations at startup updateHeartbeat(); @@ -1060,6 +1096,89 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : } }); + // Add periodic checks to send user activity data + static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; + static int SEND_STATS_INTERVAL_MS = 10000; + static int NEARBY_AVATAR_RADIUS_METERS = 10; + + // Periodically send fps as a user activity event + QTimer* sendStatsTimer = new QTimer(this); + sendStatsTimer->setInterval(SEND_STATS_INTERVAL_MS); + connect(sendStatsTimer, &QTimer::timeout, this, [this]() { + QJsonObject properties = {}; + MemoryInfo memInfo; + if (getMemoryInfo(memInfo)) { + properties["system_memory_total"] = static_cast(memInfo.totalMemoryBytes); + properties["system_memory_used"] = static_cast(memInfo.usedMemoryBytes); + properties["process_memory_used"] = static_cast(memInfo.processUsedMemoryBytes); + } + + auto displayPlugin = qApp->getActiveDisplayPlugin(); + + properties["fps"] = _frameCounter.rate(); + properties["present_rate"] = displayPlugin->presentRate(); + properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate(); + properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); + properties["sim_rate"] = getAverageSimsPerSecond(); + properties["avatar_sim_rate"] = getAvatarSimrate(); + + auto bandwidthRecorder = DependencyManager::get(); + properties["packet_rate_in"] = bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond(); + properties["packet_rate_out"] = bandwidthRecorder->getCachedTotalAverageOutputPacketsPerSecond(); + properties["kbps_in"] = bandwidthRecorder->getCachedTotalAverageInputKilobitsPerSecond(); + properties["kbps_out"] = bandwidthRecorder->getCachedTotalAverageOutputKilobitsPerSecond(); + + auto nodeList = DependencyManager::get(); + SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer); + SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); + SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); + SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer); + SharedNodePointer messagesMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer); + properties["entity_ping"] = entityServerNode ? entityServerNode->getPingMs() : -1; + properties["audio_ping"] = audioMixerNode ? audioMixerNode->getPingMs() : -1; + properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1; + properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1; + properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1; + + auto loadingRequests = ResourceCache::getLoadingRequests(); + properties["active_downloads"] = loadingRequests.size(); + properties["pending_downloads"] = ResourceCache::getPendingRequestCount(); + + properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; + + UserActivityLogger::getInstance().logAction("stats", properties); + }); + sendStatsTimer->start(); + + + // Periodically check for count of nearby avatars + static int lastCountOfNearbyAvatars = -1; + QTimer* checkNearbyAvatarsTimer = new QTimer(this); + checkNearbyAvatarsTimer->setInterval(CHECK_NEARBY_AVATARS_INTERVAL_MS); + connect(checkNearbyAvatarsTimer, &QTimer::timeout, this, [this]() { + auto avatarManager = DependencyManager::get(); + int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getPosition(), + NEARBY_AVATAR_RADIUS_METERS) - 1; + if (nearbyAvatars != lastCountOfNearbyAvatars) { + lastCountOfNearbyAvatars = nearbyAvatars; + UserActivityLogger::getInstance().logAction("nearby_avatars", { { "count", nearbyAvatars } }); + } + }); + checkNearbyAvatarsTimer->start(); + + // Track user activity event when we receive a mute packet + auto onMutedByMixer = []() { + UserActivityLogger::getInstance().logAction("received_mute_packet"); + }; + connect(DependencyManager::get().data(), &AudioClient::mutedByMixer, this, onMutedByMixer); + + // Track when the address bar is opened + auto onAddressBarToggled = [this]() { + // Record time + UserActivityLogger::getInstance().logAction("opened_address_bar", { { "uptime_ms", _sessionRunTimer.elapsed() } }); + }; + connect(DependencyManager::get().data(), &DialogsManager::addressBarToggled, this, onAddressBarToggled); + // Make sure we don't time out during slow operations at startup updateHeartbeat(); @@ -1122,7 +1241,16 @@ QString Application::getUserAgent() { return userAgent; } - +void Application::toggleMenuUnderReticle() const { + // In HMD, if the menu is near the mouse but not under it, the reticle can be at a significantly + // different depth. When you focus on the menu, the cursor can appear to your crossed eyes as both + // on the menu and off. + // Even in 2D, it is arguable whether the user would want the menu to be to the side. + const float X_LEFT_SHIFT = 50.0; + auto offscreenUi = DependencyManager::get(); + auto reticlePosition = getApplicationCompositor().getReticlePosition(); + offscreenUi->toggleMenu(QPoint(reticlePosition.x - X_LEFT_SHIFT, reticlePosition.y)); +} void Application::checkChangeCursor() { QMutexLocker locker(&_changeCursorLock); @@ -1418,7 +1546,7 @@ void Application::initializeUi() { rootContext->setContextProperty("Overlays", &_overlays); rootContext->setContextProperty("Window", DependencyManager::get().data()); - rootContext->setContextProperty("Menu", MenuScriptingInterface::getInstance()); + rootContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); rootContext->setContextProperty("Stats", Stats::getInstance()); rootContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); rootContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); @@ -1459,13 +1587,7 @@ void Application::initializeUi() { }); offscreenUi->resume(); connect(_window, &MainWindow::windowGeometryChanged, [this](const QRect& r){ - static qreal oldDevicePixelRatio = 0; - qreal devicePixelRatio = getActiveDisplayPlugin()->devicePixelRatio(); - if (devicePixelRatio != oldDevicePixelRatio) { - oldDevicePixelRatio = devicePixelRatio; - qDebug() << "Device pixel ratio changed, triggering GL resize"; - resizeGL(); - } + resizeGL(); }); // This will set up the input plugins UI @@ -1597,22 +1719,22 @@ void Application::paintGL() { if (isHMDMode()) { mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); _myCamera.setPosition(extractTranslation(camMat)); - _myCamera.setRotation(glm::quat_cast(camMat)); + _myCamera.setOrientation(glm::quat_cast(camMat)); } else { _myCamera.setPosition(myAvatar->getDefaultEyePosition()); - _myCamera.setRotation(myAvatar->getHead()->getCameraOrientation()); + _myCamera.setOrientation(myAvatar->getHead()->getCameraOrientation()); } } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { if (isHMDMode()) { auto hmdWorldMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - _myCamera.setRotation(glm::normalize(glm::quat_cast(hmdWorldMat))); + _myCamera.setOrientation(glm::normalize(glm::quat_cast(hmdWorldMat))); _myCamera.setPosition(extractTranslation(hmdWorldMat) + myAvatar->getOrientation() * boomOffset); } else { - _myCamera.setRotation(myAvatar->getHead()->getOrientation()); + _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); if (Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + _myCamera.getRotation() * boomOffset); + + _myCamera.getOrientation() * boomOffset); } else { _myCamera.setPosition(myAvatar->getDefaultEyePosition() + myAvatar->getOrientation() * boomOffset); @@ -1631,7 +1753,7 @@ void Application::paintGL() { glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; - _myCamera.setRotation(worldMirrorRotation); + _myCamera.setOrientation(worldMirrorRotation); glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); // Mirror HMD lateral offsets @@ -1642,7 +1764,7 @@ void Application::paintGL() { + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + mirrorBodyOrientation * hmdOffset); } else { - _myCamera.setRotation(myAvatar->getWorldAlignedOrientation() + _myCamera.setOrientation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); _myCamera.setPosition(myAvatar->getDefaultEyePosition() + glm::vec3(0, _raiseMirror * myAvatar->getUniformScale(), 0) @@ -1655,11 +1777,11 @@ void Application::paintGL() { if (cameraEntity != nullptr) { if (isHMDMode()) { glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - _myCamera.setRotation(cameraEntity->getRotation() * hmdRotation); + _myCamera.setOrientation(cameraEntity->getRotation() * hmdRotation); glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); _myCamera.setPosition(cameraEntity->getPosition() + (hmdRotation * hmdOffset)); } else { - _myCamera.setRotation(cameraEntity->getRotation()); + _myCamera.setOrientation(cameraEntity->getRotation()); _myCamera.setPosition(cameraEntity->getPosition()); } } @@ -1840,7 +1962,8 @@ void Application::resizeGL() { static qreal lastDevicePixelRatio = 0; qreal devicePixelRatio = _window->devicePixelRatio(); if (offscreenUi->size() != fromGlm(uiSize) || devicePixelRatio != lastDevicePixelRatio) { - offscreenUi->resize(fromGlm(uiSize)); + qDebug() << "Device pixel ratio changed, triggering resize"; + offscreenUi->resize(fromGlm(uiSize), true); _offscreenContext->makeCurrent(); lastDevicePixelRatio = devicePixelRatio; } @@ -2035,9 +2158,9 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_Return: if (isOption) { if (_window->isFullScreen()) { - _pluginContainer->unsetFullscreen(); + unsetFullscreen(); } else { - _pluginContainer->setFullscreen(nullptr); + setFullscreen(nullptr); } } else { Menu::getInstance()->triggerOption(MenuOption::AddressBar); @@ -2069,7 +2192,8 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_X: if (isShifted && isMeta) { auto offscreenUi = DependencyManager::get(); - offscreenUi->getRootContext()->engine()->clearComponentCache(); + offscreenUi->togglePinned(); + //offscreenUi->getRootContext()->engine()->clearComponentCache(); //OffscreenUi::information("Debugging", "Component cache cleared"); // placeholder for dialogs being converted to QML. } @@ -2343,9 +2467,7 @@ void Application::keyPressEvent(QKeyEvent* event) { void Application::keyReleaseEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Alt && _altPressed && hasFocus()) { - auto offscreenUi = DependencyManager::get(); - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(QPoint(reticlePosition.x, reticlePosition.y)); + toggleMenuUnderReticle(); } _keysPressed.remove(event->key()); @@ -2718,7 +2840,6 @@ void Application::idle(float nsecsElapsed) { if (firstIdle) { firstIdle = false; connect(offscreenUi.data(), &OffscreenUi::showDesktop, this, &Application::showDesktop); - _overlayConductor.setEnabled(Menu::getInstance()->isOptionChecked(MenuOption::Overlays)); } PROFILE_RANGE(__FUNCTION__); @@ -2951,7 +3072,6 @@ void Application::loadSettings() { //DependencyManager::get()->setAutomaticLODAdjust(false); Menu::getInstance()->loadSettings(); - // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. auto pluginManager = PluginManager::getInstance(); auto plugins = pluginManager->getPreferredDisplayPlugins(); @@ -3197,9 +3317,9 @@ void Application::updateMyAvatarLookAtPosition() { if (isLookingAtSomeone) { deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT; } - lookAtSpot = origin + _myCamera.getRotation() * glm::quat(glm::radians(glm::vec3( + lookAtSpot = origin + _myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3( eyePitch * deflection, eyeYaw * deflection, 0.0f))) * - glm::inverse(_myCamera.getRotation()) * (lookAtSpot - origin); + glm::inverse(_myCamera.getOrientation()) * (lookAtSpot - origin); } } @@ -3219,13 +3339,13 @@ void Application::updateThreads(float deltaTime) { } void Application::toggleOverlays() { - auto newOverlaysVisible = !_overlayConductor.getEnabled(); - Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, newOverlaysVisible); - _overlayConductor.setEnabled(newOverlaysVisible); + auto menu = Menu::getInstance(); + menu->setIsOptionChecked(MenuOption::Overlays, menu->isOptionChecked(MenuOption::Overlays)); } void Application::setOverlaysVisible(bool visible) { - _overlayConductor.setEnabled(visible); + auto menu = Menu::getInstance(); + menu->setIsOptionChecked(MenuOption::Overlays, true); } void Application::cycleCamera() { @@ -3915,7 +4035,7 @@ void Application::loadViewFrustum(Camera& camera, ViewFrustum& viewFrustum) { // Set the viewFrustum up with the correct position and orientation of the camera viewFrustum.setPosition(camera.getPosition()); - viewFrustum.setOrientation(camera.getRotation()); + viewFrustum.setOrientation(camera.getOrientation()); // Ask the ViewFrustum class to calculate our corners viewFrustum.calculate(); @@ -4188,7 +4308,7 @@ void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& regi myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_DISTANCE * myAvatar->getScale()); } _mirrorCamera.setProjection(glm::perspective(glm::radians(fov), aspect, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); - _mirrorCamera.setRotation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI, 0.0f))); + _mirrorCamera.setOrientation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI, 0.0f))); // set the bounds of rear mirror view @@ -4210,6 +4330,7 @@ void Application::resetSensors(bool andReload) { DependencyManager::get()->reset(); DependencyManager::get()->reset(); getActiveDisplayPlugin()->resetSensors(); + _overlayConductor.centerUI(); getMyAvatar()->reset(andReload); QMetaObject::invokeMethod(DependencyManager::get().data(), "reset", Qt::QueuedConnection); } @@ -4526,6 +4647,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri RayToOverlayIntersectionResultFromScriptValue); scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, @@ -4574,6 +4696,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); + + scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); } bool Application::canAcceptURL(const QString& urlString) const { @@ -5176,6 +5300,11 @@ void Application::updateDisplayMode() { return; } + UserActivityLogger::getInstance().logAction("changed_display_mode", { + { "previous_display_mode", _displayPlugin ? _displayPlugin->getName() : "" }, + { "display_mode", newDisplayPlugin ? newDisplayPlugin->getName() : "" } + }); + auto offscreenUi = DependencyManager::get(); // Make the switch atomic from the perspective of other threads @@ -5222,7 +5351,6 @@ void Application::updateDisplayMode() { _displayPlugin = newDisplayPlugin; } - emit activeDisplayPluginChanged(); // reset the avatar, to set head and hand palms back to a reasonable default pose. @@ -5300,11 +5428,55 @@ void Application::readArgumentsFromLocalSocket() const { } void Application::showDesktop() { - if (!_overlayConductor.getEnabled()) { - _overlayConductor.setEnabled(true); - } + Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, true); } CompositorHelper& Application::getApplicationCompositor() const { return *DependencyManager::get(); } + + +// virtual functions required for PluginContainer +ui::Menu* Application::getPrimaryMenu() { + auto appMenu = _window->menuBar(); + auto uiMenu = dynamic_cast(appMenu); + return uiMenu; +} + +void Application::showDisplayPluginsTools(bool show) { + DependencyManager::get()->hmdTools(show); +} + +GLWidget* Application::getPrimaryWidget() { + return _glWidget; +} + +MainWindow* Application::getPrimaryWindow() { + return getWindow(); +} + +QOpenGLContext* Application::getPrimaryContext() { + return _glWidget->context()->contextHandle(); +} + +bool Application::makeRenderingContextCurrent() { + return _offscreenContext->makeCurrent(); +} + +void Application::releaseSceneTexture(const gpu::TexturePointer& texture) { + Q_ASSERT(QThread::currentThread() == thread()); + auto& framebufferMap = _lockedFramebufferMap; + Q_ASSERT(framebufferMap.contains(texture)); + auto framebufferPointer = framebufferMap[texture]; + framebufferMap.remove(texture); + auto framebufferCache = DependencyManager::get(); + framebufferCache->releaseFramebuffer(framebufferPointer); +} + +void Application::releaseOverlayTexture(const gpu::TexturePointer& texture) { + _applicationOverlay.releaseOverlay(texture); +} + +bool Application::isForeground() const { + return _isForeground && !_window->isMinimized(); +} diff --git a/interface/src/Application.h b/interface/src/Application.h index 6b6148be32..6857ba2a3a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -86,14 +87,32 @@ class Application; #endif #define qApp (static_cast(QCoreApplication::instance())) -class Application : public QApplication, public AbstractViewStateInterface, public AbstractScriptingServicesInterface, public AbstractUriHandler { +class Application : public QApplication, + public AbstractViewStateInterface, + public AbstractScriptingServicesInterface, + public AbstractUriHandler, + public PluginContainer { Q_OBJECT // TODO? Get rid of those friend class OctreePacketProcessor; - friend class PluginContainerProxy; public: + + // virtual functions required for PluginContainer + virtual ui::Menu* getPrimaryMenu() override; + virtual void requestReset() override { resetSensors(true); } + virtual void showDisplayPluginsTools(bool show) override; + virtual GLWidget* getPrimaryWidget() override; + virtual MainWindow* getPrimaryWindow() override; + virtual QOpenGLContext* getPrimaryContext() override; + virtual bool makeRenderingContextCurrent() override; + virtual void releaseSceneTexture(const gpu::TexturePointer& texture) override; + virtual void releaseOverlayTexture(const gpu::TexturePointer& texture) override; + virtual bool isForeground() const override; + + virtual DisplayPluginPointer getActiveDisplayPlugin() const override; + enum Event { Present = DisplayPlugin::Present, Paint = Present + 1, @@ -163,7 +182,6 @@ public: Overlays& getOverlays() { return _overlays; } - bool isForeground() const { return _isForeground; } size_t getFrameCount() const { return _frameCount; } float getFps() const { return _frameCounter.rate(); } @@ -185,8 +203,6 @@ public: void setActiveDisplayPlugin(const QString& pluginName); - DisplayPluginPointer getActiveDisplayPlugin() const; - FileLogger* getLogger() const { return _logger; } glm::vec2 getViewportDimensions() const; @@ -195,6 +211,8 @@ public: float getRenderResolutionScale() const; + qint64 getCurrentSessionRuntime() const { return _sessionRunTimer.elapsed(); } + bool isAboutToQuit() const { return _aboutToQuit; } // the isHMDMode is true whenever we use the interface from an HMD and not a standard flat display @@ -390,6 +408,7 @@ private: static void dragEnterEvent(QDragEnterEvent* event); void maybeToggleMenuVisible(QMouseEvent* event) const; + void toggleMenuUnderReticle() const; MainWindow* _window; QElapsedTimer& _sessionRunTimer; diff --git a/interface/src/Camera.cpp b/interface/src/Camera.cpp index 53a3500bff..227bdadb97 100644 --- a/interface/src/Camera.cpp +++ b/interface/src/Camera.cpp @@ -62,14 +62,14 @@ void Camera::update(float deltaTime) { } void Camera::recompose() { - mat4 orientation = glm::mat4_cast(_rotation); + mat4 orientation = glm::mat4_cast(_orientation); mat4 translation = glm::translate(mat4(), _position); _transform = translation * orientation; } void Camera::decompose() { _position = vec3(_transform[3]); - _rotation = glm::quat_cast(_transform); + _orientation = glm::quat_cast(_transform); } void Camera::setTransform(const glm::mat4& transform) { @@ -85,8 +85,8 @@ void Camera::setPosition(const glm::vec3& position) { } } -void Camera::setRotation(const glm::quat& rotation) { - _rotation = rotation; +void Camera::setOrientation(const glm::quat& orientation) { + _orientation = orientation; recompose(); if (_isKeepLookingAt) { lookAt(_lookingAt); @@ -154,9 +154,9 @@ QString Camera::getModeString() const { void Camera::lookAt(const glm::vec3& lookAt) { glm::vec3 up = IDENTITY_UP; glm::mat4 lookAtMatrix = glm::lookAt(_position, lookAt, up); - glm::quat rotation = glm::quat_cast(lookAtMatrix); - rotation.w = -rotation.w; // Rosedale approved - _rotation = rotation; + glm::quat orientation = glm::quat_cast(lookAtMatrix); + orientation.w = -orientation.w; // Rosedale approved + _orientation = orientation; } void Camera::keepLookingAt(const glm::vec3& point) { @@ -171,7 +171,7 @@ void Camera::loadViewFrustum(ViewFrustum& frustum) const { // Set the viewFrustum up with the correct position and orientation of the camera frustum.setPosition(getPosition()); - frustum.setOrientation(getRotation()); + frustum.setOrientation(getOrientation()); // Ask the ViewFrustum class to calculate our corners frustum.calculate(); diff --git a/interface/src/Camera.h b/interface/src/Camera.h index 486b98c100..46cad2efc8 100644 --- a/interface/src/Camera.h +++ b/interface/src/Camera.h @@ -45,7 +45,7 @@ class Camera : public QObject { public: Camera(); - void initialize(); // instantly put the camera at the ideal position and rotation. + void initialize(); // instantly put the camera at the ideal position and orientation. void update( float deltaTime ); @@ -57,25 +57,22 @@ public: EntityItemPointer getCameraEntityPointer() const { return _cameraEntity; } -public slots: - QString getModeString() const; - void setModeString(const QString& mode); - - glm::quat getRotation() const { return _rotation; } - void setRotation(const glm::quat& rotation); - - glm::vec3 getPosition() const { return _position; } - void setPosition(const glm::vec3& position); - - glm::quat getOrientation() const { return getRotation(); } - void setOrientation(const glm::quat& orientation) { setRotation(orientation); } - const glm::mat4& getTransform() const { return _transform; } void setTransform(const glm::mat4& transform); const glm::mat4& getProjection() const { return _projection; } void setProjection(const glm::mat4& projection); +public slots: + QString getModeString() const; + void setModeString(const QString& mode); + + glm::vec3 getPosition() const { return _position; } + void setPosition(const glm::vec3& position); + + glm::quat getOrientation() const { return _orientation; } + void setOrientation(const glm::quat& orientation); + QUuid getCameraEntity() const; void setCameraEntity(QUuid entityID); @@ -105,7 +102,7 @@ private: // derived glm::vec3 _position; - glm::quat _rotation; + glm::quat _orientation; bool _isKeepLookingAt{ false }; glm::vec3 _lookingAt; EntityItemPointer _cameraEntity; diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 24256fdf39..c4d985419e 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -80,7 +80,8 @@ void DiscoverabilityManager::updateLocation() { locationObject.insert(FRIENDS_ONLY_KEY_IN_LOCATION, (_mode.get() == Discoverability::Friends)); // if we have a session ID add it now, otherwise add a null value - rootObject[SESSION_ID_KEY] = _sessionID.isEmpty() ? QJsonValue() : _sessionID; + auto sessionID = accountManager->getSessionID(); + rootObject[SESSION_ID_KEY] = sessionID.isNull() ? QJsonValue() : sessionID.toString(); JSONCallbackParameters callbackParameters; callbackParameters.jsonCallbackReceiver = this; @@ -110,11 +111,8 @@ void DiscoverabilityManager::updateLocation() { callbackParameters.jsonCallbackMethod = "handleHeartbeatResponse"; QJsonObject heartbeatObject; - if (!_sessionID.isEmpty()) { - heartbeatObject[SESSION_ID_KEY] = _sessionID; - } else { - heartbeatObject[SESSION_ID_KEY] = QJsonValue(); - } + auto sessionID = accountManager->getSessionID(); + heartbeatObject[SESSION_ID_KEY] = sessionID.isNull() ? QJsonValue() : sessionID.toString(); accountManager->sendRequest(API_USER_HEARTBEAT_PATH, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, callbackParameters, @@ -126,11 +124,11 @@ void DiscoverabilityManager::handleHeartbeatResponse(QNetworkReply& requestReply auto dataObject = AccountManager::dataObjectFromResponse(requestReply); if (!dataObject.isEmpty()) { - _sessionID = dataObject[SESSION_ID_KEY].toString(); + auto sessionID = dataObject[SESSION_ID_KEY].toString(); // give that session ID to the account manager auto accountManager = DependencyManager::get(); - accountManager->setSessionID(_sessionID); + accountManager->setSessionID(sessionID); } } diff --git a/interface/src/DiscoverabilityManager.h b/interface/src/DiscoverabilityManager.h index 9a1fa7b39c..196b0cdf81 100644 --- a/interface/src/DiscoverabilityManager.h +++ b/interface/src/DiscoverabilityManager.h @@ -49,7 +49,6 @@ private: DiscoverabilityManager(); Setting::Handle _mode; - QString _sessionID; QJsonObject _lastLocationObject; }; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index b37f70f65d..6308ac6c73 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -256,8 +256,7 @@ Menu::Menu() { UNSPECIFIED_POSITION, "Advanced"); // View > Overlays - addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Overlays, 0, true, - qApp, SLOT(setOverlaysVisible(bool))); + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Overlays, 0, true); // Navigate menu ---------------------------------- MenuWrapper* navigateMenu = addMenu("Navigate"); diff --git a/interface/src/PluginContainerProxy.cpp b/interface/src/PluginContainerProxy.cpp deleted file mode 100644 index b651a1520d..0000000000 --- a/interface/src/PluginContainerProxy.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include "PluginContainerProxy.h" - -#include -#include - -#include -#include -#include -#include -#include - -#include "Application.h" -#include "MainWindow.h" -#include "GLCanvas.h" -#include "ui/DialogsManager.h" - -#include -#include - -PluginContainerProxy::PluginContainerProxy() { -} - -PluginContainerProxy::~PluginContainerProxy() { -} - -ui::Menu* PluginContainerProxy::getPrimaryMenu() { - auto appMenu = qApp->_window->menuBar(); - auto uiMenu = dynamic_cast(appMenu); - return uiMenu; -} - -bool PluginContainerProxy::isForeground() { - return qApp->isForeground() && !qApp->getWindow()->isMinimized(); -} - -void PluginContainerProxy::requestReset() { - // We could signal qApp to sequence this, but it turns out that requestReset is only used from within the main thread anyway. - qApp->resetSensors(true); -} - -void PluginContainerProxy::showDisplayPluginsTools(bool show) { - DependencyManager::get()->hmdTools(show); -} - -GLWidget* PluginContainerProxy::getPrimaryWidget() { - return qApp->_glWidget; -} - -MainWindow* PluginContainerProxy::getPrimaryWindow() { - return qApp->getWindow(); -} - -QOpenGLContext* PluginContainerProxy::getPrimaryContext() { - return qApp->_glWidget->context()->contextHandle(); -} - -const DisplayPluginPointer PluginContainerProxy::getActiveDisplayPlugin() const { - return qApp->getActiveDisplayPlugin(); -} - -bool PluginContainerProxy::makeRenderingContextCurrent() { - return qApp->_offscreenContext->makeCurrent(); -} - -void PluginContainerProxy::releaseSceneTexture(const gpu::TexturePointer& texture) { - Q_ASSERT(QThread::currentThread() == qApp->thread()); - auto& framebufferMap = qApp->_lockedFramebufferMap; - Q_ASSERT(framebufferMap.contains(texture)); - auto framebufferPointer = framebufferMap[texture]; - framebufferMap.remove(texture); - auto framebufferCache = DependencyManager::get(); - framebufferCache->releaseFramebuffer(framebufferPointer); -} - -void PluginContainerProxy::releaseOverlayTexture(const gpu::TexturePointer& texture) { - qApp->_applicationOverlay.releaseOverlay(texture); -} - diff --git a/interface/src/PluginContainerProxy.h b/interface/src/PluginContainerProxy.h deleted file mode 100644 index a04a1b2977..0000000000 --- a/interface/src/PluginContainerProxy.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -#ifndef hifi_PluginContainerProxy_h -#define hifi_PluginContainerProxy_h - -#include -#include - -#include -#include - -class QActionGroup; - -class PluginContainerProxy : public QObject, PluginContainer { - Q_OBJECT - PluginContainerProxy(); - virtual ~PluginContainerProxy(); - virtual void showDisplayPluginsTools(bool show = true) override; - virtual void requestReset() override; - virtual bool makeRenderingContextCurrent() override; - virtual void releaseSceneTexture(const gpu::TexturePointer& texture) override; - virtual void releaseOverlayTexture(const gpu::TexturePointer& texture) override; - virtual GLWidget* getPrimaryWidget() override; - virtual MainWindow* getPrimaryWindow() override; - virtual ui::Menu* getPrimaryMenu() override; - virtual QOpenGLContext* getPrimaryContext() override; - virtual bool isForeground() override; - virtual const DisplayPluginPointer getActiveDisplayPlugin() const override; - - friend class Application; - -}; - -#endif diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index be5bf7722f..6a69ee9a9a 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -722,7 +722,7 @@ void MyAvatar::saveData() { settings.setValue("displayName", _displayName); settings.setValue("collisionSoundURL", _collisionSoundURL); settings.setValue("useSnapTurn", _useSnapTurn); - settings.setValue("clearOverlayWhenDriving", _clearOverlayWhenDriving); + settings.setValue("clearOverlayWhenMoving", _clearOverlayWhenMoving); settings.endGroup(); } @@ -842,7 +842,7 @@ void MyAvatar::loadData() { setDisplayName(settings.value("displayName").toString()); setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString()); setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool()); - setClearOverlayWhenDriving(settings.value("clearOverlayWhenDriving", _clearOverlayWhenDriving).toBool()); + setClearOverlayWhenMoving(settings.value("clearOverlayWhenMoving", _clearOverlayWhenMoving).toBool()); settings.endGroup(); @@ -1248,8 +1248,7 @@ void MyAvatar::prepareForPhysicsSimulation() { _characterController.setPositionAndOrientation(getPosition(), getOrientation()); if (qApp->isHMDMode()) { - bool hasDriveInput = fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; - _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput); + _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput()); } else { _follow.deactivate(); } @@ -2134,3 +2133,7 @@ bool MyAvatar::didTeleport() { lastPosition = pos; return (changeInPosition.length() > MAX_AVATAR_MOVEMENT_PER_FRAME); } + +bool MyAvatar::hasDriveInput() const { + return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Y]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; +} diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 05afe39a32..96fa999de5 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -159,8 +159,8 @@ public: Q_INVOKABLE bool getSnapTurn() const { return _useSnapTurn; } Q_INVOKABLE void setSnapTurn(bool on) { _useSnapTurn = on; } - Q_INVOKABLE bool getClearOverlayWhenDriving() const { return _clearOverlayWhenDriving; } - Q_INVOKABLE void setClearOverlayWhenDriving(bool on) { _clearOverlayWhenDriving = on; } + Q_INVOKABLE bool getClearOverlayWhenMoving() const { return _clearOverlayWhenMoving; } + Q_INVOKABLE void setClearOverlayWhenMoving(bool on) { _clearOverlayWhenMoving = on; } Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } @@ -266,6 +266,8 @@ public: controller::Pose getLeftHandControllerPoseInAvatarFrame() const; controller::Pose getRightHandControllerPoseInAvatarFrame() const; + bool hasDriveInput() const; + Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); Q_INVOKABLE bool getCharacterControllerEnabled(); @@ -403,7 +405,7 @@ private: QString _fullAvatarModelName; QUrl _animGraphUrl {""}; bool _useSnapTurn { true }; - bool _clearOverlayWhenDriving { false }; + bool _clearOverlayWhenMoving { true }; // cache of the current HMD sensor position and orientation // in sensor space. diff --git a/interface/src/scripting/DesktopScriptingInterface.cpp b/interface/src/scripting/DesktopScriptingInterface.cpp index 843a40348e..f7bc8afe36 100644 --- a/interface/src/scripting/DesktopScriptingInterface.cpp +++ b/interface/src/scripting/DesktopScriptingInterface.cpp @@ -16,6 +16,7 @@ #include "Application.h" #include "MainWindow.h" +#include int DesktopScriptingInterface::getWidth() { QSize size = qApp->getWindow()->windowHandle()->screen()->virtualSize(); @@ -25,3 +26,8 @@ int DesktopScriptingInterface::getHeight() { QSize size = qApp->getWindow()->windowHandle()->screen()->virtualSize(); return size.height(); } + +void DesktopScriptingInterface::setOverlayAlpha(float alpha) { + qApp->getApplicationCompositor().setAlpha(alpha); +} + diff --git a/interface/src/scripting/DesktopScriptingInterface.h b/interface/src/scripting/DesktopScriptingInterface.h index be4eaadbfb..8da502cb11 100644 --- a/interface/src/scripting/DesktopScriptingInterface.h +++ b/interface/src/scripting/DesktopScriptingInterface.h @@ -22,6 +22,8 @@ class DesktopScriptingInterface : public QObject, public Dependency { Q_PROPERTY(int height READ getHeight) // Physical height of screen(s) including task bars and system menus public: + Q_INVOKABLE void setOverlayAlpha(float alpha); + int getWidth(); int getHeight(); }; diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index 7bf1547a3c..e9677cc3c8 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -19,6 +19,9 @@ #include "Application.h" HMDScriptingInterface::HMDScriptingInterface() { + connect(qApp, &Application::activeDisplayPluginChanged, [this]{ + emit displayModeChanged(isHMDMode()); + }); } glm::vec3 HMDScriptingInterface::calculateRayUICollisionPoint(const glm::vec3& position, const glm::vec3& direction) const { @@ -105,3 +108,25 @@ QString HMDScriptingInterface::preferredAudioInput() const { QString HMDScriptingInterface::preferredAudioOutput() const { return qApp->getActiveDisplayPlugin()->getPreferredAudioOutDevice(); } + +bool HMDScriptingInterface::setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const { + return qApp->getActiveDisplayPlugin()->setHandLaser(hands, + enabled ? DisplayPlugin::HandLaserMode::Overlay : DisplayPlugin::HandLaserMode::None, + color, direction); +} + +void HMDScriptingInterface::disableHandLasers(int hands) const { + qApp->getActiveDisplayPlugin()->setHandLaser(hands, DisplayPlugin::HandLaserMode::None); +} + +bool HMDScriptingInterface::suppressKeyboard() { + return qApp->getActiveDisplayPlugin()->suppressKeyboard(); +} + +void HMDScriptingInterface::unsuppressKeyboard() { + qApp->getActiveDisplayPlugin()->unsuppressKeyboard(); +} + +bool HMDScriptingInterface::isKeyboardVisible() { + return qApp->getActiveDisplayPlugin()->isKeyboardVisible(); +} diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index d4c7b7cc0e..2739522adf 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -1,4 +1,4 @@ -// + // HMDScriptingInterface.h // interface/src/scripting // @@ -12,6 +12,8 @@ #ifndef hifi_HMDScriptingInterface_h #define hifi_HMDScriptingInterface_h +#include + #include class QScriptContext; class QScriptEngine; @@ -31,12 +33,28 @@ public: Q_INVOKABLE glm::vec3 calculateRayUICollisionPoint(const glm::vec3& position, const glm::vec3& direction) const; Q_INVOKABLE glm::vec2 overlayFromWorldPoint(const glm::vec3& position) const; Q_INVOKABLE glm::vec3 worldPointFromOverlay(const glm::vec2& overlay) const; - Q_INVOKABLE glm::vec2 sphericalToOverlay(const glm::vec2 & sphericalPos) const; Q_INVOKABLE glm::vec2 overlayToSpherical(const glm::vec2 & overlayPos) const; Q_INVOKABLE QString preferredAudioInput() const; Q_INVOKABLE QString preferredAudioOutput() const; + Q_INVOKABLE bool setHandLasers(int hands, bool enabled, const glm::vec4& color, const glm::vec3& direction) const; + + Q_INVOKABLE void disableHandLasers(int hands) const; + /// Suppress the activation of any on-screen keyboard so that a script operation will + /// not be interrupted by a keyboard popup + /// Returns false if there is already an active keyboard displayed. + /// Clients should re-enable the keyboard when the operation is complete and ensure + /// that they balance any call to suppressKeyboard() that returns true with a corresponding + /// call to unsuppressKeyboard() within a reasonable amount of time + Q_INVOKABLE bool suppressKeyboard(); + + /// Enable the keyboard following a suppressKeyboard call + Q_INVOKABLE void unsuppressKeyboard(); + + /// Query the display plugin to determine the current VR keyboard visibility + Q_INVOKABLE bool isKeyboardVisible(); + public: HMDScriptingInterface(); static QScriptValue getHUDLookAtPosition2D(QScriptContext* context, QScriptEngine* engine); diff --git a/interface/src/scripting/ToolbarScriptingInterface.cpp b/interface/src/scripting/ToolbarScriptingInterface.cpp new file mode 100644 index 0000000000..82332b3187 --- /dev/null +++ b/interface/src/scripting/ToolbarScriptingInterface.cpp @@ -0,0 +1,115 @@ +// +// Created by Bradley Austin Davis on 2016-06-16 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ToolbarScriptingInterface.h" + +#include + +class QmlWrapper : public QObject { + Q_OBJECT +public: + QmlWrapper(QObject* qmlObject, QObject* parent = nullptr) + : QObject(parent), _qmlObject(qmlObject) { + } + + Q_INVOKABLE void writeProperty(QString propertyName, QVariant propertyValue) { + auto offscreenUi = DependencyManager::get(); + offscreenUi->executeOnUiThread([=] { + _qmlObject->setProperty(propertyName.toStdString().c_str(), propertyValue); + }); + } + + Q_INVOKABLE void writeProperties(QVariant propertyMap) { + auto offscreenUi = DependencyManager::get(); + offscreenUi->executeOnUiThread([=] { + QVariantMap map = propertyMap.toMap(); + for (const QString& key : map.keys()) { + _qmlObject->setProperty(key.toStdString().c_str(), map[key]); + } + }); + } + + Q_INVOKABLE QVariant readProperty(const QString& propertyName) { + auto offscreenUi = DependencyManager::get(); + return offscreenUi->returnFromUiThread([&]()->QVariant { + return _qmlObject->property(propertyName.toStdString().c_str()); + }); + } + + Q_INVOKABLE QVariant readProperties(const QVariant& propertyList) { + auto offscreenUi = DependencyManager::get(); + return offscreenUi->returnFromUiThread([&]()->QVariant { + QVariantMap result; + for (const QVariant& property : propertyList.toList()) { + QString propertyString = property.toString(); + result.insert(propertyString, _qmlObject->property(propertyString.toStdString().c_str())); + } + return result; + }); + } + + +protected: + QObject* _qmlObject{ nullptr }; +}; + + +class ToolbarButtonProxy : public QmlWrapper { + Q_OBJECT + +public: + ToolbarButtonProxy(QObject* qmlObject, QObject* parent = nullptr) : QmlWrapper(qmlObject, parent) { + connect(qmlObject, SIGNAL(clicked()), this, SIGNAL(clicked())); + } + +signals: + void clicked(); +}; + +class ToolbarProxy : public QmlWrapper { + Q_OBJECT + +public: + ToolbarProxy(QObject* qmlObject, QObject* parent = nullptr) : QmlWrapper(qmlObject, parent) { } + + Q_INVOKABLE QObject* addButton(const QVariant& properties) { + QVariant resultVar; + bool invokeResult = QMetaObject::invokeMethod(_qmlObject, "addButton", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, properties)); + if (!invokeResult) { + return nullptr; + } + + QObject* rawButton = qvariant_cast(resultVar); + if (!rawButton) { + return nullptr; + } + + return new ToolbarButtonProxy(rawButton, this); + } +}; + + +QObject* ToolbarScriptingInterface::getToolbar(const QString& toolbarId) { + auto offscreenUi = DependencyManager::get(); + auto desktop = offscreenUi->getDesktop(); + QVariant resultVar; + bool invokeResult = QMetaObject::invokeMethod(desktop, "getToolbar", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, toolbarId)); + if (!invokeResult) { + return nullptr; + } + + QObject* rawToolbar = qvariant_cast(resultVar); + if (!rawToolbar) { + return nullptr; + } + + return new ToolbarProxy(rawToolbar); +} + + +#include "ToolbarScriptingInterface.moc" \ No newline at end of file diff --git a/interface/src/scripting/ToolbarScriptingInterface.h b/interface/src/scripting/ToolbarScriptingInterface.h new file mode 100644 index 0000000000..9379284e55 --- /dev/null +++ b/interface/src/scripting/ToolbarScriptingInterface.h @@ -0,0 +1,26 @@ +// +// Created by Bradley Austin Davis on 2016-06-16 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ToolbarScriptingInterface_h +#define hifi_ToolbarScriptingInterface_h + +#include + +#include + +#include + +class ToolbarProxy; + +class ToolbarScriptingInterface : public QObject, public Dependency { + Q_OBJECT +public: + Q_INVOKABLE QObject* getToolbar(const QString& toolbarId); +}; + +#endif // hifi_ToolbarScriptingInterface_h diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 2395f62468..6d5df31766 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -150,7 +150,8 @@ void ApplicationOverlay::renderRearViewToFbo(RenderArgs* renderArgs) { } void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { - if (!qApp->isHMDMode() && Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror)) { + if (!qApp->isHMDMode() && Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && + !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { gpu::Batch& batch = *renderArgs->_batch; auto geometryCache = DependencyManager::get(); @@ -166,7 +167,7 @@ void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { batch.setViewTransform(Transform()); float screenRatio = ((float)qApp->getDevicePixelRatio()); - float renderRatio = ((float)screenRatio * qApp->getRenderResolutionScale()); + float renderRatio = ((float)qApp->getRenderResolutionScale()); auto viewport = qApp->getMirrorViewRect(); glm::vec2 bottomLeft(viewport.left(), viewport.top() + viewport.height()); diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index 83d729779c..2ee106b6b3 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -17,179 +17,137 @@ #include "OverlayConductor.h" OverlayConductor::OverlayConductor() { + } OverlayConductor::~OverlayConductor() { } -void OverlayConductor::update(float dt) { +bool OverlayConductor::headOutsideOverlay() const { + glm::mat4 hmdMat = qApp->getHMDSensorPose(); + glm::vec3 hmdPos = extractTranslation(hmdMat); + glm::vec3 hmdForward = transformVectorFast(hmdMat, glm::vec3(0.0f, 0.0f, -1.0f)); - updateMode(); + Transform uiTransform = qApp->getApplicationCompositor().getModelTransform(); + glm::vec3 uiPos = uiTransform.getTranslation(); + glm::vec3 uiForward = uiTransform.getRotation() * glm::vec3(0.0f, 0.0f, -1.0f); - switch (_mode) { - case SITTING: { - // when sitting, the overlay is at the origin, facing down the -z axis. - // the camera is taken directly from the HMD. - Transform identity; - qApp->getApplicationCompositor().setModelTransform(identity); - qApp->getApplicationCompositor().setCameraBaseTransform(identity); - break; - } - case STANDING: { - // when standing, the overlay is at a reference position, which is set when the overlay is - // enabled. The camera is taken directly from the HMD, but in world space. - // So the sensorToWorldMatrix must be applied. - MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - Transform t; - t.evalFromRawMatrix(myAvatar->getSensorToWorldMatrix()); - qApp->getApplicationCompositor().setCameraBaseTransform(t); - - // detect when head moves out side of sweet spot, or looks away. - mat4 headMat = myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose(); - vec3 headWorldPos = extractTranslation(headMat); - vec3 headForward = glm::quat_cast(headMat) * glm::vec3(0.0f, 0.0f, -1.0f); - Transform modelXform = qApp->getApplicationCompositor().getModelTransform(); - vec3 compositorWorldPos = modelXform.getTranslation(); - vec3 compositorForward = modelXform.getRotation() * glm::vec3(0.0f, 0.0f, -1.0f); - const float MAX_COMPOSITOR_DISTANCE = 0.6f; - const float MAX_COMPOSITOR_ANGLE = 110.0f; - if (_enabled && (glm::distance(headWorldPos, compositorWorldPos) > MAX_COMPOSITOR_DISTANCE || - glm::dot(headForward, compositorForward) < cosf(glm::radians(MAX_COMPOSITOR_ANGLE)))) { - // fade out the overlay - setEnabled(false); - } - break; - } - case FLAT: - // do nothing - break; + const float MAX_COMPOSITOR_DISTANCE = 0.99f; // If you're 1m from center of ui sphere, you're at the surface. + const float MAX_COMPOSITOR_ANGLE = 180.0f; // rotation check is effectively disabled + if (glm::distance(uiPos, hmdPos) > MAX_COMPOSITOR_DISTANCE || + glm::dot(uiForward, hmdForward) < cosf(glm::radians(MAX_COMPOSITOR_ANGLE))) { + return true; } + return false; } -void OverlayConductor::updateMode() { +bool OverlayConductor::updateAvatarIsAtRest() { + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - if (myAvatar->getClearOverlayWhenDriving()) { - float speed = glm::length(myAvatar->getVelocity()); - const float MIN_DRIVING = 0.2f; - const float MAX_NOT_DRIVING = 0.01f; - const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE = 200 * 1000; - const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE = 1000 * 1000; - bool nowDriving = _driving; // Assume current _driving mode unless... - if (speed > MIN_DRIVING) { // ... we're definitely moving... - nowDriving = true; - } else if (speed < MAX_NOT_DRIVING) { // ... or definitely not. - nowDriving = false; - } - // Check that we're in this new mode for long enough to really trigger a transition. - if (nowDriving == _driving) { // If there's no change in state, clear any attepted timer. - _timeInPotentialMode = 0; - } else if (_timeInPotentialMode == 0) { // We've just changed with no timer, so start timing now. - _timeInPotentialMode = usecTimestampNow(); - } else if ((usecTimestampNow() - _timeInPotentialMode) > (nowDriving ? REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE : REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE)) { - _timeInPotentialMode = 0; // a real transition - if (nowDriving) { - _wantsOverlays = Menu::getInstance()->isOptionChecked(MenuOption::Overlays); - } else { // reset when coming out of driving - _mode = FLAT; // Seems appropriate to let things reset, below, after the following. - // All reset of, e.g., room-scale location as though by apostrophe key, without all the other adjustments. - qApp->getActiveDisplayPlugin()->resetSensors(); - myAvatar->reset(true, false, false); - } - if (_wantsOverlays) { - setEnabled(!nowDriving, false); - } - _driving = nowDriving; - } // Else haven't accumulated enough time in new mode, but keep timing. + + const quint64 REST_ENABLE_TIME_USECS = 1000 * 1000; // 1 s + const quint64 REST_DISABLE_TIME_USECS = 200 * 1000; // 200 ms + + const float AT_REST_THRESHOLD = 0.01f; + bool desiredAtRest = glm::length(myAvatar->getVelocity()) < AT_REST_THRESHOLD; + if (desiredAtRest != _desiredAtRest) { + // start timer + _desiredAtRestTimer = usecTimestampNow() + (desiredAtRest ? REST_ENABLE_TIME_USECS : REST_DISABLE_TIME_USECS); } - Mode newMode; - if (qApp->isHMDMode()) { - newMode = SITTING; + _desiredAtRest = desiredAtRest; + + if (_desiredAtRestTimer != 0 && usecTimestampNow() > _desiredAtRestTimer) { + // timer expired + // change state! + _currentAtRest = _desiredAtRest; + // disable timer + _desiredAtRestTimer = 0; + } + + return _currentAtRest; +} + +bool OverlayConductor::updateAvatarHasDriveInput() { + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + + const quint64 DRIVE_ENABLE_TIME_USECS = 200 * 1000; // 200 ms + const quint64 DRIVE_DISABLE_TIME_USECS = 1000 * 1000; // 1 s + + bool desiredDriving = myAvatar->hasDriveInput(); + if (desiredDriving != _desiredDriving) { + // start timer + _desiredDrivingTimer = usecTimestampNow() + (desiredDriving ? DRIVE_ENABLE_TIME_USECS : DRIVE_DISABLE_TIME_USECS); + } + + _desiredDriving = desiredDriving; + + if (_desiredDrivingTimer != 0 && usecTimestampNow() > _desiredDrivingTimer) { + // timer expired + // change state! + _currentDriving = _desiredDriving; + // disable timer + _desiredDrivingTimer = 0; + } + + return _currentDriving; +} + +void OverlayConductor::centerUI() { + // place the overlay at the current hmd position in sensor space + auto camMat = cancelOutRollAndPitch(qApp->getHMDSensorPose()); + qApp->getApplicationCompositor().setModelTransform(Transform(camMat)); +} + +void OverlayConductor::update(float dt) { + auto offscreenUi = DependencyManager::get(); + bool currentVisible = !offscreenUi->getDesktop()->property("pinned").toBool(); + + MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); + // centerUI when hmd mode is first enabled and mounted + if (qApp->isHMDMode() && qApp->getActiveDisplayPlugin()->isDisplayVisible()) { + if (!_hmdMode) { + _hmdMode = true; + centerUI(); + } } else { - newMode = FLAT; + _hmdMode = false; } - if (newMode != _mode) { - switch (newMode) { - case SITTING: { - // enter the SITTING state - // place the overlay at origin - Transform identity; - qApp->getApplicationCompositor().setModelTransform(identity); - break; - } - case STANDING: { // STANDING mode is not currently used. - // enter the STANDING state - // place the overlay at the current hmd position in world space - auto camMat = cancelOutRollAndPitch(myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose()); - Transform t; - t.setTranslation(extractTranslation(camMat)); - t.setRotation(glm::quat_cast(camMat)); - qApp->getApplicationCompositor().setModelTransform(t); - break; - } + bool prevDriving = _currentDriving; + bool isDriving = updateAvatarHasDriveInput(); + bool drivingChanged = prevDriving != isDriving; + bool isAtRest = updateAvatarIsAtRest(); + bool shouldRecenter = false; - case FLAT: - // do nothing - break; + if (_flags & SuppressedByDrive) { + if (!isDriving) { + _flags &= ~SuppressedByDrive; + shouldRecenter = true; + } + } else { + if (myAvatar->getClearOverlayWhenMoving() && drivingChanged && isDriving) { + _flags |= SuppressedByDrive; } } - _mode = newMode; - -} - -void OverlayConductor::setEnabled(bool enabled, bool toggleQmlEvents) { - - if (enabled == _enabled) { - return; + if (_flags & SuppressedByHead) { + if (isAtRest) { + _flags &= ~SuppressedByHead; + shouldRecenter = true; + } + } else { + if (_hmdMode && headOutsideOverlay()) { + _flags |= SuppressedByHead; + } } - if (toggleQmlEvents) { // Could recurse on us with the wrong toggleQmlEvents flag, and not need in the !toggleQmlEvent case anyway. - Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, enabled); + + bool targetVisible = Menu::getInstance()->isOptionChecked(MenuOption::Overlays) && (0 == (_flags & SuppressMask)); + if (targetVisible != currentVisible) { + offscreenUi->setPinned(!targetVisible); } - - _enabled = enabled; // set the new value - - // if the new state is visible/enabled... - if (_enabled) { - // alpha fadeIn the overlay mesh. - qApp->getApplicationCompositor().fadeIn(); - - // enable mouse clicks from script - qApp->getOverlays().enable(); - - // enable QML events - if (toggleQmlEvents) { - auto offscreenUi = DependencyManager::get(); - offscreenUi->getRootItem()->setEnabled(true); - } - - if (_mode == STANDING) { - // place the overlay at the current hmd position in world space - MyAvatar* myAvatar = DependencyManager::get()->getMyAvatar(); - auto camMat = cancelOutRollAndPitch(myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose()); - Transform t; - t.setTranslation(extractTranslation(camMat)); - t.setRotation(glm::quat_cast(camMat)); - qApp->getApplicationCompositor().setModelTransform(t); - } - } else { // other wise, if the new state is hidden/not enabled - // alpha fadeOut the overlay mesh. - qApp->getApplicationCompositor().fadeOut(); - - // disable mouse clicks from script - qApp->getOverlays().disable(); - - // disable QML events - if (toggleQmlEvents) { // I'd really rather always do this, but it looses drive state. bugzid:501 - auto offscreenUi = DependencyManager::get(); - offscreenUi->getRootItem()->setEnabled(false); - } + if (shouldRecenter && !_flags) { + centerUI(); } } - -bool OverlayConductor::getEnabled() const { - return _enabled; -} - diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index 99f4b56584..1bdfe2ed79 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -17,23 +17,31 @@ public: ~OverlayConductor(); void update(float dt); - void setEnabled(bool enable, bool toggleQmlEvents = true); - bool getEnabled() const; + void centerUI(); private: - void updateMode(); + bool headOutsideOverlay() const; + bool updateAvatarHasDriveInput(); + bool updateAvatarIsAtRest(); - enum Mode { - FLAT, - SITTING, - STANDING + enum SupressionFlags { + SuppressedByDrive = 0x01, + SuppressedByHead = 0x02, + SuppressMask = 0x03, }; - Mode _mode { FLAT }; - bool _enabled { false }; - bool _driving { false }; - quint64 _timeInPotentialMode { 0 }; - bool _wantsOverlays { true }; + uint8_t _flags { SuppressedByDrive }; + bool _hmdMode { false }; + + // used by updateAvatarHasDriveInput + quint64 _desiredDrivingTimer { 0 }; + bool _desiredDriving { false }; + bool _currentDriving { false }; + + // used by updateAvatarIsAtRest + quint64 _desiredAtRestTimer { 0 }; + bool _desiredAtRest { true }; + bool _currentAtRest { true }; }; #endif diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 6decef3240..c1705da206 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -62,9 +62,9 @@ void setupPreferences() { preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Snap turn when in HMD", getter, setter)); } { - auto getter = [=]()->bool {return myAvatar->getClearOverlayWhenDriving(); }; - auto setter = [=](bool value) { myAvatar->setClearOverlayWhenDriving(value); }; - preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when driving", getter, setter)); + auto getter = [=]()->bool {return myAvatar->getClearOverlayWhenMoving(); }; + auto setter = [=](bool value) { myAvatar->setClearOverlayWhenMoving(value); }; + preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when moving", getter, setter)); } { auto getter = []()->QString { return Snapshot::snapshotsLocation.get(); }; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index b21f5a0e84..8aee2245c0 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -163,7 +163,6 @@ void Rig::destroyAnimGraph() { } void Rig::initJointStates(const FBXGeometry& geometry, const glm::mat4& modelOffset) { - _geometryOffset = AnimPose(geometry.offset); _invGeometryOffset = _geometryOffset.inverse(); setModelOffset(modelOffset); @@ -1224,8 +1223,7 @@ void Rig::copyJointsIntoJointData(QVector& jointDataVec) const { } void Rig::copyJointsFromJointData(const QVector& jointDataVec) { - - if (_animSkeleton) { + if (_animSkeleton && jointDataVec.size() == (int)_internalPoseSet._overrideFlags.size()) { // transform all the default poses into rig space. const AnimPose geometryToRigPose(_geometryToRigTransform); diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 0c7a79e2a3..7628c09748 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -102,7 +102,6 @@ AudioClient::AudioClient() : _reverbOptions(&_scriptReverbOptions), _inputToNetworkResampler(NULL), _networkToOutputResampler(NULL), - _loopbackResampler(NULL), _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_receivedAudioStream, this), _stats(&_receivedAudioStream), @@ -315,54 +314,35 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, // FIXME: directly using 24khz has a bug somewhere that causes channels to be swapped. // Continue using our internal resampler, for now. - if (true || !audioDevice.isFormatSupported(desiredAudioFormat)) { - qCDebug(audioclient) << "The desired format for audio I/O is" << desiredAudioFormat; - qCDebug(audioclient, "The desired audio format is not supported by this device"); + qCDebug(audioclient) << "The desired format for audio I/O is" << desiredAudioFormat; - if (desiredAudioFormat.channelCount() == 1) { - adjustedAudioFormat = desiredAudioFormat; - adjustedAudioFormat.setChannelCount(2); - - if (false && audioDevice.isFormatSupported(adjustedAudioFormat)) { - return true; - } else { - adjustedAudioFormat.setChannelCount(1); - } - } - - const int FORTY_FOUR = 44100; - - adjustedAudioFormat = desiredAudioFormat; + const int FORTY_FOUR = 44100; + adjustedAudioFormat = desiredAudioFormat; #ifdef Q_OS_ANDROID - adjustedAudioFormat.setSampleRate(FORTY_FOUR); + adjustedAudioFormat.setSampleRate(FORTY_FOUR); #else - const int HALF_FORTY_FOUR = FORTY_FOUR / 2; + const int HALF_FORTY_FOUR = FORTY_FOUR / 2; - if (audioDevice.supportedSampleRates().contains(AudioConstants::SAMPLE_RATE * 2)) { - // use 48, which is a sample downsample, upsample - adjustedAudioFormat.setSampleRate(AudioConstants::SAMPLE_RATE * 2); - } else if (audioDevice.supportedSampleRates().contains(HALF_FORTY_FOUR)) { - // use 22050, resample but closer to 24 - adjustedAudioFormat.setSampleRate(HALF_FORTY_FOUR); - } else if (audioDevice.supportedSampleRates().contains(FORTY_FOUR)) { - // use 48000, resample - adjustedAudioFormat.setSampleRate(FORTY_FOUR); - } + if (audioDevice.supportedSampleRates().contains(AudioConstants::SAMPLE_RATE * 2)) { + // use 48, which is a simple downsample, upsample + adjustedAudioFormat.setSampleRate(AudioConstants::SAMPLE_RATE * 2); + } else if (audioDevice.supportedSampleRates().contains(HALF_FORTY_FOUR)) { + // use 22050, resample but closer to 24 + adjustedAudioFormat.setSampleRate(HALF_FORTY_FOUR); + } else if (audioDevice.supportedSampleRates().contains(FORTY_FOUR)) { + // use 44100, resample + adjustedAudioFormat.setSampleRate(FORTY_FOUR); + } #endif - if (adjustedAudioFormat != desiredAudioFormat) { - // return the nearest in case it needs 2 channels - adjustedAudioFormat = audioDevice.nearestFormat(adjustedAudioFormat); - return true; - } else { - return false; - } - } else { - // set the adjustedAudioFormat to the desiredAudioFormat, since it will work - adjustedAudioFormat = desiredAudioFormat; + if (adjustedAudioFormat != desiredAudioFormat) { + // return the nearest in case it needs 2 channels + adjustedAudioFormat = audioDevice.nearestFormat(adjustedAudioFormat); return true; + } else { + return false; } } @@ -467,11 +447,6 @@ void AudioClient::stop() { // "switch" to invalid devices in order to shut down the state switchInputToAudioDevice(QAudioDeviceInfo()); switchOutputToAudioDevice(QAudioDeviceInfo()); - - if (_loopbackResampler) { - delete _loopbackResampler; - _loopbackResampler = NULL; - } } void AudioClient::handleAudioEnvironmentDataPacket(QSharedPointer message) { @@ -675,16 +650,10 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { } } - // do we need to setup a resampler? - if (_inputFormat.sampleRate() != _outputFormat.sampleRate() && !_loopbackResampler) { - qCDebug(audioclient) << "Attemping to create a resampler for input format to output format for audio loopback."; - - assert(_inputFormat.sampleSize() == 16); - assert(_outputFormat.sampleSize() == 16); - int channelCount = (_inputFormat.channelCount() == 2 && _outputFormat.channelCount() == 2) ? 2 : 1; - - _loopbackResampler = new AudioSRC(_inputFormat.sampleRate(), _outputFormat.sampleRate(), channelCount); - } + // NOTE: we assume the inputFormat and the outputFormat are the same, since on any modern + // multimedia OS they should be. If there is a device that this is not true for, we can + // add back support to do resampling. + Q_ASSERT(_inputFormat.sampleRate() == _outputFormat.sampleRate()); static QByteArray loopBackByteArray; @@ -696,7 +665,8 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { int16_t* inputSamples = reinterpret_cast(inputByteArray.data()); int16_t* loopbackSamples = reinterpret_cast(loopBackByteArray.data()); - possibleResampling(_loopbackResampler, + auto NO_RESAMPLER = nullptr; + possibleResampling(NO_RESAMPLER, inputSamples, loopbackSamples, numInputSamples, numLoopbackSamples, _inputFormat, _outputFormat); @@ -1039,12 +1009,6 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _networkToOutputResampler = NULL; } - if (_loopbackResampler) { - // if we were using an input to output resample, delete it here - delete _loopbackResampler; - _loopbackResampler = NULL; - } - if (!outputDeviceInfo.isNull()) { qCDebug(audioclient) << "The audio output device " << outputDeviceInfo.deviceName() << "is available."; _outputAudioDeviceName = outputDeviceInfo.deviceName().trimmed(); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 3a14c878f6..dc46db5657 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -260,7 +260,6 @@ private: // possible streams needed for resample AudioSRC* _inputToNetworkResampler; AudioSRC* _networkToOutputResampler; - AudioSRC* _loopbackResampler; // Adds Reverb void configureReverb(); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 6d99d6ad81..709cc76d01 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -269,7 +269,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { _lastSentJointData.resize(_jointData.size()); for (int i=0; i < _jointData.size(); i++) { - const JointData& data = _jointData.at(i); + const JointData& data = _jointData[i]; if (sendAll || _lastSentJointData[i].rotation != data.rotation) { if (sendAll || !cullSmallChanges || @@ -294,7 +294,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { validityBit = 0; validity = *validityPosition++; for (int i = 0; i < _jointData.size(); i ++) { - const JointData& data = _jointData[ i ]; + const JointData& data = _jointData[i]; if (validity & (1 << validityBit)) { destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation); } @@ -317,7 +317,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { float maxTranslationDimension = 0.0; for (int i=0; i < _jointData.size(); i++) { - const JointData& data = _jointData.at(i); + const JointData& data = _jointData[i]; if (sendAll || _lastSentJointData[i].translation != data.translation) { if (sendAll || !cullSmallChanges || @@ -348,7 +348,7 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { validityBit = 0; validity = *validityPosition++; for (int i = 0; i < _jointData.size(); i ++) { - const JointData& data = _jointData[ i ]; + const JointData& data = _jointData[i]; if (validity & (1 << validityBit)) { destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); @@ -425,7 +425,6 @@ bool AvatarData::shouldLogError(const quint64& now) { // read data in packet starting at byte offset and return number of bytes parsed int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { - // lazily allocate memory for HeadData in case we're not an Avatar instance if (!_headData) { _headData = new HeadData(this); @@ -669,7 +668,9 @@ void AvatarData::setJointData(int index, const glm::quat& rotation, const glm::v } JointData& data = _jointData[index]; data.rotation = rotation; + data.rotationSet = true; data.translation = translation; + data.translationSet = true; } void AvatarData::clearJointData(int index) { @@ -774,6 +775,7 @@ void AvatarData::setJointRotation(int index, const glm::quat& rotation) { } JointData& data = _jointData[index]; data.rotation = rotation; + data.rotationSet = true; } void AvatarData::setJointTranslation(int index, const glm::vec3& translation) { @@ -789,6 +791,7 @@ void AvatarData::setJointTranslation(int index, const glm::vec3& translation) { } JointData& data = _jointData[index]; data.translation = translation; + data.translationSet = true; } void AvatarData::clearJointData(const QString& name) { @@ -858,7 +861,6 @@ void AvatarData::setJointTranslations(QVector jointTranslations) { "setJointTranslations", Qt::BlockingQueuedConnection, Q_ARG(QVector, jointTranslations)); } - if (_jointData.size() < jointTranslations.size()) { _jointData.resize(jointTranslations.size()); } @@ -1100,6 +1102,7 @@ void AvatarData::sendIdentityPacket() { void AvatarData::updateJointMappings() { _jointIndices.clear(); _jointNames.clear(); + _jointData.clear(); if (_skeletonModelURL.fileName().toLower().endsWith(".fst")) { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); @@ -1457,7 +1460,6 @@ void AvatarData::fromJson(const QJsonObject& json) { auto joint = jointDataFromJsonValue(jointJson); jointArray.push_back(joint); setJointData(i, joint.rotation, joint.translation); - _jointData[i].rotationSet = true; // Have to do that to broadcast the avatar new pose i++; } setRawJointData(jointArray); diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 9084fd837b..d153cfd977 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -44,6 +44,20 @@ bool AvatarHashMap::isAvatarInRange(const glm::vec3& position, const float range return false; } +int AvatarHashMap::numberOfAvatarsInRange(const glm::vec3& position, float rangeMeters) { + auto hashCopy = getHashCopy(); + auto rangeMeters2 = rangeMeters * rangeMeters; + int count = 0; + for (const AvatarSharedPointer& sharedAvatar : hashCopy) { + glm::vec3 avatarPosition = sharedAvatar->getPosition(); + auto distance2 = glm::distance2(avatarPosition, position); + if (distance2 < rangeMeters2) { + ++count; + } + } + return count; +} + AvatarSharedPointer AvatarHashMap::newSharedAvatar() { return std::make_shared(); } diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 5f58074427..9d3ebb60f5 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -39,6 +39,7 @@ public: Q_INVOKABLE AvatarData* getAvatar(QUuid avatarID); virtual AvatarSharedPointer getAvatarBySessionID(const QUuid& sessionID) { return findAvatar(sessionID); } + int numberOfAvatarsInRange(const glm::vec3& position, float rangeMeters); signals: void avatarAddedEvent(const QUuid& sessionUUID); diff --git a/libraries/controllers/src/controllers/Forward.h b/libraries/controllers/src/controllers/Forward.h index e1a62556d4..23dd162831 100644 --- a/libraries/controllers/src/controllers/Forward.h +++ b/libraries/controllers/src/controllers/Forward.h @@ -32,6 +32,7 @@ class Mapping; using MappingPointer = std::shared_ptr; using MappingList = std::list; +struct Pose; } #endif diff --git a/libraries/controllers/src/controllers/StandardControls.h b/libraries/controllers/src/controllers/StandardControls.h index d7eb3de2c2..501f97f04b 100644 --- a/libraries/controllers/src/controllers/StandardControls.h +++ b/libraries/controllers/src/controllers/StandardControls.h @@ -44,7 +44,8 @@ namespace controller { LS_TOUCH, LEFT_THUMB_UP, LS_CENTER, - LS_OUTER, + LS_X, + LS_Y, RIGHT_PRIMARY_THUMB, RIGHT_SECONDARY_THUMB, @@ -53,7 +54,8 @@ namespace controller { RS_TOUCH, RIGHT_THUMB_UP, RS_CENTER, - RS_OUTER, + RS_X, + RS_Y, LEFT_PRIMARY_INDEX, LEFT_SECONDARY_INDEX, diff --git a/libraries/display-plugins/CMakeLists.txt b/libraries/display-plugins/CMakeLists.txt index f2d58d825e..fe08647074 100644 --- a/libraries/display-plugins/CMakeLists.txt +++ b/libraries/display-plugins/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME display-plugins) setup_hifi_library(OpenGL) -link_hifi_libraries(shared plugins gl gpu-gl ui) +link_hifi_libraries(shared plugins ui-plugins gl gpu-gl ui) target_opengl() diff --git a/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.cpp b/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.cpp index 4b8d957e5f..d068bef3b0 100644 --- a/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.cpp +++ b/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.cpp @@ -11,7 +11,7 @@ #include #include "DisplayPlugin.h" -#include +#include static Setting::Handle IPD_SCALE_HANDLE("hmd.ipdScale", 1.0f); diff --git a/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.h b/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.h index 5df58ce677..f260fa959f 100644 --- a/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.h +++ b/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.h @@ -31,6 +31,7 @@ public: signals: void IPDScaleChanged(); + void displayModeChanged(bool isHMDMode); private: float _IPDScale{ 1.0 }; diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp index 48dda1f73d..f488a805c6 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp @@ -13,7 +13,7 @@ #include #include -#include +#include const QString Basic2DWindowOpenGLDisplayPlugin::NAME("Desktop"); diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index f9d527de8f..032350a07c 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -336,7 +336,9 @@ void CompositorHelper::computeHmdPickRay(const glm::vec2& cursorPos, glm::vec3& } glm::mat4 CompositorHelper::getUiTransform() const { - return _currentCamera * glm::inverse(_currentDisplayPlugin->getHeadPose()); + glm::mat4 modelMat; + _modelTransform.getMatrix(modelMat); + return _currentCamera * glm::inverse(_currentDisplayPlugin->getHeadPose()) * modelMat; } //Finds the collision point of a world space ray @@ -346,7 +348,7 @@ bool CompositorHelper::calculateRayUICollisionPoint(const glm::vec3& position, c auto relativePosition = vec3(relativePosition4) / relativePosition4.w; auto relativeDirection = glm::inverse(glm::quat_cast(UITransform)) * direction; - float uiRadius = _oculusUIRadius; // * myAvatar->getUniformScale(); // FIXME - how do we want to handle avatar scale + float uiRadius = _hmdUIRadius; // * myAvatar->getUniformScale(); // FIXME - how do we want to handle avatar scale float instersectionDistance; if (raySphereIntersect(relativeDirection, relativePosition, uiRadius, &instersectionDistance)){ @@ -407,84 +409,25 @@ void CompositorHelper::updateTooltips() { //} } -static const float FADE_DURATION = 500.0f; -static const float FADE_IN_ALPHA = 1.0f; -static const float FADE_OUT_ALPHA = 0.0f; - -void CompositorHelper::startFadeFailsafe(float endValue) { - _fadeStarted = usecTimestampNow(); - _fadeFailsafeEndValue = endValue; - - const int SLIGHT_DELAY = 10; - QTimer::singleShot(FADE_DURATION + SLIGHT_DELAY, [this]{ - checkFadeFailsafe(); - }); -} - -void CompositorHelper::checkFadeFailsafe() { - auto elapsedInFade = usecTimestampNow() - _fadeStarted; - if (elapsedInFade > FADE_DURATION) { - setAlpha(_fadeFailsafeEndValue); - } -} - -void CompositorHelper::fadeIn() { - _fadeInAlpha = true; - - _alphaPropertyAnimation->setDuration(FADE_DURATION); - _alphaPropertyAnimation->setStartValue(_alpha); - _alphaPropertyAnimation->setEndValue(FADE_IN_ALPHA); - _alphaPropertyAnimation->start(); - - // Sometimes, this "QPropertyAnimation" fails to complete the animation, and we end up with a partially faded - // state. So we will also have this fail-safe, where we record the timestamp of the fadeRequest, and the target - // value of the fade, and if after that time we still haven't faded all the way, we will kick it to the final - // fade value - startFadeFailsafe(FADE_IN_ALPHA); -} - -void CompositorHelper::fadeOut() { - _fadeInAlpha = false; - - _alphaPropertyAnimation->setDuration(FADE_DURATION); - _alphaPropertyAnimation->setStartValue(_alpha); - _alphaPropertyAnimation->setEndValue(FADE_OUT_ALPHA); - _alphaPropertyAnimation->start(); - startFadeFailsafe(FADE_OUT_ALPHA); -} - -void CompositorHelper::toggle() { - if (_fadeInAlpha) { - fadeOut(); - } else { - fadeIn(); - } -} - +// eyePose and headPosition are in sensor space. +// the resulting matrix should be in view space. glm::mat4 CompositorHelper::getReticleTransform(const glm::mat4& eyePose, const glm::vec3& headPosition) const { glm::mat4 result; if (isHMD()) { - vec3 reticleScale = vec3(Cursor::Manager::instance().getScale() * reticleSize); - auto reticlePosition = getReticlePosition(); - auto spherical = overlayToSpherical(reticlePosition); - // The pointer transform relative to the sensor - auto pointerTransform = glm::mat4_cast(quat(vec3(-spherical.y, spherical.x, 0.0f))) * glm::translate(mat4(), vec3(0, 0, -1)); - float reticleDepth = getReticleDepth(); - if (reticleDepth != 1.0f) { - // Cursor position in UI space - auto cursorPosition = vec3(pointerTransform[3]) / pointerTransform[3].w; - // Ray to the cursor, in UI space - auto cursorRay = glm::normalize(cursorPosition - headPosition) * reticleDepth; - // Move the ray to be relative to the head pose - pointerTransform[3] = vec4(cursorRay + headPosition, 1); - // Scale up the cursor because of distance - reticleScale *= reticleDepth; + vec2 spherical = overlayToSpherical(getReticlePosition()); + vec3 overlaySurfacePoint = getPoint(spherical.x, spherical.y); // overlay space + vec3 sensorSurfacePoint = _modelTransform.transform(overlaySurfacePoint); // sensor space + vec3 d = sensorSurfacePoint - headPosition; + vec3 reticlePosition; + if (glm::length(d) >= EPSILON) { + d = glm::normalize(d); + } else { + d = glm::normalize(overlaySurfacePoint); } - glm::mat4 overlayXfm; - _modelTransform.getMatrix(overlayXfm); - pointerTransform = overlayXfm * pointerTransform; - pointerTransform = glm::inverse(eyePose) * pointerTransform; - result = glm::scale(pointerTransform, reticleScale); + reticlePosition = headPosition + (d * getReticleDepth()); + quat reticleOrientation = glm::quat_cast(_currentDisplayPlugin->getHeadPose()); + vec3 reticleScale = vec3(Cursor::Manager::instance().getScale() * reticleSize * getReticleDepth()); + return glm::inverse(eyePose) * createMatFromScaleQuatAndPos(reticleScale, reticleOrientation, reticlePosition); } else { static const float CURSOR_PIXEL_SIZE = 32.0f; const auto canvasSize = vec2(toGlm(_renderingWidget->size()));; diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index c0b53b329e..2a3dd0c852 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -38,7 +38,7 @@ const float MAGNIFY_MULT = 2.0f; class CompositorHelper : public QObject, public Dependency { Q_OBJECT - Q_PROPERTY(float alpha READ getAlpha WRITE setAlpha) + Q_PROPERTY(float alpha READ getAlpha WRITE setAlpha NOTIFY alphaChanged) Q_PROPERTY(bool reticleOverDesktop READ getReticleOverDesktop WRITE setReticleOverDesktop) public: static const uvec2 VIRTUAL_SCREEN_SIZE; @@ -75,12 +75,8 @@ public: void setModelTransform(const Transform& transform) { _modelTransform = transform; } const Transform& getModelTransform() const { return _modelTransform; } - void fadeIn(); - void fadeOut(); - void toggle(); - float getAlpha() const { return _alpha; } - void setAlpha(float alpha) { _alpha = alpha; } + void setAlpha(float alpha) { if (alpha != _alpha) { emit alphaChanged(); _alpha = alpha; } } bool getReticleVisible() const { return _reticleVisible; } void setReticleVisible(bool visible) { _reticleVisible = visible; } @@ -113,10 +109,11 @@ public: void setReticleOverDesktop(bool value) { _isOverDesktop = value; } void setDisplayPlugin(const DisplayPluginPointer& displayPlugin) { _currentDisplayPlugin = displayPlugin; } - void setFrameInfo(uint32_t frame, const glm::mat4& camera) { _currentCamera = camera; _currentFrame = frame; } + void setFrameInfo(uint32_t frame, const glm::mat4& camera) { _currentCamera = camera; } signals: void allowMouseCaptureChanged(); + void alphaChanged(); protected slots: void sendFakeMouseEvent(); @@ -127,7 +124,6 @@ private: DisplayPluginPointer _currentDisplayPlugin; glm::mat4 _currentCamera; - uint32_t _currentFrame { 0 }; QWidget* _renderingWidget{ nullptr }; //// Support for hovering and tooltips @@ -144,16 +140,7 @@ private: float _textureAspectRatio { VIRTUAL_UI_ASPECT_RATIO }; float _alpha { 1.0f }; - float _prevAlpha { 1.0f }; - float _fadeInAlpha { true }; - float _oculusUIRadius { 1.0f }; - - quint64 _fadeStarted { 0 }; - float _fadeFailsafeEndValue { 1.0f }; - void checkFadeFailsafe(); - void startFadeFailsafe(float endValue); - - int _reticleQuad; + float _hmdUIRadius { 1.0f }; int _previousBorderWidth { -1 }; int _previousBorderHeight { -1 }; diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp index 3f642072a0..4fadbdb94b 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp @@ -10,7 +10,7 @@ #include "NullDisplayPlugin.h" #include -#include +#include const QString NullDisplayPlugin::NAME("NullDisplayPlugin"); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index d34b698410..b72f52351f 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -22,12 +22,13 @@ #include #include #include -#include +#include #include #include #include #include #include "CompositorHelper.h" +#include #if THREADED_PRESENT @@ -202,6 +203,7 @@ private: #endif + OpenGLDisplayPlugin::OpenGLDisplayPlugin() { _sceneTextureEscrow.setRecycler([this](const gpu::TexturePointer& texture){ cleanupForSceneTexture(texture); @@ -213,9 +215,10 @@ OpenGLDisplayPlugin::OpenGLDisplayPlugin() { } void OpenGLDisplayPlugin::cleanupForSceneTexture(const gpu::TexturePointer& sceneTexture) { - Lock lock(_mutex); - Q_ASSERT(_sceneTextureToFrameIndexMap.contains(sceneTexture)); - _sceneTextureToFrameIndexMap.remove(sceneTexture); + withRenderThreadLock([&] { + Q_ASSERT(_sceneTextureToFrameIndexMap.contains(sceneTexture)); + _sceneTextureToFrameIndexMap.remove(sceneTexture); + }); } @@ -233,10 +236,11 @@ bool OpenGLDisplayPlugin::activate() { cursorData.hotSpot = vec2(0.5f); } } - + if (!_container) { + return false; + } _vsyncSupported = _container->getPrimaryWidget()->isVsyncSupported(); - #if THREADED_PRESENT // Start the present thread if necessary QSharedPointer presentThread; @@ -272,11 +276,27 @@ bool OpenGLDisplayPlugin::activate() { _container->makeRenderingContextCurrent(); #endif - return DisplayPlugin::activate(); + auto compositorHelper = DependencyManager::get(); + connect(compositorHelper.data(), &CompositorHelper::alphaChanged, [this] { + auto compositorHelper = DependencyManager::get(); + auto animation = new QPropertyAnimation(this, "overlayAlpha"); + animation->setDuration(200); + animation->setEndValue(compositorHelper->getAlpha()); + animation->start(); + }); + + if (isHmd() && (getHmdScreen() >= 0)) { + _container->showDisplayPluginsTools(); + } + + return Parent::activate(); } void OpenGLDisplayPlugin::deactivate() { + auto compositorHelper = DependencyManager::get(); + disconnect(compositorHelper.data()); + #if THREADED_PRESENT auto presentThread = DependencyManager::get(); // Does not return until the GL transition has completeed @@ -288,7 +308,16 @@ void OpenGLDisplayPlugin::deactivate() { _container->makeRenderingContextCurrent(); #endif internalDeactivate(); - DisplayPlugin::deactivate(); + + _container->showDisplayPluginsTools(false); + if (!_container->currentDisplayActions().isEmpty()) { + auto menu = _container->getPrimaryMenu(); + foreach(auto itemInfo, _container->currentDisplayActions()) { + menu->removeMenuItem(itemInfo.first, itemInfo.second); + } + _container->currentDisplayActions().clear(); + } + Parent::deactivate(); } @@ -394,10 +423,9 @@ void OpenGLDisplayPlugin::submitSceneTexture(uint32_t frameIndex, const gpu::Tex return; } - { - Lock lock(_mutex); + withRenderThreadLock([&] { _sceneTextureToFrameIndexMap[sceneTexture] = frameIndex; - } + }); // Submit it to the presentation thread via escrow _sceneTextureEscrow.submit(sceneTexture); @@ -431,11 +459,12 @@ void OpenGLDisplayPlugin::updateTextures() { } void OpenGLDisplayPlugin::updateFrameData() { - Lock lock(_mutex); - auto previousFrameIndex = _currentPresentFrameIndex; - _currentPresentFrameIndex = _sceneTextureToFrameIndexMap[_currentSceneTexture]; - auto skippedCount = (_currentPresentFrameIndex - previousFrameIndex) - 1; - _droppedFrameRate.increment(skippedCount); + withPresentThreadLock([&] { + auto previousFrameIndex = _currentPresentFrameIndex; + _currentPresentFrameIndex = _sceneTextureToFrameIndexMap[_currentSceneTexture]; + auto skippedCount = (_currentPresentFrameIndex - previousFrameIndex) - 1; + _droppedFrameRate.increment(skippedCount); + }); } void OpenGLDisplayPlugin::compositeOverlay() { @@ -444,25 +473,22 @@ void OpenGLDisplayPlugin::compositeOverlay() { auto compositorHelper = DependencyManager::get(); useProgram(_program); + // set the alpha + Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); // check the alpha - auto overlayAlpha = compositorHelper->getAlpha(); - if (overlayAlpha > 0.0f) { - // set the alpha - Uniform(*_program, _alphaUniform).Set(overlayAlpha); - - // Overlay draw - if (isStereo()) { - Uniform(*_program, _mvpUniform).Set(mat4()); - for_each_eye([&](Eye eye) { - eyeViewport(eye); - drawUnitQuad(); - }); - } else { - // Overlay draw - Uniform(*_program, _mvpUniform).Set(mat4()); + // Overlay draw + if (isStereo()) { + Uniform(*_program, _mvpUniform).Set(mat4()); + for_each_eye([&](Eye eye) { + eyeViewport(eye); drawUnitQuad(); - } + }); + } else { + // Overlay draw + Uniform(*_program, _mvpUniform).Set(mat4()); + drawUnitQuad(); } + // restore the alpha Uniform(*_program, _alphaUniform).Set(1.0); } @@ -471,23 +497,19 @@ void OpenGLDisplayPlugin::compositePointer() { auto compositorHelper = DependencyManager::get(); useProgram(_program); - // check the alpha - auto overlayAlpha = compositorHelper->getAlpha(); - if (overlayAlpha > 0.0f) { - // set the alpha - Uniform(*_program, _alphaUniform).Set(overlayAlpha); - - Uniform(*_program, _mvpUniform).Set(compositorHelper->getReticleTransform(glm::mat4())); - if (isStereo()) { - for_each_eye([&](Eye eye) { - eyeViewport(eye); - drawUnitQuad(); - }); - } else { + // set the alpha + Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); + Uniform(*_program, _mvpUniform).Set(compositorHelper->getReticleTransform(glm::mat4())); + if (isStereo()) { + for_each_eye([&](Eye eye) { + eyeViewport(eye); drawUnitQuad(); - } + }); + } else { + drawUnitQuad(); } Uniform(*_program, _mvpUniform).Set(mat4()); + // restore the alpha Uniform(*_program, _alphaUniform).Set(1.0); } @@ -507,14 +529,14 @@ void OpenGLDisplayPlugin::compositeLayers() { } _compositeFramebuffer->Bound(Framebuffer::Target::Draw, [&] { Context::Viewport(targetRenderSize.x, targetRenderSize.y); - Context::Clear().DepthBuffer(); - glBindTexture(GL_TEXTURE_2D, getSceneTextureId()); - compositeScene(); + auto sceneTextureId = getSceneTextureId(); auto overlayTextureId = getOverlayTextureId(); + glBindTexture(GL_TEXTURE_2D, sceneTextureId); + compositeScene(); if (overlayTextureId) { - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBindTexture(GL_TEXTURE_2D, overlayTextureId); + Context::Enable(Capability::Blend); + Context::BlendFunc(BlendFunction::SrcAlpha, BlendFunction::OneMinusSrcAlpha); compositeOverlay(); auto compositorHelper = DependencyManager::get(); @@ -522,11 +544,16 @@ void OpenGLDisplayPlugin::compositeLayers() { auto& cursorManager = Cursor::Manager::instance(); const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; glBindTexture(GL_TEXTURE_2D, cursorData.texture); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, overlayTextureId); compositePointer(); + glBindTexture(GL_TEXTURE_2D, 0); + glActiveTexture(GL_TEXTURE0); } glBindTexture(GL_TEXTURE_2D, 0); - glDisable(GL_BLEND); + Context::Disable(Capability::Blend); } + compositeExtra(); }); } @@ -564,7 +591,11 @@ float OpenGLDisplayPlugin::newFramePresentRate() const { } float OpenGLDisplayPlugin::droppedFrameRate() const { - return _droppedFrameRate.rate(); + float result; + withRenderThreadLock([&] { + result = _droppedFrameRate.rate(); + }); + return result; } float OpenGLDisplayPlugin::presentRate() const { @@ -679,3 +710,18 @@ void OpenGLDisplayPlugin::useProgram(const ProgramPtr& program) { _activeProgram = program; } } + +void OpenGLDisplayPlugin::assertIsRenderThread() const { + Q_ASSERT(QThread::currentThread() != _presentThread); +} + +void OpenGLDisplayPlugin::assertIsPresentThread() const { + Q_ASSERT(QThread::currentThread() == _presentThread); +} + +bool OpenGLDisplayPlugin::beginFrameRender(uint32_t frameIndex) { + withRenderThreadLock([&] { + _compositeOverlayAlpha = _overlayAlpha; + }); + return Parent::beginFrameRender(frameIndex); +} diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index c87ff1bc93..068b236289 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -24,6 +24,9 @@ #define THREADED_PRESENT 1 class OpenGLDisplayPlugin : public DisplayPlugin { + Q_OBJECT + Q_PROPERTY(float overlayAlpha MEMBER _overlayAlpha) + using Parent = DisplayPlugin; protected: using Mutex = std::mutex; using Lock = std::unique_lock; @@ -60,6 +63,7 @@ public: float droppedFrameRate() const override; + bool beginFrameRender(uint32_t frameIndex) override; protected: #if THREADED_PRESENT friend class PresentThread; @@ -74,6 +78,7 @@ protected: virtual void compositeScene(); virtual void compositeOverlay(); virtual void compositePointer(); + virtual void compositeExtra() {}; virtual bool hasFocus() const override; @@ -109,12 +114,12 @@ protected: int32_t _alphaUniform { -1 }; ShapeWrapperPtr _plane; - mutable Mutex _mutex; RateCounter<> _droppedFrameRate; RateCounter<> _newFrameRate; RateCounter<> _presentRate; QMap _sceneTextureToFrameIndexMap; uint32_t _currentPresentFrameIndex { 0 }; + float _compositeOverlayAlpha{ 1.0f }; gpu::TexturePointer _currentSceneTexture; gpu::TexturePointer _currentOverlayTexture; @@ -135,8 +140,28 @@ protected: BasicFramebufferWrapperPtr _compositeFramebuffer; bool _lockCurrentTexture { false }; + void assertIsRenderThread() const; + void assertIsPresentThread() const; + + template + void withPresentThreadLock(F f) const { + assertIsPresentThread(); + Lock lock(_presentMutex); + f(); + } + + template + void withRenderThreadLock(F f) const { + assertIsRenderThread(); + Lock lock(_presentMutex); + f(); + } + private: + // Any resource shared by the main thread and the presentation thread must + // be serialized through this mutex + mutable Mutex _presentMutex; ProgramPtr _activeProgram; + float _overlayAlpha{ 1.0f }; }; - diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 4e594d89ed..f1aa1edc81 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -9,13 +9,14 @@ #include #include +#include #include #include #include #include -#include +#include #include #include #include @@ -37,7 +38,6 @@ QRect HmdDisplayPlugin::getRecommendedOverlayRect() const { return CompositorHelper::VIRTUAL_SCREEN_RECOMMENDED_OVERLAY_RECT; } - bool HmdDisplayPlugin::internalActivate() { _monoPreview = _container->getBoolSetting("monoPreview", DEFAULT_MONO_VIEW); @@ -197,14 +197,43 @@ static ProgramPtr getReprojectionProgram() { #endif +static const char * LASER_VS = R"VS(#version 410 core +uniform mat4 mvp = mat4(1); + +in vec3 Position; + +out vec3 vPosition; + +void main() { + gl_Position = mvp * vec4(Position, 1); + vPosition = Position; +} + +)VS"; + +static const char * LASER_FS = R"FS(#version 410 core + +uniform vec4 color = vec4(1.0, 1.0, 1.0, 1.0); +in vec3 vPosition; + +out vec4 FragColor; + +void main() { + FragColor = color; +} + +)FS"; + void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); // Only enable mirroring if we know vsync is disabled enableVsync(false); _enablePreview = !isVsyncEnabled(); _sphereSection = loadSphereSection(_program, CompositorHelper::VIRTUAL_UI_TARGET_FOV.y, CompositorHelper::VIRTUAL_UI_ASPECT_RATIO); + compileProgram(_laserProgram, LASER_VS, LASER_FS); + _laserGeometry = loadLaser(_laserProgram); compileProgram(_reprojectionProgram, REPROJECTION_VS, REPROJECTION_FS); - + using namespace oglplus; REPROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "reprojection").Location(); INVERSE_PROJECTION_MATRIX_LOCATION = Uniform(*_reprojectionProgram, "inverseProjections").Location(); @@ -215,6 +244,8 @@ void HmdDisplayPlugin::uncustomizeContext() { _sphereSection.reset(); _compositeFramebuffer.reset(); _reprojectionProgram.reset(); + _laserProgram.reset(); + _laserGeometry.reset(); Parent::uncustomizeContext(); } @@ -253,23 +284,20 @@ void HmdDisplayPlugin::compositeScene() { void HmdDisplayPlugin::compositeOverlay() { using namespace oglplus; auto compositorHelper = DependencyManager::get(); + glm::mat4 modelMat = compositorHelper->getModelTransform().getMatrix(); - // check the alpha useProgram(_program); - auto overlayAlpha = compositorHelper->getAlpha(); - if (overlayAlpha > 0.0f) { - // set the alpha - Uniform(*_program, _alphaUniform).Set(overlayAlpha); - - _sphereSection->Use(); - for_each_eye([&](Eye eye) { - eyeViewport(eye); - auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)); - auto mvp = _eyeProjections[eye] * modelView; - Uniform(*_program, _mvpUniform).Set(mvp); - _sphereSection->Draw(); - }); - } + // set the alpha + Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); + _sphereSection->Use(); + for_each_eye([&](Eye eye) { + eyeViewport(eye); + auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)) * modelMat; + auto mvp = _eyeProjections[eye] * modelView; + Uniform(*_program, _mvpUniform).Set(mvp); + _sphereSection->Draw(); + }); + // restore the alpha Uniform(*_program, _alphaUniform).Set(1.0); } @@ -278,29 +306,27 @@ void HmdDisplayPlugin::compositePointer() { auto compositorHelper = DependencyManager::get(); - // check the alpha useProgram(_program); - auto overlayAlpha = compositorHelper->getAlpha(); - if (overlayAlpha > 0.0f) { - // set the alpha - Uniform(*_program, _alphaUniform).Set(overlayAlpha); + // set the alpha + Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); - // Mouse pointer - _plane->Use(); - // Reconstruct the headpose from the eye poses - auto headPosition = vec3(_currentPresentFrameInfo.presentPose[3]); - for_each_eye([&](Eye eye) { - eyeViewport(eye); - auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); - auto reticleTransform = compositorHelper->getReticleTransform(eyePose, headPosition); - auto mvp = _eyeProjections[eye] * reticleTransform; - Uniform(*_program, _mvpUniform).Set(mvp); - _plane->Draw(); - }); - } + // Mouse pointer + _plane->Use(); + // Reconstruct the headpose from the eye poses + auto headPosition = vec3(_currentPresentFrameInfo.presentPose[3]); + for_each_eye([&](Eye eye) { + eyeViewport(eye); + auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); + auto reticleTransform = compositorHelper->getReticleTransform(eyePose, headPosition); + auto mvp = _eyeProjections[eye] * reticleTransform; + Uniform(*_program, _mvpUniform).Set(mvp); + _plane->Draw(); + }); + // restore the alpha Uniform(*_program, _alphaUniform).Set(1.0); } + void HmdDisplayPlugin::internalPresent() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) @@ -357,22 +383,117 @@ void HmdDisplayPlugin::setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm: void HmdDisplayPlugin::updateFrameData() { // Check if we have old frame data to discard - { - Lock lock(_mutex); + withPresentThreadLock([&] { auto itr = _frameInfos.find(_currentPresentFrameIndex); if (itr != _frameInfos.end()) { _frameInfos.erase(itr); } - } + }); Parent::updateFrameData(); - { - Lock lock(_mutex); + withPresentThreadLock([&] { _currentPresentFrameInfo = _frameInfos[_currentPresentFrameIndex]; - } + }); } glm::mat4 HmdDisplayPlugin::getHeadPose() const { return _currentRenderFrameInfo.renderPose; } + +bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const vec4& color, const vec3& direction) { + HandLaserInfo info; + info.mode = mode; + info.color = color; + info.direction = direction; + withRenderThreadLock([&] { + if (hands & Hand::LeftHand) { + _handLasers[0] = info; + } + if (hands & Hand::RightHand) { + _handLasers[1] = info; + } + }); + // FIXME defer to a child class plugin to determine if hand lasers are actually + // available based on the presence or absence of hand controllers + return true; +} + +void HmdDisplayPlugin::compositeExtra() { + const int NUMBER_OF_HANDS = 2; + std::array handLasers; + std::array renderHandPoses; + Transform uiModelTransform; + withPresentThreadLock([&] { + handLasers = _handLasers; + renderHandPoses = _handPoses; + uiModelTransform = _uiModelTransform; + }); + + // If neither hand laser is activated, exit + if (!handLasers[0].valid() && !handLasers[1].valid()) { + return; + } + + static const glm::mat4 identity; + if (renderHandPoses[0] == identity && renderHandPoses[1] == identity) { + return; + } + + // Render hand lasers + using namespace oglplus; + useProgram(_laserProgram); + _laserGeometry->Use(); + std::array handLaserModelMatrices; + + for (int i = 0; i < NUMBER_OF_HANDS; ++i) { + if (renderHandPoses[i] == identity) { + continue; + } + const auto& handLaser = handLasers[i]; + if (!handLaser.valid()) { + continue; + } + + const auto& laserDirection = handLaser.direction; + auto model = renderHandPoses[i]; + auto castDirection = glm::quat_cast(model) * laserDirection; + if (glm::abs(glm::length2(castDirection) - 1.0f) > EPSILON) { + castDirection = glm::normalize(castDirection); + } + + // FIXME fetch the actual UI radius from... somewhere? + float uiRadius = 1.0f; + + // Find the intersection of the laser with he UI and use it to scale the model matrix + float distance; + if (!glm::intersectRaySphere(vec3(renderHandPoses[i][3]), castDirection, uiModelTransform.getTranslation(), uiRadius * uiRadius, distance)) { + continue; + } + + // Make sure we rotate to match the desired laser direction + if (laserDirection != Vectors::UNIT_NEG_Z) { + auto rotation = glm::rotation(Vectors::UNIT_NEG_Z, laserDirection); + model = model * glm::mat4_cast(rotation); + } + + model = glm::scale(model, vec3(distance)); + handLaserModelMatrices[i] = model; + } + + for_each_eye([&](Eye eye) { + eyeViewport(eye); + auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); + auto view = glm::inverse(eyePose); + const auto& projection = _eyeProjections[eye]; + for (int i = 0; i < NUMBER_OF_HANDS; ++i) { + if (handLaserModelMatrices[i] == identity) { + continue; + } + Uniform(*_laserProgram, "mvp").Set(projection * view * handLaserModelMatrices[i]); + Uniform(*_laserProgram, "color").Set(handLasers[i].color); + _laserGeometry->Draw(); + // TODO render some kind of visual indicator at the intersection point with the UI. + } + }); +} diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index e6ceb7e376..fada15d864 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -10,6 +10,7 @@ #include #include +#include #include "../OpenGLDisplayPlugin.h" @@ -30,7 +31,7 @@ public: virtual glm::mat4 getHeadPose() const override; - + bool setHandLaser(uint32_t hands, HandLaserMode mode, const vec4& color, const vec3& direction) override; protected: virtual void hmdPresent() = 0; @@ -46,7 +47,22 @@ protected: void customizeContext() override; void uncustomizeContext() override; void updateFrameData() override; + void compositeExtra() override; + struct HandLaserInfo { + HandLaserMode mode { HandLaserMode::None }; + vec4 color { 1.0f }; + vec3 direction { 0, 0, -1 }; + + // Is this hand laser info suitable for drawing? + bool valid() const { + return (mode != HandLaserMode::None && color.a > 0.0f && direction != vec3()); + } + }; + + Transform _uiModelTransform; + std::array _handLasers; + std::array _handPoses; std::array _eyeOffsets; std::array _eyeProjections; std::array _eyeInverseProjections; @@ -75,5 +91,7 @@ private: bool _enableReprojection { true }; ShapeWrapperPtr _sphereSection; ProgramPtr _reprojectionProgram; + ProgramPtr _laserProgram; + ShapeWrapperPtr _laserGeometry; }; diff --git a/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp index 5f55841be1..5d9f812edf 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp @@ -9,7 +9,7 @@ #include "SideBySideStereoDisplayPlugin.h" #include #include -#include +#include #include #include "../CompositorHelper.h" diff --git a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp index 6c6716c8fa..cfdfb1fc21 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp @@ -15,7 +15,7 @@ #include #include -#include +#include #include #include #include "../CompositorHelper.h" diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 7b3b3c3efe..366e365107 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -593,23 +593,25 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { // the model is still being downloaded. return false; + } else if (type == SHAPE_TYPE_STATIC_MESH) { + return (_model && _model->isLoaded()); } return true; } void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { ShapeType type = getShapeType(); + glm::vec3 dimensions = getDimensions(); if (type == SHAPE_TYPE_COMPOUND) { updateModelBounds(); // should never fall in here when collision model not fully loaded // hence we assert that all geometries exist and are loaded assert(_model->isLoaded() && _model->isCollisionLoaded()); - const FBXGeometry& renderGeometry = _model->getFBXGeometry(); const FBXGeometry& collisionGeometry = _model->getCollisionFBXGeometry(); - QVector>& points = info.getPoints(); - points.clear(); + ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); + pointCollection.clear(); uint32_t i = 0; // the way OBJ files get read, each section under a "g" line is its own meshPart. We only expect @@ -619,8 +621,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { foreach (const FBXMesh& mesh, collisionGeometry.meshes) { // each meshPart is a convex hull foreach (const FBXMeshPart &meshPart, mesh.parts) { - points.push_back(QVector()); - QVector& pointsInPart = points[i]; + pointCollection.push_back(QVector()); + ShapeInfo::PointList& pointsInPart = pointCollection[i]; // run through all the triangles and (uniquely) add each point to the hull uint32_t numIndices = (uint32_t)meshPart.triangleIndices.size(); @@ -664,7 +666,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { if (pointsInPart.size() == 0) { qCDebug(entitiesrenderer) << "Warning -- meshPart has no faces"; - points.pop_back(); + pointCollection.pop_back(); continue; } ++i; @@ -677,29 +679,136 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& info) { // to the visual model and apply them to the collision model (without regard for the // collision model's extents). - glm::vec3 scale = getDimensions() / renderGeometry.getUnscaledMeshExtents().size(); + glm::vec3 scaleToFit = dimensions / _model->getFBXGeometry().getUnscaledMeshExtents().size(); // multiply each point by scale before handing the point-set off to the physics engine. // also determine the extents of the collision model. - AABox box; - for (int i = 0; i < points.size(); i++) { - for (int j = 0; j < points[i].size(); j++) { + for (int i = 0; i < pointCollection.size(); i++) { + for (int j = 0; j < pointCollection[i].size(); j++) { // compensate for registration - points[i][j] += _model->getOffset(); + pointCollection[i][j] += _model->getOffset(); // scale so the collision points match the model points - points[i][j] *= scale; - // this next subtraction is done so we can give info the offset, which will cause - // the shape-key to change. - points[i][j] -= _model->getOffset(); - box += points[i][j]; + pointCollection[i][j] *= scaleToFit; + } + } + info.setParams(type, dimensions, _compoundShapeURL); + } else if (type == SHAPE_TYPE_STATIC_MESH) { + // compute meshPart local transforms + QVector localTransforms; + const FBXGeometry& geometry = _model->getFBXGeometry(); + int numberOfMeshes = geometry.meshes.size(); + for (int i = 0; i < numberOfMeshes; i++) { + const FBXMesh& mesh = geometry.meshes.at(i); + if (mesh.clusters.size() > 0) { + const FBXCluster& cluster = mesh.clusters.at(0); + auto jointMatrix = _model->getRig()->getJointTransform(cluster.jointIndex); + localTransforms.push_back(jointMatrix * cluster.inverseBindMatrix); + } else { + glm::mat4 identity; + localTransforms.push_back(identity); } } - glm::vec3 collisionModelDimensions = box.getDimensions(); - info.setParams(type, collisionModelDimensions, _compoundShapeURL); - info.setOffset(_model->getOffset()); + updateModelBounds(); + + // should never fall in here when collision model not fully loaded + assert(_model->isLoaded()); + + ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); + pointCollection.clear(); + + ShapeInfo::PointList points; + ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); + auto& meshes = _model->getGeometry()->getGeometry()->getMeshes(); + + Extents extents; + int meshCount = 0; + for (auto& mesh : meshes) { + const gpu::BufferView& vertices = mesh->getVertexBuffer(); + const gpu::BufferView& indices = mesh->getIndexBuffer(); + const gpu::BufferView& parts = mesh->getPartBuffer(); + + // copy points + const glm::mat4& localTransform = localTransforms[meshCount]; + uint32_t meshIndexOffset = (uint32_t)points.size(); + gpu::BufferView::Iterator vertexItr = vertices.cbegin(); + points.reserve((int32_t)((gpu::Size)points.size() + vertices.getNumElements())); + while (vertexItr != vertices.cend()) { + glm::vec3 point = extractTranslation(localTransform * glm::translate(*vertexItr)); + points.push_back(point); + extents.addPoint(point); + ++vertexItr; + } + + // copy triangleIndices + triangleIndices.reserve((int32_t)((gpu::Size)(triangleIndices.size()) + indices.getNumElements())); + gpu::BufferView::Iterator partItr = parts.cbegin(); + while (partItr != parts.cend()) { + + if (partItr->_topology == model::Mesh::TRIANGLES) { + assert(partItr->_numIndices % 3 == 0); + auto indexItr = indices.cbegin() + partItr->_startIndex; + auto indexEnd = indexItr + partItr->_numIndices; + while (indexItr != indexEnd) { + triangleIndices.push_back(*indexItr + meshIndexOffset); + ++indexItr; + } + } else if (partItr->_topology == model::Mesh::TRIANGLE_STRIP) { + assert(partItr->_numIndices > 2); + uint32_t approxNumIndices = 3 * partItr->_numIndices; + if (approxNumIndices > (uint32_t)(triangleIndices.capacity() - triangleIndices.size())) { + // we underestimated the final size of triangleIndices so we pre-emptively expand it + triangleIndices.reserve(triangleIndices.size() + approxNumIndices); + } + + auto indexItr = indices.cbegin() + partItr->_startIndex; + auto indexEnd = indexItr + (partItr->_numIndices - 2); + + // first triangle uses the first three indices + triangleIndices.push_back(*(indexItr++) + meshIndexOffset); + triangleIndices.push_back(*(indexItr++) + meshIndexOffset); + triangleIndices.push_back(*(indexItr++) + meshIndexOffset); + + // the rest use previous and next index + uint32_t triangleCount = 1; + while (indexItr != indexEnd) { + if ((*indexItr) != model::Mesh::PRIMITIVE_RESTART_INDEX) { + if (triangleCount % 2 == 0) { + // even triangles use first two indices in order + triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset); + triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset); + } else { + // odd triangles swap order of first two indices + triangleIndices.push_back(*(indexItr - 1) + meshIndexOffset); + triangleIndices.push_back(*(indexItr - 2) + meshIndexOffset); + } + triangleIndices.push_back(*indexItr + meshIndexOffset); + ++triangleCount; + } + ++indexItr; + } + } + ++partItr; + } + ++meshCount; + } + + // scale and shift + glm::vec3 extentsSize = extents.size(); + glm::vec3 scaleToFit = dimensions / extentsSize; + for (int i = 0; i < 3; ++i) { + if (extentsSize[i] < 1.0e-6f) { + scaleToFit[i] = 1.0f; + } + } + for (int i = 0; i < points.size(); ++i) { + points[i] = (points[i] * scaleToFit); + } + + pointCollection.push_back(points); + info.setParams(SHAPE_TYPE_STATIC_MESH, 0.5f * dimensions, _modelURL); } else { ModelEntityItem::computeShapeInfo(info); - info.setParams(type, 0.5f * getDimensions()); + info.setParams(type, 0.5f * dimensions); adjustShapeInfoByRegistration(info); } } diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index ad35a1a00c..eb6db2874f 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1198,7 +1198,7 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { QtConcurrent::run([entity, voxelSurfaceStyle, voxelVolumeSize, mesh] { auto polyVoxEntity = std::static_pointer_cast(entity); - QVector> points; + QVector> pointCollection; AABox box; glm::mat4 vtoM = std::static_pointer_cast(entity)->voxelToLocalMatrix(); @@ -1207,7 +1207,7 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { // pull each triangle in the mesh into a polyhedron which can be collided with unsigned int i = 0; - const gpu::BufferView vertexBufferView = mesh->getVertexBuffer(); + const gpu::BufferView& vertexBufferView = mesh->getVertexBuffer(); const gpu::BufferView& indexBufferView = mesh->getIndexBuffer(); gpu::BufferView::Iterator it = indexBufferView.cbegin(); @@ -1241,9 +1241,9 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { pointsInPart << p3Model; // add next convex hull QVector newMeshPoints; - points << newMeshPoints; + pointCollection << newMeshPoints; // add points to the new convex hull - points[i++] << pointsInPart; + pointCollection[i++] << pointsInPart; } } else { unsigned int i = 0; @@ -1299,19 +1299,19 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { // add next convex hull QVector newMeshPoints; - points << newMeshPoints; + pointCollection << newMeshPoints; // add points to the new convex hull - points[i++] << pointsInPart; + pointCollection[i++] << pointsInPart; } }); } - polyVoxEntity->setCollisionPoints(points, box); + polyVoxEntity->setCollisionPoints(pointCollection, box); }); } -void RenderablePolyVoxEntityItem::setCollisionPoints(const QVector> points, AABox box) { +void RenderablePolyVoxEntityItem::setCollisionPoints(ShapeInfo::PointCollection pointCollection, AABox box) { // this catches the payload from computeShapeInfoWorker - if (points.isEmpty()) { + if (pointCollection.isEmpty()) { EntityItem::computeShapeInfo(_shapeInfo); return; } @@ -1325,7 +1325,7 @@ void RenderablePolyVoxEntityItem::setCollisionPoints(const QVector thunk); void setMesh(model::MeshPointer mesh); - void setCollisionPoints(const QVector> points, AABox box); + void setCollisionPoints(ShapeInfo::PointCollection points, AABox box); PolyVox::SimpleVolume* getVolData() { return _volData; } uint8_t getVoxelInternal(int x, int y, int z); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 64b6a2c655..f0a4d40860 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1602,14 +1602,20 @@ void EntityItem::updateMass(float mass) { void EntityItem::updateVelocity(const glm::vec3& value) { glm::vec3 velocity = getLocalVelocity(); if (velocity != value) { - const float MIN_LINEAR_SPEED = 0.001f; - if (glm::length(value) < MIN_LINEAR_SPEED) { - velocity = ENTITY_ITEM_ZERO_VEC3; + if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { + if (velocity != Vectors::ZERO) { + setLocalVelocity(Vectors::ZERO); + } } else { - velocity = value; + const float MIN_LINEAR_SPEED = 0.001f; + if (glm::length(value) < MIN_LINEAR_SPEED) { + velocity = ENTITY_ITEM_ZERO_VEC3; + } else { + velocity = value; + } + setLocalVelocity(velocity); + _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; } - setLocalVelocity(velocity); - _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; } } @@ -1630,22 +1636,30 @@ void EntityItem::updateDamping(float value) { void EntityItem::updateGravity(const glm::vec3& value) { if (_gravity != value) { - _gravity = value; - _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; + if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { + _gravity = Vectors::ZERO; + } else { + _gravity = value; + _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; + } } } void EntityItem::updateAngularVelocity(const glm::vec3& value) { glm::vec3 angularVelocity = getLocalAngularVelocity(); if (angularVelocity != value) { - const float MIN_ANGULAR_SPEED = 0.0002f; - if (glm::length(value) < MIN_ANGULAR_SPEED) { - angularVelocity = ENTITY_ITEM_ZERO_VEC3; + if (getShapeType() == SHAPE_TYPE_STATIC_MESH) { + setLocalAngularVelocity(Vectors::ZERO); } else { - angularVelocity = value; + const float MIN_ANGULAR_SPEED = 0.0002f; + if (glm::length(value) < MIN_ANGULAR_SPEED) { + angularVelocity = ENTITY_ITEM_ZERO_VEC3; + } else { + angularVelocity = value; + } + setLocalAngularVelocity(angularVelocity); + _dirtyFlags |= Simulation::DIRTY_ANGULAR_VELOCITY; } - setLocalAngularVelocity(angularVelocity); - _dirtyFlags |= Simulation::DIRTY_ANGULAR_VELOCITY; } } @@ -1679,9 +1693,17 @@ void EntityItem::updateCollisionMask(uint8_t value) { } void EntityItem::updateDynamic(bool value) { - if (_dynamic != value) { - _dynamic = value; - _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + if (getDynamic() != value) { + // dynamic and STATIC_MESH are incompatible so we check for that case + if (value && getShapeType() == SHAPE_TYPE_STATIC_MESH) { + if (_dynamic) { + _dynamic = false; + _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + } + } else { + _dynamic = value; + _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + } } } @@ -1731,7 +1753,7 @@ void EntityItem::computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask group = BULLET_COLLISION_GROUP_COLLISIONLESS; mask = 0; } else { - if (_dynamic) { + if (getDynamic()) { group = BULLET_COLLISION_GROUP_DYNAMIC; } else if (isMovingRelativeToParent() || hasActions()) { group = BULLET_COLLISION_GROUP_KINEMATIC; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 4a691462ab..9fa13690f1 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -283,7 +283,7 @@ public: void computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask) const; - bool getDynamic() const { return _dynamic; } + bool getDynamic() const { return SHAPE_TYPE_STATIC_MESH == getShapeType() ? false : _dynamic; } void setDynamic(bool value) { _dynamic = value; } virtual bool shouldBePhysical() const { return false; } @@ -348,7 +348,7 @@ public: void updateDynamic(bool value); void updateLifetime(float value); void updateCreated(uint64_t value); - virtual void updateShapeType(ShapeType type) { /* do nothing */ } + virtual void setShapeType(ShapeType type) { /* do nothing */ } uint32_t getDirtyFlags() const { return _dirtyFlags; } void clearDirtyFlags(uint32_t mask = 0xffffffff) { _dirtyFlags &= ~mask; } diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 89bf9f1a21..a62f4b182a 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -88,8 +88,21 @@ void EntityItemProperties::setLastEdited(quint64 usecTime) { _lastEdited = usecTime > _created ? usecTime : _created; } -const char* shapeTypeNames[] = {"none", "box", "sphere", "plane", "compound", "capsule-x", - "capsule-y", "capsule-z", "cylinder-x", "cylinder-y", "cylinder-z"}; +const char* shapeTypeNames[] = { + "none", + "box", + "sphere", + "capsule-x", + "capsule-y", + "capsule-z", + "cylinder-x", + "cylinder-y", + "cylinder-z", + "hull", + "plane", + "compound", + "static-mesh" +}; QHash stringToShapeTypeLookup; @@ -101,14 +114,16 @@ void buildStringToShapeTypeLookup() { addShapeType(SHAPE_TYPE_NONE); addShapeType(SHAPE_TYPE_BOX); addShapeType(SHAPE_TYPE_SPHERE); - addShapeType(SHAPE_TYPE_PLANE); - addShapeType(SHAPE_TYPE_COMPOUND); addShapeType(SHAPE_TYPE_CAPSULE_X); addShapeType(SHAPE_TYPE_CAPSULE_Y); addShapeType(SHAPE_TYPE_CAPSULE_Z); addShapeType(SHAPE_TYPE_CYLINDER_X); addShapeType(SHAPE_TYPE_CYLINDER_Y); addShapeType(SHAPE_TYPE_CYLINDER_Z); + addShapeType(SHAPE_TYPE_HULL); + addShapeType(SHAPE_TYPE_PLANE); + addShapeType(SHAPE_TYPE_COMPOUND); + addShapeType(SHAPE_TYPE_STATIC_MESH); } QString getCollisionGroupAsString(uint8_t group) { diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index e0863041a1..856e526b4c 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -179,6 +179,7 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties } entity->setLastBroadcast(usecTimestampNow()); + propertiesWithSimID.setLastEdited(entity->getLastEdited()); } else { qCDebug(entities) << "script failed to add new Entity to local Octree"; success = false; @@ -190,9 +191,11 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties if (success) { emit debitEnergySource(cost); queueEntityMessage(PacketType::EntityAdd, id, propertiesWithSimID); - } - return id; + return id; + } else { + return QUuid(); + } } QUuid EntityScriptingInterface::addModelEntity(const QString& name, const QString& modelUrl, const glm::vec3& position) { @@ -376,6 +379,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& properties.setQueryAACube(entity->getQueryAACube()); } entity->setLastBroadcast(usecTimestampNow()); + properties.setLastEdited(entity->getLastEdited()); // if we've moved an entity with children, check/update the queryAACube of all descendents and tell the server // if they've changed. diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 77a0c6d6fe..21e5865c09 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -130,16 +130,13 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI EntityItemProperties properties = origProperties; bool allowLockChange; - bool canRezPermanentEntities; QUuid senderID; if (senderNode.isNull()) { auto nodeList = DependencyManager::get(); allowLockChange = nodeList->isAllowedEditor(); - canRezPermanentEntities = nodeList->getThisNodeCanRez(); senderID = nodeList->getSessionUUID(); } else { allowLockChange = senderNode->isAllowedEditor(); - canRezPermanentEntities = senderNode->getCanRez(); senderID = senderNode->getUUID(); } @@ -148,12 +145,6 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI return false; } - if (!canRezPermanentEntities && (entity->getLifetime() != properties.getLifetime())) { - // we don't allow a Node that can't create permanent entities to adjust lifetimes on existing ones - qCDebug(entities) << "Refusing disallowed entity lifetime adjustment."; - return false; - } - // enforce support for locked entities. If an entity is currently locked, then the only // property we allow you to change is the locked property. if (entity->getLocked()) { @@ -319,26 +310,9 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI return true; } -bool EntityTree::permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp) { - float lifeTime = properties.getLifetime(); - - if (lifeTime == 0.0f || lifeTime > _maxTmpEntityLifetime) { - // this is an attempt to rez a permanent entity. - if (!canRez) { - return false; - } - } else { - // this is an attempt to rez a temporary entity. - if (!canRezTmp) { - return false; - } - } - - return true; -} - EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer result = NULL; + EntityItemProperties props = properties; auto nodeList = DependencyManager::get(); if (!nodeList) { @@ -346,16 +320,13 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti return nullptr; } - bool clientOnly = properties.getClientOnly(); - - if (!clientOnly && getIsClient() && - !permissionsAllowRez(properties, nodeList->getThisNodeCanRez(), nodeList->getThisNodeCanRezTmp())) { - // if our Node isn't allowed to create entities in this domain, don't try. + if (!properties.getClientOnly() && getIsClient() && + !nodeList->getThisNodeCanRez() && !nodeList->getThisNodeCanRezTmp()) { return nullptr; } bool recordCreationTime = false; - if (properties.getCreated() == UNKNOWN_CREATED_TIME) { + if (props.getCreated() == UNKNOWN_CREATED_TIME) { // the entity's creation time was not specified in properties, which means this is a NEW entity // and we must record its creation time recordCreationTime = true; @@ -370,8 +341,8 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti } // construct the instance of the entity - EntityTypes::EntityType type = properties.getType(); - result = EntityTypes::constructEntityItem(type, entityID, properties); + EntityTypes::EntityType type = props.getType(); + result = EntityTypes::constructEntityItem(type, entityID, props); if (result) { if (recordCreationTime) { @@ -888,6 +859,13 @@ void EntityTree::fixupTerseEditLogging(EntityItemProperties& properties, QList= 0) { + float value = properties.getLifetime(); + changedProperties[index] = QString("lifetime:") + QString::number((int)value); + } + } } int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned char* editData, int maxLength, @@ -920,11 +898,23 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c EntityItemID entityItemID; EntityItemProperties properties; startDecode = usecTimestampNow(); - + bool validEditPacket = EntityItemProperties::decodeEntityEditPacket(editData, maxLength, processedBytes, entityItemID, properties); endDecode = usecTimestampNow(); + const quint64 LAST_EDITED_SERVERSIDE_BUMP = 1; // usec + if (!senderNode->getCanRez() && senderNode->getCanRezTmp()) { + // this node is only allowed to rez temporary entities. if need be, cap the lifetime. + if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME || + properties.getLifetime() > _maxTmpEntityLifetime) { + properties.setLifetime(_maxTmpEntityLifetime); + // also bump up the lastEdited time of the properties so that the interface that created this edit + // will accept our adjustment to lifetime back into its own entity-tree. + properties.setLastEdited(properties.getLastEdited() + LAST_EDITED_SERVERSIDE_BUMP); + } + } + // If we got a valid edit packet, then it could be a new entity or it could be an update to // an existing entity... handle appropriately if (validEditPacket) { @@ -953,7 +943,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c endUpdate = usecTimestampNow(); _totalUpdates++; } else if (message.getType() == PacketType::EntityAdd) { - if (permissionsAllowRez(properties, senderNode->getCanRez(), senderNode->getCanRezTmp())) { + if (senderNode->getCanRez() || senderNode->getCanRezTmp()) { // this is a new entity... assign a new entityID properties.setCreated(properties.getLastEdited()); startCreate = usecTimestampNow(); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 8afb8d878f..15daf3bf3c 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -64,7 +64,6 @@ public: void setEntityMaxTmpLifetime(float maxTmpEntityLifetime) { _maxTmpEntityLifetime = maxTmpEntityLifetime; } - bool permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp); /// Implements our type specific root element factory virtual OctreeElementPointer createNewElement(unsigned char* octalCode = NULL) override; diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 40faf2c3c3..8e925b2f79 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -77,7 +77,7 @@ bool ModelEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(modelURL, setModelURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(textures, setTextures); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, updateShapeType); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointRotationsSet, setJointRotationsSet); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointRotations, setJointRotations); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointTranslationsSet, setJointTranslationsSet); @@ -145,7 +145,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, dataAt += bytesFromAnimation; } - READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, updateShapeType); + READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, setShapeType); if (animationPropertiesChanged) { _dirtyFlags |= Simulation::DIRTY_UPDATEABLE; @@ -257,37 +257,54 @@ void ModelEntityItem::debugDump() const { qCDebug(entities) << " compound shape URL:" << getCompoundShapeURL(); } -void ModelEntityItem::updateShapeType(ShapeType type) { - // BEGIN_TEMPORARY_WORKAROUND - // we have allowed inconsistent ShapeType's to be stored in SVO files in the past (this was a bug) - // but we are now enforcing the entity properties to be consistent. To make the possible we're - // introducing a temporary workaround: we will ignore ShapeType updates that conflict with the - // _compoundShapeURL. - if (hasCompoundShapeURL()) { - type = SHAPE_TYPE_COMPOUND; - } - // END_TEMPORARY_WORKAROUND - +void ModelEntityItem::setShapeType(ShapeType type) { if (type != _shapeType) { + if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { + // dynamic and STATIC_MESH are incompatible + // since the shape is being set here we clear the dynamic bit + _dynamic = false; + _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + } _shapeType = type; _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; } } -// virtual ShapeType ModelEntityItem::getShapeType() const { - if (_shapeType == SHAPE_TYPE_COMPOUND) { - return hasCompoundShapeURL() ? SHAPE_TYPE_COMPOUND : SHAPE_TYPE_NONE; - } else { - return _shapeType; + return computeTrueShapeType(); +} + +ShapeType ModelEntityItem::computeTrueShapeType() const { + ShapeType type = _shapeType; + if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { + // dynamic is incompatible with STATIC_MESH + // shouldn't fall in here but just in case --> fall back to COMPOUND + type = SHAPE_TYPE_COMPOUND; + } + if (type == SHAPE_TYPE_COMPOUND && !hasCompoundShapeURL()) { + // no compoundURL set --> fall back to NONE + type = SHAPE_TYPE_NONE; + } + return type; +} + +void ModelEntityItem::setModelURL(const QString& url) { + if (_modelURL != url) { + _modelURL = url; + _parsedModelURL = QUrl(url); + if (_shapeType == SHAPE_TYPE_STATIC_MESH) { + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + } } } void ModelEntityItem::setCompoundShapeURL(const QString& url) { if (_compoundShapeURL != url) { + ShapeType oldType = computeTrueShapeType(); _compoundShapeURL = url; - _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; - _shapeType = _compoundShapeURL.isEmpty() ? SHAPE_TYPE_NONE : SHAPE_TYPE_COMPOUND; + if (oldType != computeTrueShapeType()) { + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + } } } diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 29730bf4df..7b7edaf945 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -50,9 +50,10 @@ public: virtual bool needsToCallUpdate() const; virtual void debugDump() const; - void updateShapeType(ShapeType type); + void setShapeType(ShapeType type); virtual ShapeType getShapeType() const; + // TODO: Move these to subclasses, or other appropriate abstraction // getters/setters applicable to models and particles @@ -76,7 +77,7 @@ public: } // model related properties - virtual void setModelURL(const QString& url) { _modelURL = url; _parsedModelURL = QUrl(url); } + virtual void setModelURL(const QString& url); virtual void setCompoundShapeURL(const QString& url); // Animation related items... @@ -130,6 +131,7 @@ public: private: void setAnimationSettings(const QString& value); // only called for old bitstream format + ShapeType computeTrueShapeType() const; protected: // these are used: diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index a7bd0038e6..c501737146 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -342,7 +342,7 @@ bool ParticleEffectEntityItem::setProperties(const EntityItemProperties& propert SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); SET_ENTITY_PROPERTY_FROM_PROPERTIES(alpha, setAlpha); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, updateShapeType); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(maxParticles, setMaxParticles); SET_ENTITY_PROPERTY_FROM_PROPERTIES(lifespan, setLifespan); SET_ENTITY_PROPERTY_FROM_PROPERTIES(isEmitting, setIsEmitting); @@ -406,7 +406,7 @@ int ParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned ch READ_ENTITY_PROPERTY(PROP_EMITTING_PARTICLES, bool, setIsEmitting); } - READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, updateShapeType); + READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, setShapeType); READ_ENTITY_PROPERTY(PROP_MAX_PARTICLES, quint32, setMaxParticles); READ_ENTITY_PROPERTY(PROP_LIFESPAN, float, setLifespan); READ_ENTITY_PROPERTY(PROP_EMIT_RATE, float, setEmitRate); @@ -584,7 +584,7 @@ void ParticleEffectEntityItem::debugDump() const { qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); } -void ParticleEffectEntityItem::updateShapeType(ShapeType type) { +void ParticleEffectEntityItem::setShapeType(ShapeType type) { if (type != _shapeType) { _shapeType = type; _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index 4538a1bb43..9ddda62c8b 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -95,7 +95,7 @@ public: void setAlphaSpread(float alphaSpread); float getAlphaSpread() const { return _alphaSpread; } - void updateShapeType(ShapeType type); + void setShapeType(ShapeType type); virtual ShapeType getShapeType() const { return _shapeType; } virtual void debugDump() const; diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index a28b8210c2..0b99d0377f 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -73,7 +73,7 @@ bool ZoneEntityItem::setProperties(const EntityItemProperties& properties) { bool somethingChangedInStage = _stageProperties.setProperties(properties); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, updateShapeType); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(shapeType, setShapeType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(compoundShapeURL, setCompoundShapeURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(backgroundMode, setBackgroundMode); @@ -117,7 +117,7 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, bytesRead += bytesFromStage; dataAt += bytesFromStage; - READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, updateShapeType); + READ_ENTITY_PROPERTY(PROP_SHAPE_TYPE, ShapeType, setShapeType); READ_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); READ_ENTITY_PROPERTY(PROP_BACKGROUND_MODE, BackgroundMode, setBackgroundMode); diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 56968aa9c9..f0f2a91d63 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -55,7 +55,7 @@ public: static void setDrawZoneBoundaries(bool value) { _drawZoneBoundaries = value; } virtual bool isReadyToComputeShape() { return false; } - void updateShapeType(ShapeType type) { _shapeType = type; } + void setShapeType(ShapeType type) { _shapeType = type; } virtual ShapeType getShapeType() const; virtual bool hasCompoundShapeURL() const { return !_compoundShapeURL.isEmpty(); } diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index 302e0b8515..2781d5b9b0 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -5,6 +5,15 @@ #include #include #include +#include +#include +#ifdef DEBUG +static bool enableDebug = true; +#else +static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); +static bool enableDebug = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); +#endif + const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { static QSurfaceFormat format; @@ -14,9 +23,9 @@ const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { format.setDepthBufferSize(DEFAULT_GL_DEPTH_BUFFER_BITS); format.setStencilBufferSize(DEFAULT_GL_STENCIL_BUFFER_BITS); setGLFormatVersion(format); -#ifdef DEBUG - format.setOption(QSurfaceFormat::DebugContext); -#endif + if (enableDebug) { + format.setOption(QSurfaceFormat::DebugContext); + } format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); QSurfaceFormat::setDefaultFormat(format); }); @@ -39,6 +48,13 @@ const QGLFormat& getDefaultGLFormat() { return glFormat; } +int glVersionToInteger(QString glVersion) { + QStringList versionParts = glVersion.split(QRegularExpression("[\\.\\s]")); + int majorNumber = versionParts[0].toInt(); + int minorNumber = versionParts[1].toInt(); + return majorNumber * 100 + minorNumber * 10; +} + QJsonObject getGLContextData() { if (!QOpenGLContext::currentContext()) { return QJsonObject(); diff --git a/libraries/gl/src/gl/GLHelpers.h b/libraries/gl/src/gl/GLHelpers.h index ddb254f1c5..477bf7abc8 100644 --- a/libraries/gl/src/gl/GLHelpers.h +++ b/libraries/gl/src/gl/GLHelpers.h @@ -27,5 +27,6 @@ void setGLFormatVersion(F& format, int major = 4, int minor = 5) { format.setVer const QSurfaceFormat& getDefaultOpenGLSurfaceFormat(); const QGLFormat& getDefaultGLFormat(); QJsonObject getGLContextData(); +int glVersionToInteger(QString glVersion); #endif diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index 90ff369cd6..eec3d2bf6b 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -12,23 +12,30 @@ #include "OffscreenGLCanvas.h" +#include #include #include #include #include "GLHelpers.h" +#ifdef DEBUG +static bool enableDebugLogger = true; +#else +static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); +static bool enableDebugLogger = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); +#endif + + OffscreenGLCanvas::OffscreenGLCanvas() : _context(new QOpenGLContext), _offscreenSurface(new QOffscreenSurface){ } OffscreenGLCanvas::~OffscreenGLCanvas() { -#ifdef DEBUG if (_logger) { makeCurrent(); delete _logger; _logger = nullptr; } -#endif _context->doneCurrent(); } @@ -60,7 +67,7 @@ bool OffscreenGLCanvas::makeCurrent() { qDebug() << "GL Renderer: " << QString((const char*) glGetString(GL_RENDERER)); }); -#ifdef DEBUG + if (result && !_logger) { _logger = new QOpenGLDebugLogger(this); if (_logger->initialize()) { @@ -71,7 +78,6 @@ bool OffscreenGLCanvas::makeCurrent() { _logger->startLogging(QOpenGLDebugLogger::LoggingMode::SynchronousLogging); } } -#endif return result; } diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.h b/libraries/gl/src/gl/OffscreenGLCanvas.h index 387804bf56..69210f638d 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.h +++ b/libraries/gl/src/gl/OffscreenGLCanvas.h @@ -36,10 +36,7 @@ protected: std::once_flag _reportOnce; QOpenGLContext* _context; QOffscreenSurface* _offscreenSurface; -#ifdef DEBUG QOpenGLDebugLogger* _logger{ nullptr }; -#endif - }; #endif // hifi_OffscreenGLCanvas_h diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 388ca26482..b4a9080ee8 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -396,6 +396,8 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { _renderer->_renderControl->_renderWindow = _proxyWindow; + connect(_renderer->_quickWindow, &QQuickWindow::focusObjectChanged, this, &OffscreenQmlSurface::onFocusObjectChanged); + // Create a QML engine. _qmlEngine = new QQmlEngine; if (!_qmlEngine->incubationController()) { @@ -414,7 +416,7 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { _updateTimer.start(); } -void OffscreenQmlSurface::resize(const QSize& newSize_) { +void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) { if (!_renderer || !_renderer->_quickWindow) { return; @@ -433,7 +435,7 @@ void OffscreenQmlSurface::resize(const QSize& newSize_) { } QSize currentSize = _renderer->_quickWindow->geometry().size(); - if (newSize == currentSize) { + if (newSize == currentSize && !forceResize) { return; } @@ -742,3 +744,21 @@ QVariant OffscreenQmlSurface::returnFromUiThread(std::function funct return function(); } + +void OffscreenQmlSurface::onFocusObjectChanged(QObject* object) { + if (!object) { + setFocusText(false); + return; + } + + QInputMethodQueryEvent query(Qt::ImEnabled); + qApp->sendEvent(object, &query); + setFocusText(query.value(Qt::ImEnabled).toBool()); +} + +void OffscreenQmlSurface::setFocusText(bool newFocusText) { + if (newFocusText != _focusText) { + _focusText = newFocusText; + emit focusTextChanged(_focusText); + } +} diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index 22a1b99fe6..a4a5ecba7e 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -30,7 +30,7 @@ class OffscreenQmlRenderThread; class OffscreenQmlSurface : public QObject { Q_OBJECT - + Q_PROPERTY(bool focusText READ isFocusText NOTIFY focusTextChanged) public: OffscreenQmlSurface(); virtual ~OffscreenQmlSurface(); @@ -38,7 +38,7 @@ public: using MouseTranslator = std::function; virtual void create(QOpenGLContext* context); - void resize(const QSize& size); + void resize(const QSize& size, bool forceResize = false); QSize size() const; Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); Q_INVOKABLE QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { @@ -55,6 +55,7 @@ public: _mouseTranslator = mouseTranslator; } + bool isFocusText() const { return _focusText; } void pause(); void resume(); bool isPaused() const; @@ -70,6 +71,8 @@ public: signals: void textureUpdated(unsigned int texture); + void focusObjectChanged(QObject* newFocus); + void focusTextChanged(bool focusText); public slots: void requestUpdate(); @@ -78,6 +81,7 @@ public slots: protected: bool filterEnabled(QObject* originalDestination, QEvent* event) const; + void setFocusText(bool newFocusText); private: QObject* finishQmlLoad(std::function f); @@ -85,6 +89,7 @@ private: private slots: void updateQuick(); + void onFocusObjectChanged(QObject* newFocus); private: friend class OffscreenQmlRenderThread; @@ -97,6 +102,7 @@ private: bool _render{ false }; bool _polish{ true }; bool _paused{ true }; + bool _focusText { false }; uint8_t _maxFps{ 60 }; MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p.toPoint(); } }; QWindow* _proxyWindow { nullptr }; diff --git a/libraries/gl/src/gl/OglplusHelpers.cpp b/libraries/gl/src/gl/OglplusHelpers.cpp index 5bf0298593..7a535a806d 100644 --- a/libraries/gl/src/gl/OglplusHelpers.cpp +++ b/libraries/gl/src/gl/OglplusHelpers.cpp @@ -45,9 +45,11 @@ in vec2 vTexCoord; out vec4 FragColor; void main() { - FragColor = texture(sampler, vTexCoord); FragColor.a *= alpha; + if (FragColor.a <= 0.0) { + discard; + } } )FS"; @@ -359,6 +361,94 @@ ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov, float aspect, i ); } +namespace oglplus { + namespace shapes { + + class Laser : public DrawingInstructionWriter, public DrawMode { + public: + using IndexArray = std::vector; + using PosArray = std::vector; + /// The type of the index container returned by Indices() + // vertex positions + PosArray _pos_data; + IndexArray _idx_data; + unsigned int _prim_count { 0 }; + + public: + Laser() { + int vertices = 2; + _pos_data.resize(vertices * 3); + _pos_data[0] = 0; + _pos_data[1] = 0; + _pos_data[2] = 0; + + _pos_data[3] = 0; + _pos_data[4] = 0; + _pos_data[5] = -1; + + _idx_data.push_back(0); + _idx_data.push_back(1); + _prim_count = 1; + } + + /// Returns the winding direction of faces + FaceOrientation FaceWinding(void) const { + return FaceOrientation::CCW; + } + + /// Queries the bounding sphere coordinates and dimensions + template + void BoundingSphere(Sphere& bounding_sphere) const { + bounding_sphere = Sphere(0, 0, -0.5, 0.5); + } + + typedef GLuint(Laser::*VertexAttribFunc)(std::vector&) const; + + /// Makes the vertex positions and returns the number of values per vertex + template + GLuint Positions(std::vector& dest) const { + dest.clear(); + dest.insert(dest.begin(), _pos_data.begin(), _pos_data.end()); + return 3; + } + + typedef VertexAttribsInfo< + Laser, + std::tuple + > VertexAttribs; + + + /// Returns element indices that are used with the drawing instructions + const IndexArray & Indices(Default = Default()) const { + return _idx_data; + } + + /// Returns the instructions for rendering of faces + DrawingInstructions Instructions(PrimitiveType primitive) const { + DrawingInstructions instr = MakeInstructions(); + DrawOperation operation; + operation.method = DrawOperation::Method::DrawElements; + operation.mode = primitive; + operation.first = 0; + operation.count = _prim_count * 3; + operation.restart_index = DrawOperation::NoRestartIndex(); + operation.phase = 0; + AddInstruction(instr, operation); + return instr; + } + + /// Returns the instructions for rendering of faces + DrawingInstructions Instructions(Default = Default()) const { + return Instructions(PrimitiveType::Lines); + } + }; + } +} + +ShapeWrapperPtr loadLaser(const ProgramPtr& program) { + return std::make_shared(shapes::ShapeWrapper("Position", shapes::Laser(), *program)); +} + void TextureRecycler::setSize(const uvec2& size) { if (size == _size) { return; diff --git a/libraries/gl/src/gl/OglplusHelpers.h b/libraries/gl/src/gl/OglplusHelpers.h index afb06069b8..8940205b21 100644 --- a/libraries/gl/src/gl/OglplusHelpers.h +++ b/libraries/gl/src/gl/OglplusHelpers.h @@ -64,8 +64,9 @@ ProgramPtr loadCubemapShader(); void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& fs); ShapeWrapperPtr loadSkybox(ProgramPtr program); ShapeWrapperPtr loadPlane(ProgramPtr program, float aspect = 1.0f); -ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov = PI / 3.0f * 2.0f, float aspect = 16.0f / 9.0f, int slices = 32, int stacks = 32); - +ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov = PI / 3.0f * 2.0f, float aspect = 16.0f / 9.0f, int slices = 128, int stacks = 128); +ShapeWrapperPtr loadLaser(const ProgramPtr& program); + // A basic wrapper for constructing a framebuffer with a renderbuffer // for the depth attachment and an undefined type for the color attachement diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index e18b784018..ce2f4c8d66 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -17,6 +17,7 @@ #include #include "../gl41/GL41Backend.h" +#include "../gl45/GL45Backend.h" #if defined(NSIGHT_FOUND) #include "nvToolsExt.h" @@ -31,25 +32,21 @@ using namespace gpu; using namespace gpu::gl; - static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); -bool enableOpenGL45 = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); +static bool enableOpenGL45 = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); Backend* GLBackend::createBackend() { - -#if 0 // FIXME provide a mechanism to override the backend for testing // Where the gpuContext is initialized and where the TRUE Backend is created and assigned auto version = QOpenGLContextWrapper::currentContextVersion(); GLBackend* result; if (enableOpenGL45 && version >= 0x0405) { - result = new gpu::gl45::GLBackend; + qDebug() << "Using OpenGL 4.5 backend"; + result = new gpu::gl45::GL45Backend(); } else { - result = new gpu::gl41::GLBackend; + qDebug() << "Using OpenGL 4.1 backend"; + result = new gpu::gl41::GL41Backend(); } -#else - GLBackend* result = new gpu::gl41::GL41Backend; -#endif result->initInput(); result->initTransform(); gl::GLTexture::initTextureTransferHelper(); diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 5255a6cb25..d27ec3808b 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -186,7 +186,7 @@ protected: virtual void killInput() final; virtual void syncInputStateCache() final; virtual void resetInputStage() final; - virtual void updateInput() = 0; + virtual void updateInput(); struct InputStageState { bool _invalidFormat { true }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp index 448cc508eb..99c1ff0438 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp @@ -150,3 +150,176 @@ void GLBackend::do_setIndirectBuffer(Batch& batch, size_t paramOffset) { (void)CHECK_GL_ERROR(); } + + +// Core 41 doesn't expose the features to really separate the vertex format from the vertex buffers binding +// Core 43 does :) +// FIXME crashing problem with glVertexBindingDivisor / glVertexAttribFormat +// Once resolved, break this up into the GL 4.1 and 4.5 backends +#if 1 || (GPU_INPUT_PROFILE == GPU_CORE_41) +#define NO_SUPPORT_VERTEX_ATTRIB_FORMAT +#else +#define SUPPORT_VERTEX_ATTRIB_FORMAT +#endif + +void GLBackend::updateInput() { +#if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) + if (_input._invalidFormat) { + + InputStageState::ActivationCache newActivation; + + // Assign the vertex format required + if (_input._format) { + for (auto& it : _input._format->getAttributes()) { + const Stream::Attribute& attrib = (it).second; + + GLuint slot = attrib._slot; + GLuint count = attrib._element.getLocationScalarCount(); + uint8_t locationCount = attrib._element.getLocationCount(); + GLenum type = _elementTypeToGL41Type[attrib._element.getType()]; + GLuint offset = attrib._offset;; + GLboolean isNormalized = attrib._element.isNormalized(); + + GLenum perLocationSize = attrib._element.getLocationSize(); + + for (size_t locNum = 0; locNum < locationCount; ++locNum) { + newActivation.set(slot + locNum); + glVertexAttribFormat(slot + locNum, count, type, isNormalized, offset + locNum * perLocationSize); + glVertexAttribBinding(slot + locNum, attrib._channel); + } + glVertexBindingDivisor(attrib._channel, attrib._frequency); + } + (void)CHECK_GL_ERROR(); + } + + // Manage Activation what was and what is expected now + for (size_t i = 0; i < newActivation.size(); i++) { + bool newState = newActivation[i]; + if (newState != _input._attributeActivation[i]) { + if (newState) { + glEnableVertexAttribArray(i); + } else { + glDisableVertexAttribArray(i); + } + _input._attributeActivation.flip(i); + } + } + (void)CHECK_GL_ERROR(); + + _input._invalidFormat = false; + _stats._ISNumFormatChanges++; + } + + if (_input._invalidBuffers.any()) { + int numBuffers = _input._buffers.size(); + auto buffer = _input._buffers.data(); + auto vbo = _input._bufferVBOs.data(); + auto offset = _input._bufferOffsets.data(); + auto stride = _input._bufferStrides.data(); + + for (int bufferNum = 0; bufferNum < numBuffers; bufferNum++) { + if (_input._invalidBuffers.test(bufferNum)) { + glBindVertexBuffer(bufferNum, (*vbo), (*offset), (*stride)); + } + buffer++; + vbo++; + offset++; + stride++; + } + _input._invalidBuffers.reset(); + (void)CHECK_GL_ERROR(); + } +#else + if (_input._invalidFormat || _input._invalidBuffers.any()) { + + if (_input._invalidFormat) { + InputStageState::ActivationCache newActivation; + + _stats._ISNumFormatChanges++; + + // Check expected activation + if (_input._format) { + for (auto& it : _input._format->getAttributes()) { + const Stream::Attribute& attrib = (it).second; + uint8_t locationCount = attrib._element.getLocationCount(); + for (int i = 0; i < locationCount; ++i) { + newActivation.set(attrib._slot + i); + } + } + } + + // Manage Activation what was and what is expected now + for (unsigned int i = 0; i < newActivation.size(); i++) { + bool newState = newActivation[i]; + if (newState != _input._attributeActivation[i]) { + + if (newState) { + glEnableVertexAttribArray(i); + } else { + glDisableVertexAttribArray(i); + } + (void)CHECK_GL_ERROR(); + + _input._attributeActivation.flip(i); + } + } + } + + // now we need to bind the buffers and assign the attrib pointers + if (_input._format) { + const Buffers& buffers = _input._buffers; + const Offsets& offsets = _input._bufferOffsets; + const Offsets& strides = _input._bufferStrides; + + const Stream::Format::AttributeMap& attributes = _input._format->getAttributes(); + auto& inputChannels = _input._format->getChannels(); + _stats._ISNumInputBufferChanges++; + + GLuint boundVBO = 0; + for (auto& channelIt : inputChannels) { + const Stream::Format::ChannelMap::value_type::second_type& channel = (channelIt).second; + if ((channelIt).first < buffers.size()) { + int bufferNum = (channelIt).first; + + if (_input._invalidBuffers.test(bufferNum) || _input._invalidFormat) { + // GLuint vbo = gpu::GL41Backend::getBufferID((*buffers[bufferNum])); + GLuint vbo = _input._bufferVBOs[bufferNum]; + if (boundVBO != vbo) { + glBindBuffer(GL_ARRAY_BUFFER, vbo); + (void)CHECK_GL_ERROR(); + boundVBO = vbo; + } + _input._invalidBuffers[bufferNum] = false; + + for (unsigned int i = 0; i < channel._slots.size(); i++) { + const Stream::Attribute& attrib = attributes.at(channel._slots[i]); + GLuint slot = attrib._slot; + GLuint count = attrib._element.getLocationScalarCount(); + uint8_t locationCount = attrib._element.getLocationCount(); + GLenum type = gl::ELEMENT_TYPE_TO_GL[attrib._element.getType()]; + // GLenum perLocationStride = strides[bufferNum]; + GLenum perLocationStride = attrib._element.getLocationSize(); + GLuint stride = (GLuint)strides[bufferNum]; + GLuint pointer = (GLuint)(attrib._offset + offsets[bufferNum]); + GLboolean isNormalized = attrib._element.isNormalized(); + + for (size_t locNum = 0; locNum < locationCount; ++locNum) { + glVertexAttribPointer(slot + (GLuint)locNum, count, type, isNormalized, stride, + reinterpret_cast(pointer + perLocationStride * (GLuint)locNum)); + glVertexAttribDivisor(slot + (GLuint)locNum, attrib._frequency); + } + + // TODO: Support properly the IAttrib version + + (void)CHECK_GL_ERROR(); + } + } + } + } + } + // everything format related should be in sync now + _input._invalidFormat = false; + } +#endif +} + diff --git a/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp b/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp index 19cf798b19..fa54e7c8fe 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp @@ -24,8 +24,15 @@ GLPipeline* GLPipeline::sync(const Pipeline& pipeline) { // No object allocated yet, let's see if it's worth it... ShaderPointer shader = pipeline.getProgram(); + + // If this pipeline's shader has already failed to compile, don't try again + if (shader->compilationHasFailed()) { + return nullptr; + } + GLShader* programObject = GLShader::sync(*shader); if (programObject == nullptr) { + shader->setCompilationHasFailed(true); return nullptr; } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp index 1b0caa7345..d3e86de606 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp @@ -13,173 +13,7 @@ using namespace gpu; using namespace gpu::gl41; -// Core 41 doesn't expose the features to really separate the vertex format from the vertex buffers binding -// Core 43 does :) -// FIXME crashing problem with glVertexBindingDivisor / glVertexAttribFormat -#if 1 || (GPU_INPUT_PROFILE == GPU_CORE_41) -#define NO_SUPPORT_VERTEX_ATTRIB_FORMAT -#else -#define SUPPORT_VERTEX_ATTRIB_FORMAT -#endif - void GL41Backend::updateInput() { -#if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) - if (_input._invalidFormat) { - - InputStageState::ActivationCache newActivation; - - // Assign the vertex format required - if (_input._format) { - for (auto& it : _input._format->getAttributes()) { - const Stream::Attribute& attrib = (it).second; - - GLuint slot = attrib._slot; - GLuint count = attrib._element.getLocationScalarCount(); - uint8_t locationCount = attrib._element.getLocationCount(); - GLenum type = _elementTypeToGL41Type[attrib._element.getType()]; - GLuint offset = attrib._offset;; - GLboolean isNormalized = attrib._element.isNormalized(); - - GLenum perLocationSize = attrib._element.getLocationSize(); - - for (size_t locNum = 0; locNum < locationCount; ++locNum) { - newActivation.set(slot + locNum); - glVertexAttribFormat(slot + locNum, count, type, isNormalized, offset + locNum * perLocationSize); - glVertexAttribBinding(slot + locNum, attrib._channel); - } - glVertexBindingDivisor(attrib._channel, attrib._frequency); - } - (void) CHECK_GL_ERROR(); - } - - // Manage Activation what was and what is expected now - for (size_t i = 0; i < newActivation.size(); i++) { - bool newState = newActivation[i]; - if (newState != _input._attributeActivation[i]) { - if (newState) { - glEnableVertexAttribArray(i); - } else { - glDisableVertexAttribArray(i); - } - _input._attributeActivation.flip(i); - } - } - (void) CHECK_GL_ERROR(); - - _input._invalidFormat = false; - _stats._ISNumFormatChanges++; - } - - if (_input._invalidBuffers.any()) { - int numBuffers = _input._buffers.size(); - auto buffer = _input._buffers.data(); - auto vbo = _input._bufferVBOs.data(); - auto offset = _input._bufferOffsets.data(); - auto stride = _input._bufferStrides.data(); - - for (int bufferNum = 0; bufferNum < numBuffers; bufferNum++) { - if (_input._invalidBuffers.test(bufferNum)) { - glBindVertexBuffer(bufferNum, (*vbo), (*offset), (*stride)); - } - buffer++; - vbo++; - offset++; - stride++; - } - _input._invalidBuffers.reset(); - (void) CHECK_GL_ERROR(); - } -#else - if (_input._invalidFormat || _input._invalidBuffers.any()) { - - if (_input._invalidFormat) { - InputStageState::ActivationCache newActivation; - - _stats._ISNumFormatChanges++; - - // Check expected activation - if (_input._format) { - for (auto& it : _input._format->getAttributes()) { - const Stream::Attribute& attrib = (it).second; - uint8_t locationCount = attrib._element.getLocationCount(); - for (int i = 0; i < locationCount; ++i) { - newActivation.set(attrib._slot + i); - } - } - } - - // Manage Activation what was and what is expected now - for (unsigned int i = 0; i < newActivation.size(); i++) { - bool newState = newActivation[i]; - if (newState != _input._attributeActivation[i]) { - - if (newState) { - glEnableVertexAttribArray(i); - } else { - glDisableVertexAttribArray(i); - } - (void) CHECK_GL_ERROR(); - - _input._attributeActivation.flip(i); - } - } - } - - // now we need to bind the buffers and assign the attrib pointers - if (_input._format) { - const Buffers& buffers = _input._buffers; - const Offsets& offsets = _input._bufferOffsets; - const Offsets& strides = _input._bufferStrides; - - const Stream::Format::AttributeMap& attributes = _input._format->getAttributes(); - auto& inputChannels = _input._format->getChannels(); - _stats._ISNumInputBufferChanges++; - - GLuint boundVBO = 0; - for (auto& channelIt : inputChannels) { - const Stream::Format::ChannelMap::value_type::second_type& channel = (channelIt).second; - if ((channelIt).first < buffers.size()) { - int bufferNum = (channelIt).first; - - if (_input._invalidBuffers.test(bufferNum) || _input._invalidFormat) { - // GLuint vbo = gpu::GL41Backend::getBufferID((*buffers[bufferNum])); - GLuint vbo = _input._bufferVBOs[bufferNum]; - if (boundVBO != vbo) { - glBindBuffer(GL_ARRAY_BUFFER, vbo); - (void) CHECK_GL_ERROR(); - boundVBO = vbo; - } - _input._invalidBuffers[bufferNum] = false; - - for (unsigned int i = 0; i < channel._slots.size(); i++) { - const Stream::Attribute& attrib = attributes.at(channel._slots[i]); - GLuint slot = attrib._slot; - GLuint count = attrib._element.getLocationScalarCount(); - uint8_t locationCount = attrib._element.getLocationCount(); - GLenum type = gl::ELEMENT_TYPE_TO_GL[attrib._element.getType()]; - // GLenum perLocationStride = strides[bufferNum]; - GLenum perLocationStride = attrib._element.getLocationSize(); - GLuint stride = (GLuint)strides[bufferNum]; - GLuint pointer = (GLuint)(attrib._offset + offsets[bufferNum]); - GLboolean isNormalized = attrib._element.isNormalized(); - - for (size_t locNum = 0; locNum < locationCount; ++locNum) { - glVertexAttribPointer(slot + (GLuint)locNum, count, type, isNormalized, stride, - reinterpret_cast(pointer + perLocationStride * (GLuint)locNum)); - glVertexAttribDivisor(slot + (GLuint)locNum, attrib._frequency); - } - - // TODO: Support properly the IAttrib version - - (void) CHECK_GL_ERROR(); - } - } - } - } - } - // everything format related should be in sync now - _input._invalidFormat = false; - } -#endif + Parent::updateInput(); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp index 3c6109bbdf..478d210535 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp @@ -16,7 +16,7 @@ using namespace gpu; using namespace gpu::gl41; class GL41Query : public gpu::gl::GLQuery { - using Parent = gpu::gl::GLBuffer; + using Parent = gpu::gl::GLQuery; public: static GLuint allocateQuery() { GLuint result; @@ -25,7 +25,7 @@ public: } GL41Query(const Query& query) - : gl::GLQuery(query, allocateQuery()) { } + : Parent(query, allocateQuery()) { } }; gl::GLQuery* GL41Backend::syncGPUObject(const Query& query) { diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp new file mode 100644 index 0000000000..bb6ae67233 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp @@ -0,0 +1,149 @@ +// +// Created by Sam Gateau on 10/27/2014. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GL45Backend.h" + +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(gpugl45logging, "hifi.gpu.gl45") + +using namespace gpu; +using namespace gpu::gl45; + +void GL45Backend::do_draw(Batch& batch, size_t paramOffset) { + Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; + uint32 numVertices = batch._params[paramOffset + 1]._uint; + uint32 startVertex = batch._params[paramOffset + 0]._uint; + + if (isStereo()) { + setupStereoSide(0); + glDrawArrays(mode, startVertex, numVertices); + setupStereoSide(1); + glDrawArrays(mode, startVertex, numVertices); + + _stats._DSNumTriangles += 2 * numVertices / 3; + _stats._DSNumDrawcalls += 2; + + } else { + glDrawArrays(mode, startVertex, numVertices); + _stats._DSNumTriangles += numVertices / 3; + _stats._DSNumDrawcalls++; + } + _stats._DSNumAPIDrawcalls++; + + (void) CHECK_GL_ERROR(); +} + +void GL45Backend::do_drawIndexed(Batch& batch, size_t paramOffset) { + Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; + uint32 numIndices = batch._params[paramOffset + 1]._uint; + uint32 startIndex = batch._params[paramOffset + 0]._uint; + + GLenum glType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType]; + + auto typeByteSize = TYPE_SIZE[_input._indexBufferType]; + GLvoid* indexBufferByteOffset = reinterpret_cast(startIndex * typeByteSize + _input._indexBufferOffset); + + if (isStereo()) { + setupStereoSide(0); + glDrawElements(mode, numIndices, glType, indexBufferByteOffset); + setupStereoSide(1); + glDrawElements(mode, numIndices, glType, indexBufferByteOffset); + + _stats._DSNumTriangles += 2 * numIndices / 3; + _stats._DSNumDrawcalls += 2; + } else { + glDrawElements(mode, numIndices, glType, indexBufferByteOffset); + _stats._DSNumTriangles += numIndices / 3; + _stats._DSNumDrawcalls++; + } + _stats._DSNumAPIDrawcalls++; + + (void) CHECK_GL_ERROR(); +} + +void GL45Backend::do_drawInstanced(Batch& batch, size_t paramOffset) { + GLint numInstances = batch._params[paramOffset + 4]._uint; + Primitive primitiveType = (Primitive)batch._params[paramOffset + 3]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; + uint32 numVertices = batch._params[paramOffset + 2]._uint; + uint32 startVertex = batch._params[paramOffset + 1]._uint; + + + if (isStereo()) { + GLint trueNumInstances = 2 * numInstances; + + setupStereoSide(0); + glDrawArraysInstanced(mode, startVertex, numVertices, numInstances); + setupStereoSide(1); + glDrawArraysInstanced(mode, startVertex, numVertices, numInstances); + + _stats._DSNumTriangles += (trueNumInstances * numVertices) / 3; + _stats._DSNumDrawcalls += trueNumInstances; + } else { + glDrawArraysInstanced(mode, startVertex, numVertices, numInstances); + _stats._DSNumTriangles += (numInstances * numVertices) / 3; + _stats._DSNumDrawcalls += numInstances; + } + _stats._DSNumAPIDrawcalls++; + + (void) CHECK_GL_ERROR(); +} + +void GL45Backend::do_drawIndexedInstanced(Batch& batch, size_t paramOffset) { + GLint numInstances = batch._params[paramOffset + 4]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 3]._uint]; + uint32 numIndices = batch._params[paramOffset + 2]._uint; + uint32 startIndex = batch._params[paramOffset + 1]._uint; + uint32 startInstance = batch._params[paramOffset + 0]._uint; + GLenum glType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType]; + auto typeByteSize = TYPE_SIZE[_input._indexBufferType]; + GLvoid* indexBufferByteOffset = reinterpret_cast(startIndex * typeByteSize + _input._indexBufferOffset); + + if (isStereo()) { + GLint trueNumInstances = 2 * numInstances; + setupStereoSide(0); + glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); + setupStereoSide(1); + glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); + _stats._DSNumTriangles += (trueNumInstances * numIndices) / 3; + _stats._DSNumDrawcalls += trueNumInstances; + } else { + glDrawElementsInstancedBaseVertexBaseInstance(mode, numIndices, glType, indexBufferByteOffset, numInstances, 0, startInstance); + _stats._DSNumTriangles += (numInstances * numIndices) / 3; + _stats._DSNumDrawcalls += numInstances; + } + + _stats._DSNumAPIDrawcalls++; + + (void)CHECK_GL_ERROR(); +} + +void GL45Backend::do_multiDrawIndirect(Batch& batch, size_t paramOffset) { + uint commandCount = batch._params[paramOffset + 0]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; + glMultiDrawArraysIndirect(mode, reinterpret_cast(_input._indirectBufferOffset), commandCount, (GLsizei)_input._indirectBufferStride); + _stats._DSNumDrawcalls += commandCount; + _stats._DSNumAPIDrawcalls++; + (void)CHECK_GL_ERROR(); +} + +void GL45Backend::do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) { + uint commandCount = batch._params[paramOffset + 0]._uint; + GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; + GLenum indexType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType]; + glMultiDrawElementsIndirect(mode, indexType, reinterpret_cast(_input._indirectBufferOffset), commandCount, (GLsizei)_input._indirectBufferStride); + _stats._DSNumDrawcalls += commandCount; + _stats._DSNumAPIDrawcalls++; + (void)CHECK_GL_ERROR(); +} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h new file mode 100644 index 0000000000..d0dfbd0e41 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -0,0 +1,85 @@ +// +// GL45Backend.h +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 10/27/2014. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_45_GL45Backend_h +#define hifi_gpu_45_GL45Backend_h + +#include "../gl/GLBackend.h" +#include "../gl/GLTexture.h" + +namespace gpu { namespace gl45 { + +class GL45Backend : public gl::GLBackend { + using Parent = gl::GLBackend; + // Context Backend static interface required + friend class Context; + +public: + explicit GL45Backend(bool syncCache) : Parent(syncCache) {} + GL45Backend() : Parent() {} + + class GL45Texture : public gpu::gl::GLTexture { + using Parent = gpu::gl::GLTexture; + GLuint allocate(const Texture& texture); + public: + GL45Texture(const Texture& texture, bool transferrable); + GL45Texture(const Texture& texture, GLTexture* original); + + protected: + void transferMip(uint16_t mipLevel, uint8_t face = 0) const; + void allocateStorage() const override; + void updateSize() const override; + void transfer() const override; + void syncSampler() const override; + void generateMips() const override; + void withPreservedTexture(std::function f) const override; + }; + + +protected: + GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; + gl::GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; + + GLuint getBufferID(const Buffer& buffer) override; + gl::GLBuffer* syncGPUObject(const Buffer& buffer) override; + + GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; + gl::GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; + + GLuint getQueryID(const QueryPointer& query) override; + gl::GLQuery* syncGPUObject(const Query& query) override; + + // Draw Stage + void do_draw(Batch& batch, size_t paramOffset) override; + void do_drawIndexed(Batch& batch, size_t paramOffset) override; + void do_drawInstanced(Batch& batch, size_t paramOffset) override; + void do_drawIndexedInstanced(Batch& batch, size_t paramOffset) override; + void do_multiDrawIndirect(Batch& batch, size_t paramOffset) override; + void do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) override; + + // Input Stage + void updateInput() override; + + // Synchronize the state cache of this Backend with the actual real state of the GL Context + void transferTransformState(const Batch& batch) const override; + void initTransform() override; + void updateTransform(const Batch& batch); + void resetTransformStage(); + + // Output stage + void do_blit(Batch& batch, size_t paramOffset) override; +}; + +} } + +Q_DECLARE_LOGGING_CATEGORY(gpugl45logging) + + +#endif diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp new file mode 100644 index 0000000000..1676b0ce1c --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp @@ -0,0 +1,50 @@ +// +// Created by Bradley Austin Davis on 2016/05/15 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GL45Backend.h" +#include "../gl/GLBuffer.h" + +using namespace gpu; +using namespace gpu::gl45; + +class GL45Buffer : public gl::GLBuffer { + using Parent = gpu::gl::GLBuffer; + static GLuint allocate() { + GLuint result; + glCreateBuffers(1, &result); + return result; + } + +public: + GL45Buffer(const Buffer& buffer, GLBuffer* original) : Parent(buffer, allocate()) { + glNamedBufferStorage(_buffer, _size, nullptr, GL_DYNAMIC_STORAGE_BIT); + if (original && original->_size) { + glCopyNamedBufferSubData(original->_buffer, _buffer, 0, 0, std::min(original->_size, _size)); + } + Backend::setGPUObject(buffer, this); + } + + void transfer() override { + Size offset; + Size size; + Size currentPage { 0 }; + auto data = _gpuObject.getSysmem().readData(); + while (_gpuObject.getNextTransferBlock(offset, size, currentPage)) { + glNamedBufferSubData(_buffer, (GLintptr)offset, (GLsizeiptr)size, data + offset); + } + (void)CHECK_GL_ERROR(); + _gpuObject._flags &= ~Buffer::DIRTY; + } +}; + +GLuint GL45Backend::getBufferID(const Buffer& buffer) { + return GL45Buffer::getId(buffer); +} + +gl::GLBuffer* GL45Backend::syncGPUObject(const Buffer& buffer) { + return GL45Buffer::sync(buffer); +} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp new file mode 100644 index 0000000000..b578d7d7a8 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp @@ -0,0 +1,18 @@ +// +// GL45BackendInput.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 3/8/2015. +// 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 "GL45Backend.h" + +using namespace gpu; +using namespace gpu::gl45; + +void GL45Backend::updateInput() { + Parent::updateInput(); +} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp new file mode 100644 index 0000000000..b846dd4df3 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp @@ -0,0 +1,145 @@ +// +// GL45BackendTexture.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 1/19/2015. +// 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 "GL45Backend.h" +#include "../gl/GLFramebuffer.h" +#include "../gl/GLTexture.h" + +#include + +namespace gpu { namespace gl45 { + +class GL45Framebuffer : public gl::GLFramebuffer { + using Parent = gl::GLFramebuffer; + static GLuint allocate() { + GLuint result; + glCreateFramebuffers(1, &result); + return result; + } +public: + void update() override { + gl::GLTexture* gltexture = nullptr; + TexturePointer surface; + if (_gpuObject.getColorStamps() != _colorStamps) { + if (_gpuObject.hasColor()) { + _colorBuffers.clear(); + static const GLenum colorAttachments[] = { + GL_COLOR_ATTACHMENT0, + GL_COLOR_ATTACHMENT1, + GL_COLOR_ATTACHMENT2, + GL_COLOR_ATTACHMENT3, + GL_COLOR_ATTACHMENT4, + GL_COLOR_ATTACHMENT5, + GL_COLOR_ATTACHMENT6, + GL_COLOR_ATTACHMENT7, + GL_COLOR_ATTACHMENT8, + GL_COLOR_ATTACHMENT9, + GL_COLOR_ATTACHMENT10, + GL_COLOR_ATTACHMENT11, + GL_COLOR_ATTACHMENT12, + GL_COLOR_ATTACHMENT13, + GL_COLOR_ATTACHMENT14, + GL_COLOR_ATTACHMENT15 }; + + int unit = 0; + for (auto& b : _gpuObject.getRenderBuffers()) { + surface = b._texture; + if (surface) { + gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer + } else { + gltexture = nullptr; + } + + if (gltexture) { + glNamedFramebufferTexture(_id, colorAttachments[unit], gltexture->_texture, 0); + _colorBuffers.push_back(colorAttachments[unit]); + } else { + glNamedFramebufferTexture(_id, colorAttachments[unit], 0, 0); + } + unit++; + } + } + _colorStamps = _gpuObject.getColorStamps(); + } + + GLenum attachement = GL_DEPTH_STENCIL_ATTACHMENT; + if (!_gpuObject.hasStencil()) { + attachement = GL_DEPTH_ATTACHMENT; + } else if (!_gpuObject.hasDepth()) { + attachement = GL_STENCIL_ATTACHMENT; + } + + if (_gpuObject.getDepthStamp() != _depthStamp) { + auto surface = _gpuObject.getDepthStencilBuffer(); + if (_gpuObject.hasDepthStencil() && surface) { + gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer + } + + if (gltexture) { + glNamedFramebufferTexture(_id, attachement, gltexture->_texture, 0); + } else { + glNamedFramebufferTexture(_id, attachement, 0, 0); + } + _depthStamp = _gpuObject.getDepthStamp(); + } + + + // Last but not least, define where we draw + if (!_colorBuffers.empty()) { + glNamedFramebufferDrawBuffers(_id, (GLsizei)_colorBuffers.size(), _colorBuffers.data()); + } else { + glNamedFramebufferDrawBuffer(_id, GL_NONE); + } + + // Now check for completness + _status = glCheckNamedFramebufferStatus(_id, GL_DRAW_FRAMEBUFFER); + + // restore the current framebuffer + checkStatus(GL_DRAW_FRAMEBUFFER); + } + + +public: + GL45Framebuffer(const gpu::Framebuffer& framebuffer) + : Parent(framebuffer, allocate()) { } +}; + +gl::GLFramebuffer* GL45Backend::syncGPUObject(const Framebuffer& framebuffer) { + return gl::GLFramebuffer::sync(framebuffer); +} + +GLuint GL45Backend::getFramebufferID(const FramebufferPointer& framebuffer) { + return framebuffer ? gl::GLFramebuffer::getId(*framebuffer) : 0; +} + +void GL45Backend::do_blit(Batch& batch, size_t paramOffset) { + auto srcframebuffer = batch._framebuffers.get(batch._params[paramOffset]._uint); + Vec4i srcvp; + for (auto i = 0; i < 4; ++i) { + srcvp[i] = batch._params[paramOffset + 1 + i]._int; + } + + auto dstframebuffer = batch._framebuffers.get(batch._params[paramOffset + 5]._uint); + Vec4i dstvp; + for (auto i = 0; i < 4; ++i) { + dstvp[i] = batch._params[paramOffset + 6 + i]._int; + } + + // Assign dest framebuffer if not bound already + auto destFbo = getFramebufferID(dstframebuffer); + auto srcFbo = getFramebufferID(srcframebuffer); + glBlitNamedFramebuffer(srcFbo, destFbo, + srcvp.x, srcvp.y, srcvp.z, srcvp.w, + dstvp.x, dstvp.y, dstvp.z, dstvp.w, + GL_COLOR_BUFFER_BIT, GL_LINEAR); + (void) CHECK_GL_ERROR(); +} + +} } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp new file mode 100644 index 0000000000..a7a7c26bed --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp @@ -0,0 +1,38 @@ +// +// GL45BackendQuery.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 7/7/2015. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GL45Backend.h" + +#include "../gl/GLQuery.h" + +namespace gpu { namespace gl45 { + +class GL45Query : public gpu::gl::GLQuery { + using Parent = gpu::gl::GLQuery; +public: + static GLuint allocateQuery() { + GLuint result; + glCreateQueries(GL_TIME_ELAPSED, 1, &result); + return result; + } + + GL45Query(const Query& query) + : Parent(query, allocateQuery()) { } +}; + +gl::GLQuery* GL45Backend::syncGPUObject(const Query& query) { + return GL45Query::sync(query); +} + +GLuint GL45Backend::getQueryID(const QueryPointer& query) { + return GL45Query::getId(query); +} + +} } \ No newline at end of file diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp new file mode 100644 index 0000000000..36fb4bfde3 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -0,0 +1,181 @@ +// +// GL45BackendTexture.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 1/19/2015. +// 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 "GL45Backend.h" + +#include +#include +#include + +#include "../gl/GLTexelFormat.h" + +using namespace gpu; +using namespace gpu::gl45; + +using GLTexelFormat = gl::GLTexelFormat; +using GL45Texture = GL45Backend::GL45Texture; + +GLuint GL45Texture::allocate(const Texture& texture) { + Backend::incrementTextureGPUCount(); + GLuint result; + glCreateTextures(getGLTextureType(texture), 1, &result); + return result; +} + +GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) { + return GL45Texture::getId(texture, transfer); +} + +gl::GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { + return GL45Texture::sync(texture, transfer); +} + +GL45Backend::GL45Texture::GL45Texture(const Texture& texture, bool transferrable) + : gl::GLTexture(texture, allocate(texture), transferrable) {} + +GL45Backend::GL45Texture::GL45Texture(const Texture& texture, GLTexture* original) + : gl::GLTexture(texture, allocate(texture), original) {} + +void GL45Backend::GL45Texture::withPreservedTexture(std::function f) const { + f(); +} + +void GL45Backend::GL45Texture::generateMips() const { + glGenerateTextureMipmap(_id); + (void)CHECK_GL_ERROR(); +} + +void GL45Backend::GL45Texture::allocateStorage() const { + gl::GLTexelFormat texelFormat = gl::GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); + glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); + if (_gpuObject.getTexelFormat().isCompressed()) { + qFatal("Compressed textures not yet supported"); + } + // Get the dimensions, accounting for the downgrade level + Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip); + glTextureStorage2D(_id, usedMipLevels(), texelFormat.internalFormat, dimensions.x, dimensions.y); + (void)CHECK_GL_ERROR(); +} + +void GL45Backend::GL45Texture::updateSize() const { + setSize(_virtualSize); + if (!_id) { + return; + } + + if (_gpuObject.getTexelFormat().isCompressed()) { + qFatal("Compressed textures not yet supported"); + } +} + +// Move content bits from the CPU to the GPU for a given mip / face +void GL45Backend::GL45Texture::transferMip(uint16_t mipLevel, uint8_t face) const { + auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); + gl::GLTexelFormat texelFormat = gl::GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); + auto size = _gpuObject.evalMipDimensions(mipLevel); + if (GL_TEXTURE_2D == _target) { + glTextureSubImage2D(_id, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + } else if (GL_TEXTURE_CUBE_MAP == _target) { + glTextureSubImage3D(_id, mipLevel, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); + } else { + Q_ASSERT(false); + } + (void)CHECK_GL_ERROR(); +} + +// This should never happen on the main thread +// Move content bits from the CPU to the GPU +void GL45Backend::GL45Texture::transfer() const { + PROFILE_RANGE(__FUNCTION__); + //qDebug() << "Transferring texture: " << _privateTexture; + // Need to update the content of the GPU object from the source sysmem of the texture + if (_contentStamp >= _gpuObject.getDataStamp()) { + return; + } + + if (_downsampleSource._texture) { + GLuint fbo { 0 }; + glCreateFramebuffers(1, &fbo); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + // Find the distance between the old min mip and the new one + uint16 mipOffset = _minMip - _downsampleSource._minMip; + for (uint16 i = _minMip; i <= _maxMip; ++i) { + uint16 targetMip = i - _minMip; + uint16 sourceMip = targetMip + mipOffset; + Vec3u dimensions = _gpuObject.evalMipDimensions(i); + for (GLenum target : getFaceTargets(_target)) { + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, _downsampleSource._texture, sourceMip); + (void)CHECK_GL_ERROR(); + glCopyTextureSubImage2D(_id, targetMip, 0, 0, 0, 0, dimensions.x, dimensions.y); + (void)CHECK_GL_ERROR(); + } + } + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + } else { + // GO through the process of allocating the correct storage and/or update the content + switch (_gpuObject.getType()) { + case Texture::TEX_2D: + { + for (uint16_t i = _minMip; i <= _maxMip; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i)) { + transferMip(i); + } + } + } + break; + + case Texture::TEX_CUBE: + // transfer pixels from each faces + for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) { + for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { + if (_gpuObject.isStoredMipFaceAvailable(i, f)) { + transferMip(i, f); + } + } + } + break; + + default: + qCWarning(gpugl45logging) << __FUNCTION__ << " case for Texture Type " << _gpuObject.getType() << " not supported"; + break; + } + } + if (_gpuObject.isAutogenerateMips()) { + glGenerateTextureMipmap(_id); + (void)CHECK_GL_ERROR(); + } +} + +void GL45Backend::GL45Texture::syncSampler() const { + const Sampler& sampler = _gpuObject.getSampler(); + + const auto& fm = FILTER_MODES[sampler.getFilter()]; + glTextureParameteri(_id, GL_TEXTURE_MIN_FILTER, fm.minFilter); + glTextureParameteri(_id, GL_TEXTURE_MAG_FILTER, fm.magFilter); + + if (sampler.doComparison()) { + glTextureParameteri(_id, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE); + glTextureParameteri(_id, GL_TEXTURE_COMPARE_FUNC, gl::COMPARISON_TO_GL[sampler.getComparisonFunction()]); + } else { + glTextureParameteri(_id, GL_TEXTURE_COMPARE_MODE, GL_NONE); + } + + glTextureParameteri(_id, GL_TEXTURE_WRAP_S, WRAP_MODES[sampler.getWrapModeU()]); + glTextureParameteri(_id, GL_TEXTURE_WRAP_T, WRAP_MODES[sampler.getWrapModeV()]); + glTextureParameteri(_id, GL_TEXTURE_WRAP_R, WRAP_MODES[sampler.getWrapModeW()]); + glTextureParameterfv(_id, GL_TEXTURE_BORDER_COLOR, (const float*)&sampler.getBorderColor()); + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, (uint16)sampler.getMipOffset()); + glTextureParameterf(_id, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); + glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); + glTextureParameterf(_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); +} + diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp new file mode 100644 index 0000000000..96afb4cc71 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp @@ -0,0 +1,71 @@ +// +// GL45BackendTransform.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 3/8/2015. +// 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 "GL45Backend.h" + +using namespace gpu; +using namespace gpu::gl45; + +void GL45Backend::initTransform() { + GLuint transformBuffers[3]; + glCreateBuffers(3, transformBuffers); + _transform._objectBuffer = transformBuffers[0]; + _transform._cameraBuffer = transformBuffers[1]; + _transform._drawCallInfoBuffer = transformBuffers[2]; + glCreateTextures(GL_TEXTURE_BUFFER, 1, &_transform._objectBufferTexture); + size_t cameraSize = sizeof(TransformStageState::CameraBufferElement); + while (_transform._cameraUboSize < cameraSize) { + _transform._cameraUboSize += _uboAlignment; + } +} + +void GL45Backend::transferTransformState(const Batch& batch) const { + // FIXME not thread safe + static std::vector bufferData; + if (!_transform._cameras.empty()) { + bufferData.resize(_transform._cameraUboSize * _transform._cameras.size()); + for (size_t i = 0; i < _transform._cameras.size(); ++i) { + memcpy(bufferData.data() + (_transform._cameraUboSize * i), &_transform._cameras[i], sizeof(TransformStageState::CameraBufferElement)); + } + glNamedBufferData(_transform._cameraBuffer, bufferData.size(), bufferData.data(), GL_STREAM_DRAW); + } + + if (!batch._objects.empty()) { + auto byteSize = batch._objects.size() * sizeof(Batch::TransformObject); + bufferData.resize(byteSize); + memcpy(bufferData.data(), batch._objects.data(), byteSize); + glNamedBufferData(_transform._objectBuffer, bufferData.size(), bufferData.data(), GL_STREAM_DRAW); + } + + if (!batch._namedData.empty()) { + bufferData.clear(); + for (auto& data : batch._namedData) { + auto currentSize = bufferData.size(); + auto bytesToCopy = data.second.drawCallInfos.size() * sizeof(Batch::DrawCallInfo); + bufferData.resize(currentSize + bytesToCopy); + memcpy(bufferData.data() + currentSize, data.second.drawCallInfos.data(), bytesToCopy); + _transform._drawCallInfoOffsets[data.first] = (GLvoid*)currentSize; + } + glNamedBufferData(_transform._drawCallInfoBuffer, bufferData.size(), bufferData.data(), GL_STREAM_DRAW); + } + +#ifdef GPU_SSBO_DRAW_CALL_INFO + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, TRANSFORM_OBJECT_SLOT, _transform._objectBuffer); +#else + glTextureBuffer(_transform._objectBufferTexture, GL_RGBA32F, _transform._objectBuffer); + glActiveTexture(GL_TEXTURE0 + TRANSFORM_OBJECT_SLOT); + glBindTexture(GL_TEXTURE_BUFFER, _transform._objectBufferTexture); +#endif + + CHECK_GL_ERROR(); + + // Make sure the current Camera offset is unknown before render Draw + _transform._currentCameraOffset = INVALID_OFFSET; +} diff --git a/libraries/gpu/src/gpu/Shader.h b/libraries/gpu/src/gpu/Shader.h index 59c6401150..e4643f2b7c 100755 --- a/libraries/gpu/src/gpu/Shader.h +++ b/libraries/gpu/src/gpu/Shader.h @@ -120,6 +120,9 @@ public: bool isProgram() const { return getType() > NUM_DOMAINS; } bool isDomain() const { return getType() < NUM_DOMAINS; } + void setCompilationHasFailed(bool compilationHasFailed) { _compilationHasFailed = compilationHasFailed; } + bool compilationHasFailed() const { return _compilationHasFailed; } + const Source& getSource() const { return _source; } const Shaders& getShaders() const { return _shaders; } @@ -180,6 +183,9 @@ protected: // The type of the shader, the master key Type _type; + + // Whether or not the shader compilation failed + bool _compilationHasFailed { false }; }; typedef Shader::Pointer ShaderPointer; diff --git a/libraries/gpu/src/gpu/null/NullBackend.h b/libraries/gpu/src/gpu/null/NullBackend.h new file mode 100644 index 0000000000..097cee27e7 --- /dev/null +++ b/libraries/gpu/src/gpu/null/NullBackend.h @@ -0,0 +1,54 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// 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_gpu_Null_Backend_h +#define hifi_gpu_Null_Backend_h + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../Context.h" + +namespace gpu { namespace null { + +class Backend : public gpu::Backend { + using Parent = gpu::Backend; + // Context Backend static interface required + friend class gpu::Context; + static void init() {} + static gpu::Backend* createBackend() { return new Backend(); } + static bool makeProgram(Shader& shader, const Shader::BindingSet& slotBindings) { return true; } + +protected: + explicit Backend(bool syncCache) : Parent() { } + Backend() : Parent() { } +public: + ~Backend() { } + + void render(Batch& batch) final { } + + // This call synchronize the Full Backend cache with the current GLState + // THis is only intended to be used when mixing raw gl calls with the gpu api usage in order to sync + // the gpu::Backend state with the true gl state which has probably been messed up by these ugly naked gl calls + // Let's try to avoid to do that as much as possible! + void syncCache() final { } + + // This is the ugly "download the pixels to sysmem for taking a snapshot" + // Just avoid using it, it's ugly and will break performances + virtual void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) final { } +}; + +} } + +#endif diff --git a/libraries/input-plugins/CMakeLists.txt b/libraries/input-plugins/CMakeLists.txt index b81554511d..b0ea13843b 100644 --- a/libraries/input-plugins/CMakeLists.txt +++ b/libraries/input-plugins/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME input-plugins) setup_hifi_library() -link_hifi_libraries(shared plugins controllers) +link_hifi_libraries(shared plugins ui-plugins controllers) GroupSources("src/input-plugins") diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 26b3801ec1..e49bf5d5a5 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -44,6 +44,7 @@ Q_DECLARE_METATYPE(QNetworkAccessManager::Operation) Q_DECLARE_METATYPE(JSONCallbackParameters) const QString ACCOUNTS_GROUP = "accounts"; +static const auto METAVERSE_SESSION_ID_HEADER = QString("HFM-SessionID").toLocal8Bit(); JSONCallbackParameters::JSONCallbackParameters(QObject* jsonCallbackReceiver, const QString& jsonCallbackMethod, QObject* errorCallbackReceiver, const QString& errorCallbackMethod, @@ -222,8 +223,7 @@ void AccountManager::sendRequest(const QString& path, // if we're allowed to send usage data, include whatever the current session ID is with this request auto& activityLogger = UserActivityLogger::getInstance(); if (activityLogger.isEnabled()) { - static const QString METAVERSE_SESSION_ID_HEADER = "HFM-SessionID"; - networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER.toLocal8Bit(), + networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER, uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit()); } @@ -322,6 +322,9 @@ void AccountManager::processReply() { QNetworkReply* requestReply = reinterpret_cast(sender()); if (requestReply->error() == QNetworkReply::NoError) { + if (requestReply->hasRawHeader(METAVERSE_SESSION_ID_HEADER)) { + _sessionID = requestReply->rawHeader(METAVERSE_SESSION_ID_HEADER); + } passSuccessToCallback(requestReply); } else { passErrorToCallback(requestReply); @@ -471,6 +474,11 @@ void AccountManager::setAccessTokenForCurrentAuthURL(const QString& accessToken) persistAccountToFile(); } +void AccountManager::setTemporaryDomain(const QUuid& domainID, const QString& key) { + _accountInfo.setTemporaryDomain(domainID, key); + persistAccountToFile(); +} + void AccountManager::requestAccessToken(const QString& login, const QString& password) { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); @@ -647,22 +655,33 @@ void AccountManager::processGeneratedKeypair() { const QString DOMAIN_PUBLIC_KEY_UPDATE_PATH = "api/v1/domains/%1/public_key"; QString uploadPath; - if (keypairGenerator->getDomainID().isNull()) { + const auto& domainID = keypairGenerator->getDomainID(); + if (domainID.isNull()) { uploadPath = USER_PUBLIC_KEY_UPDATE_PATH; } else { - uploadPath = DOMAIN_PUBLIC_KEY_UPDATE_PATH.arg(uuidStringWithoutCurlyBraces(keypairGenerator->getDomainID())); + uploadPath = DOMAIN_PUBLIC_KEY_UPDATE_PATH.arg(uuidStringWithoutCurlyBraces(domainID)); } // setup a multipart upload to send up the public key QHttpMultiPart* requestMultiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - QHttpPart keyPart; - keyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); - keyPart.setHeader(QNetworkRequest::ContentDispositionHeader, - QVariant("form-data; name=\"public_key\"; filename=\"public_key\"")); - keyPart.setBody(keypairGenerator->getPublicKey()); + QHttpPart publicKeyPart; + publicKeyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); - requestMultiPart->append(keyPart); + publicKeyPart.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"public_key\"; filename=\"public_key\"")); + publicKeyPart.setBody(keypairGenerator->getPublicKey()); + requestMultiPart->append(publicKeyPart); + + if (!domainID.isNull()) { + const auto& key = getTemporaryDomainKey(domainID); + QHttpPart apiKeyPart; + publicKeyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); + apiKeyPart.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"api_key\"")); + apiKeyPart.setBody(key.toUtf8()); + requestMultiPart->append(apiKeyPart); + } // setup callback parameters so we know once the keypair upload has succeeded or failed JSONCallbackParameters callbackParameters; diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 4803d2625f..c12f663d3e 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -26,9 +26,9 @@ class JSONCallbackParameters { public: - JSONCallbackParameters(QObject* jsonCallbackReceiver = NULL, const QString& jsonCallbackMethod = QString(), - QObject* errorCallbackReceiver = NULL, const QString& errorCallbackMethod = QString(), - QObject* updateReceiver = NULL, const QString& updateSlot = QString()); + JSONCallbackParameters(QObject* jsonCallbackReceiver = nullptr, const QString& jsonCallbackMethod = QString(), + QObject* errorCallbackReceiver = nullptr, const QString& errorCallbackMethod = QString(), + QObject* updateReceiver = nullptr, const QString& updateSlot = QString()); bool isEmpty() const { return !jsonCallbackReceiver && !errorCallbackReceiver; } @@ -86,8 +86,12 @@ public: static QJsonObject dataObjectFromResponse(QNetworkReply& requestReply); + QUuid getSessionID() const { return _sessionID; } void setSessionID(const QUuid& sessionID) { _sessionID = sessionID; } + void setTemporaryDomain(const QUuid& domainID, const QString& key); + const QString& getTemporaryDomainKey(const QUuid& domainID) { return _accountInfo.getTemporaryDomainKey(domainID); } + public slots: void requestAccessToken(const QString& login, const QString& password); @@ -139,7 +143,7 @@ private: bool _isWaitingForKeypairResponse { false }; QByteArray _pendingPrivateKey; - QUuid _sessionID; + QUuid _sessionID { QUuid::createUuid() }; }; #endif // hifi_AccountManager_h diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 80989acd2c..df9b4094b0 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -23,6 +23,7 @@ #include "AddressManager.h" #include "NodeList.h" #include "NetworkLogging.h" +#include "UserActivityLogger.h" const QString ADDRESS_MANAGER_SETTINGS_GROUP = "AddressManager"; @@ -130,6 +131,10 @@ const JSONCallbackParameters& AddressManager::apiCallbackParameters() { } bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { + static QString URL_TYPE_USER = "user"; + static QString URL_TYPE_DOMAIN_ID = "domain_id"; + static QString URL_TYPE_PLACE = "place"; + static QString URL_TYPE_NETWORK_ADDRESS = "network_address"; if (lookupUrl.scheme() == HIFI_URL_SCHEME) { qCDebug(networking) << "Trying to go to URL" << lookupUrl.toString(); @@ -147,6 +152,8 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { if (handleUsername(lookupUrl.authority())) { // handled a username for lookup + UserActivityLogger::getInstance().wentTo(trigger, URL_TYPE_USER, lookupUrl.toString()); + // in case we're failing to connect to where we thought this user was // store their username as previous lookup so we can refresh their location via API _previousLookup = lookupUrl; @@ -157,6 +164,8 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { if (handleNetworkAddress(lookupUrl.host() + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { + UserActivityLogger::getInstance().wentTo(trigger, URL_TYPE_NETWORK_ADDRESS, lookupUrl.toString()); + // a network address lookup clears the previous lookup since we don't expect to re-attempt it _previousLookup.clear(); @@ -174,6 +183,8 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // we may have a path that defines a relative viewpoint - if so we should jump to that now handlePath(path, trigger); } else if (handleDomainID(lookupUrl.host())){ + UserActivityLogger::getInstance().wentTo(trigger, URL_TYPE_DOMAIN_ID, lookupUrl.toString()); + // store this domain ID as the previous lookup in case we're failing to connect and want to refresh API info _previousLookup = lookupUrl; @@ -181,6 +192,8 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // try to look up the domain ID on the metaverse API attemptDomainIDLookup(lookupUrl.host(), lookupUrl.path(), trigger); } else { + UserActivityLogger::getInstance().wentTo(trigger, URL_TYPE_PLACE, lookupUrl.toString()); + // store this place name as the previous lookup in case we fail to connect and want to refresh API info _previousLookup = lookupUrl; diff --git a/libraries/networking/src/DataServerAccountInfo.cpp b/libraries/networking/src/DataServerAccountInfo.cpp index 211bfdccfa..6c6f3eb90c 100644 --- a/libraries/networking/src/DataServerAccountInfo.cpp +++ b/libraries/networking/src/DataServerAccountInfo.cpp @@ -25,6 +25,8 @@ #pragma clang diagnostic ignored "-Wdeprecated-declarations" #endif +const QString DataServerAccountInfo::EMPTY_KEY = QString(); + DataServerAccountInfo::DataServerAccountInfo(const DataServerAccountInfo& otherInfo) : QObject() { _accessToken = otherInfo._accessToken; _username = otherInfo._username; @@ -33,6 +35,8 @@ DataServerAccountInfo::DataServerAccountInfo(const DataServerAccountInfo& otherI _walletID = otherInfo._walletID; _privateKey = otherInfo._privateKey; _domainID = otherInfo._domainID; + _temporaryDomainID = otherInfo._temporaryDomainID; + _temporaryDomainApiKey = otherInfo._temporaryDomainApiKey; } DataServerAccountInfo& DataServerAccountInfo::operator=(const DataServerAccountInfo& otherInfo) { @@ -51,6 +55,8 @@ void DataServerAccountInfo::swap(DataServerAccountInfo& otherInfo) { swap(_walletID, otherInfo._walletID); swap(_privateKey, otherInfo._privateKey); swap(_domainID, otherInfo._domainID); + swap(_temporaryDomainID, otherInfo._temporaryDomainID); + swap(_temporaryDomainApiKey, otherInfo._temporaryDomainApiKey); } void DataServerAccountInfo::setAccessTokenFromJSON(const QJsonObject& jsonObject) { @@ -145,13 +151,14 @@ QByteArray DataServerAccountInfo::signPlaintext(const QByteArray& plaintext) { QDataStream& operator<<(QDataStream &out, const DataServerAccountInfo& info) { out << info._accessToken << info._username << info._xmppPassword << info._discourseApiKey - << info._walletID << info._privateKey << info._domainID; - + << info._walletID << info._privateKey << info._domainID + << info._temporaryDomainID << info._temporaryDomainApiKey; return out; } QDataStream& operator>>(QDataStream &in, DataServerAccountInfo& info) { in >> info._accessToken >> info._username >> info._xmppPassword >> info._discourseApiKey - >> info._walletID >> info._privateKey >> info._domainID; + >> info._walletID >> info._privateKey >> info._domainID + >> info._temporaryDomainID >> info._temporaryDomainApiKey; return in; } diff --git a/libraries/networking/src/DataServerAccountInfo.h b/libraries/networking/src/DataServerAccountInfo.h index 6ee726efde..8cb416cf34 100644 --- a/libraries/networking/src/DataServerAccountInfo.h +++ b/libraries/networking/src/DataServerAccountInfo.h @@ -22,6 +22,7 @@ const float SATOSHIS_PER_CREDIT = 100000000.0f; class DataServerAccountInfo : public QObject { Q_OBJECT + const static QString EMPTY_KEY; public: DataServerAccountInfo() {}; DataServerAccountInfo(const DataServerAccountInfo& otherInfo); @@ -52,6 +53,9 @@ public: void setDomainID(const QUuid& domainID) { _domainID = domainID; } const QUuid& getDomainID() const { return _domainID; } + void setTemporaryDomain(const QUuid& domainID, const QString& key) { _temporaryDomainID = domainID; _temporaryDomainApiKey = key; } + const QString& getTemporaryDomainKey(const QUuid& domainID) { return domainID == _temporaryDomainID ? _temporaryDomainApiKey : EMPTY_KEY; } + bool hasProfile() const; void setProfileInfoFromJSON(const QJsonObject& jsonObject); @@ -67,7 +71,9 @@ private: QString _xmppPassword; QString _discourseApiKey; QUuid _walletID; - QUuid _domainID; // if this holds account info for a domain, this holds the ID of that domain + QUuid _domainID; + QUuid _temporaryDomainID; + QString _temporaryDomainApiKey; QByteArray _privateKey; }; diff --git a/libraries/networking/src/UserActivityLogger.cpp b/libraries/networking/src/UserActivityLogger.cpp index 83c6eb304e..eba4d31167 100644 --- a/libraries/networking/src/UserActivityLogger.cpp +++ b/libraries/networking/src/UserActivityLogger.cpp @@ -18,6 +18,7 @@ #include "UserActivityLogger.h" #include +#include "AddressManager.h" static const QString USER_ACTIVITY_URL = "/api/v1/user_activities"; @@ -125,6 +126,19 @@ void UserActivityLogger::changedDomain(QString domainURL) { } void UserActivityLogger::connectedDevice(QString typeOfDevice, QString deviceName) { + static QStringList DEVICE_BLACKLIST = { + "Desktop", + "NullDisplayPlugin", + "3D TV - Side by Side Stereo", + "3D TV - Interleaved", + + "Keyboard/Mouse" + }; + + if (DEVICE_BLACKLIST.contains(deviceName)) { + return; + } + const QString ACTION_NAME = "connected_device"; QJsonObject actionDetails; const QString TYPE_OF_DEVICE = "type_of_device"; @@ -148,12 +162,34 @@ void UserActivityLogger::loadedScript(QString scriptName) { } -void UserActivityLogger::wentTo(QString destinationType, QString destinationName) { +void UserActivityLogger::wentTo(AddressManager::LookupTrigger lookupTrigger, QString destinationType, QString destinationName) { + // Only accept these types of triggers. Other triggers are usually used internally in AddressManager. + QString trigger; + switch (lookupTrigger) { + case AddressManager::UserInput: + trigger = "UserInput"; + break; + case AddressManager::Back: + trigger = "Back"; + break; + case AddressManager::Forward: + trigger = "Forward"; + break; + case AddressManager::StartupFromSettings: + trigger = "StartupFromSettings"; + break; + default: + return; + } + + const QString ACTION_NAME = "went_to"; QJsonObject actionDetails; + const QString TRIGGER_TYPE_KEY = "trigger"; const QString DESTINATION_TYPE_KEY = "destination_type"; const QString DESTINATION_NAME_KEY = "detination_name"; + actionDetails.insert(TRIGGER_TYPE_KEY, trigger); actionDetails.insert(DESTINATION_TYPE_KEY, destinationType); actionDetails.insert(DESTINATION_NAME_KEY, destinationName); diff --git a/libraries/networking/src/UserActivityLogger.h b/libraries/networking/src/UserActivityLogger.h index c2ab93db2f..b41960a8ad 100644 --- a/libraries/networking/src/UserActivityLogger.h +++ b/libraries/networking/src/UserActivityLogger.h @@ -20,6 +20,7 @@ #include #include +#include "AddressManager.h" class UserActivityLogger : public QObject { Q_OBJECT @@ -42,7 +43,7 @@ public slots: void changedDomain(QString domainURL); void connectedDevice(QString typeOfDevice, QString deviceName); void loadedScript(QString scriptName); - void wentTo(QString destinationType, QString destinationName); + void wentTo(AddressManager::LookupTrigger trigger, QString destinationType, QString destinationName); private slots: void requestError(QNetworkReply& errorReply); diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp new file mode 100644 index 0000000000..8b22b8ff58 --- /dev/null +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -0,0 +1,31 @@ +// +// UserActivityLoggerScriptingInterface.h +// libraries/networking/src +// +// Created by Ryan Huffman on 6/06/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "UserActivityLoggerScriptingInterface.h" +#include "UserActivityLogger.h" + +void UserActivityLoggerScriptingInterface::enabledEdit() { + logAction("enabled_edit"); +} + +void UserActivityLoggerScriptingInterface::openedMarketplace() { + logAction("opened_marketplace"); +} + +void UserActivityLoggerScriptingInterface::toggledAway(bool isAway) { + logAction("toggled_away", { { "is_away", isAway } }); +} + +void UserActivityLoggerScriptingInterface::logAction(QString action, QJsonObject details) { + QMetaObject::invokeMethod(&UserActivityLogger::getInstance(), "logAction", + Q_ARG(QString, action), + Q_ARG(QJsonObject, details)); +} diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h new file mode 100644 index 0000000000..9d60d666e2 --- /dev/null +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -0,0 +1,31 @@ +// +// UserActivityLoggerScriptingInterface.h +// libraries/networking/src +// +// Created by Ryan Huffman on 6/06/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_UserActivityLoggerScriptingInterface_h +#define hifi_UserActivityLoggerScriptingInterface_h + +#include +#include + +#include + +class UserActivityLoggerScriptingInterface : public QObject, public Dependency { + Q_OBJECT +public: + Q_INVOKABLE void enabledEdit(); + Q_INVOKABLE void openedMarketplace(); + Q_INVOKABLE void toggledAway(bool isAway); + +private: + void logAction(QString action, QJsonObject details = {}); +}; + +#endif // hifi_UserActivityLoggerScriptingInterface_h diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 6ca50420f3..c74b10820d 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: - return VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS; + return VERSION_MODEL_ENTITIES_SUPPORT_STATIC_MESH; case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index ae54450fee..e484a06502 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -180,6 +180,7 @@ const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57; const PacketVersion VERSION_ENTITIES_NO_FLY_ZONES = 58; const PacketVersion VERSION_ENTITIES_MORE_SHAPES = 59; const PacketVersion VERSION_ENTITIES_PROPERLY_ENCODE_SHAPE_EDITS = 60; +const PacketVersion VERSION_MODEL_ENTITIES_SUPPORT_STATIC_MESH = 61; enum class AvatarMixerPacketVersion : PacketVersion { TranslationSupport = 17, diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 8f22c576f0..08d207fa72 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -135,7 +135,14 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { _nextOwnershipBid = 0; } if ((flags & Simulation::DIRTY_PHYSICS_ACTIVATION) && !_body->isActive()) { - _body->activate(); + if (_body->isKinematicObject()) { + // only force activate kinematic bodies (dynamic shouldn't need force and + // active static bodies are special (see PhysicsEngine::_activeStaticBodies)) + _body->activate(true); + _lastKinematicStep = ObjectMotionState::getWorldSimulationStep(); + } else { + _body->activate(); + } } } @@ -152,6 +159,11 @@ PhysicsMotionType EntityMotionState::computePhysicsMotionType() const { } assert(entityTreeIsLocked()); + if (_entity->getShapeType() == SHAPE_TYPE_STATIC_MESH + || (_body && _body->getCollisionShape()->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE)) { + return MOTION_TYPE_STATIC; + } + if (_entity->getDynamic()) { if (!_entity->getParentID().isNull()) { // if something would have been dynamic but is a child of something else, force it to be kinematic, instead. diff --git a/libraries/physics/src/ObjectMotionState.cpp b/libraries/physics/src/ObjectMotionState.cpp index de435e80da..f915121718 100644 --- a/libraries/physics/src/ObjectMotionState.cpp +++ b/libraries/physics/src/ObjectMotionState.cpp @@ -203,35 +203,37 @@ void ObjectMotionState::handleEasyChanges(uint32_t& flags) { } } - if (flags & Simulation::DIRTY_LINEAR_VELOCITY) { - btVector3 newLinearVelocity = glmToBullet(getObjectLinearVelocity()); - if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { - float delta = (newLinearVelocity - _body->getLinearVelocity()).length(); - if (delta > ACTIVATION_LINEAR_VELOCITY_DELTA) { - flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + if (_body->getCollisionShape()->getShapeType() != TRIANGLE_MESH_SHAPE_PROXYTYPE) { + if (flags & Simulation::DIRTY_LINEAR_VELOCITY) { + btVector3 newLinearVelocity = glmToBullet(getObjectLinearVelocity()); + if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { + float delta = (newLinearVelocity - _body->getLinearVelocity()).length(); + if (delta > ACTIVATION_LINEAR_VELOCITY_DELTA) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } } - } - _body->setLinearVelocity(newLinearVelocity); + _body->setLinearVelocity(newLinearVelocity); - btVector3 newGravity = glmToBullet(getObjectGravity()); - if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { - float delta = (newGravity - _body->getGravity()).length(); - if (delta > ACTIVATION_GRAVITY_DELTA || - (delta > 0.0f && _body->getGravity().length2() == 0.0f)) { - flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + btVector3 newGravity = glmToBullet(getObjectGravity()); + if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { + float delta = (newGravity - _body->getGravity()).length(); + if (delta > ACTIVATION_GRAVITY_DELTA || + (delta > 0.0f && _body->getGravity().length2() == 0.0f)) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } } + _body->setGravity(newGravity); } - _body->setGravity(newGravity); - } - if (flags & Simulation::DIRTY_ANGULAR_VELOCITY) { - btVector3 newAngularVelocity = glmToBullet(getObjectAngularVelocity()); - if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { - float delta = (newAngularVelocity - _body->getAngularVelocity()).length(); - if (delta > ACTIVATION_ANGULAR_VELOCITY_DELTA) { - flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + if (flags & Simulation::DIRTY_ANGULAR_VELOCITY) { + btVector3 newAngularVelocity = glmToBullet(getObjectAngularVelocity()); + if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { + float delta = (newAngularVelocity - _body->getAngularVelocity()).length(); + if (delta > ACTIVATION_ANGULAR_VELOCITY_DELTA) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } } + _body->setAngularVelocity(newAngularVelocity); } - _body->setAngularVelocity(newAngularVelocity); } if (flags & Simulation::DIRTY_MATERIAL) { diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 6806b3a398..cdf33a6edb 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -217,7 +217,7 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re } else if (entity->isReadyToComputeShape()) { ShapeInfo shapeInfo; entity->computeShapeInfo(shapeInfo); - int numPoints = shapeInfo.getMaxNumPoints(); + int numPoints = shapeInfo.getLargestSubshapePointCount(); if (numPoints > MAX_HULL_POINTS) { qWarning() << "convex hull with" << numPoints << "points for entity" << entity->getName() @@ -231,7 +231,7 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re result.push_back(motionState); entityItr = _entitiesToAddToPhysics.erase(entityItr); } else { - //qDebug() << "Warning! Failed to generate new shape for entity." << entity->getName(); + //qWarning() << "Failed to generate new shape for entity." << entity->getName(); ++entityItr; } } else { diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index d667d1075d..3afc170a8c 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -67,7 +67,8 @@ static const btVector3 _unitSphereDirections[NUM_UNIT_SPHERE_DIRECTIONS] = { }; -btConvexHullShape* ShapeFactory::createConvexHull(const QVector& points) { +// util method +btConvexHullShape* createConvexHull(const ShapeInfo::PointList& points) { assert(points.size() > 0); btConvexHullShape* hull = new btConvexHullShape(); @@ -158,6 +159,84 @@ btConvexHullShape* ShapeFactory::createConvexHull(const QVector& poin return hull; } +// util method +btTriangleIndexVertexArray* createStaticMeshArray(const ShapeInfo& info) { + assert(info.getType() == SHAPE_TYPE_STATIC_MESH); // should only get here for mesh shapes + + const ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); + assert(pointCollection.size() == 1); // should only have one mesh + + const ShapeInfo::PointList& pointList = pointCollection[0]; + assert(pointList.size() > 2); // should have at least one triangle's worth of points + + const ShapeInfo::TriangleIndices& triangleIndices = info.getTriangleIndices(); + assert(triangleIndices.size() > 2); // should have at least one triangle's worth of indices + + // allocate mesh buffers + btIndexedMesh mesh; + int32_t numIndices = triangleIndices.size(); + const int32_t VERTICES_PER_TRIANGLE = 3; + mesh.m_numTriangles = numIndices / VERTICES_PER_TRIANGLE; + if (numIndices < INT16_MAX) { + // small number of points so we can use 16-bit indices + mesh.m_triangleIndexBase = new unsigned char[sizeof(int16_t) * (size_t)numIndices]; + mesh.m_indexType = PHY_SHORT; + mesh.m_triangleIndexStride = VERTICES_PER_TRIANGLE * sizeof(int16_t); + } else { + mesh.m_triangleIndexBase = new unsigned char[sizeof(int32_t) * (size_t)numIndices]; + mesh.m_indexType = PHY_INTEGER; + mesh.m_triangleIndexStride = VERTICES_PER_TRIANGLE * sizeof(int32_t); + } + mesh.m_numVertices = pointList.size(); + mesh.m_vertexBase = new unsigned char[VERTICES_PER_TRIANGLE * sizeof(btScalar) * (size_t)mesh.m_numVertices]; + mesh.m_vertexStride = VERTICES_PER_TRIANGLE * sizeof(btScalar); + mesh.m_vertexType = PHY_FLOAT; + + // copy data into buffers + btScalar* vertexData = static_cast((void*)(mesh.m_vertexBase)); + for (int32_t i = 0; i < mesh.m_numVertices; ++i) { + int32_t j = i * VERTICES_PER_TRIANGLE; + const glm::vec3& point = pointList[i]; + vertexData[j] = point.x; + vertexData[j + 1] = point.y; + vertexData[j + 2] = point.z; + } + if (numIndices < INT16_MAX) { + int16_t* indices = static_cast((void*)(mesh.m_triangleIndexBase)); + for (int32_t i = 0; i < numIndices; ++i) { + indices[i] = triangleIndices[i]; + } + } else { + int32_t* indices = static_cast((void*)(mesh.m_triangleIndexBase)); + for (int32_t i = 0; i < numIndices; ++i) { + indices[i] = triangleIndices[i]; + } + } + + // store buffers in a new dataArray and return the pointer + // (external StaticMeshShape will own all of the data that was allocated here) + btTriangleIndexVertexArray* dataArray = new btTriangleIndexVertexArray; + dataArray->addIndexedMesh(mesh, mesh.m_indexType); + return dataArray; +} + +// util method +void deleteStaticMeshArray(btTriangleIndexVertexArray* dataArray) { + assert(dataArray); + IndexedMeshArray& meshes = dataArray->getIndexedMeshArray(); + for (int32_t i = 0; i < meshes.size(); ++i) { + btIndexedMesh mesh = meshes[i]; + mesh.m_numTriangles = 0; + delete [] mesh.m_triangleIndexBase; + mesh.m_triangleIndexBase = nullptr; + mesh.m_numVertices = 0; + delete [] mesh.m_vertexBase; + mesh.m_vertexBase = nullptr; + } + meshes.clear(); + delete dataArray; +} + btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { btCollisionShape* shape = NULL; int type = info.getType(); @@ -179,15 +258,15 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { } break; case SHAPE_TYPE_COMPOUND: { - const QVector>& points = info.getPoints(); + const ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); uint32_t numSubShapes = info.getNumSubShapes(); if (numSubShapes == 1) { - shape = createConvexHull(info.getPoints()[0]); + shape = createConvexHull(pointCollection[0]); } else { auto compound = new btCompoundShape(); btTransform trans; trans.setIdentity(); - foreach (QVector hullPoints, points) { + foreach (const ShapeInfo::PointList& hullPoints, pointCollection) { btConvexHullShape* hull = createConvexHull(hullPoints); compound->addChildShape (trans, hull); } @@ -195,6 +274,11 @@ btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) { } } break; + case SHAPE_TYPE_STATIC_MESH: { + btTriangleIndexVertexArray* dataArray = createStaticMeshArray(info); + shape = new StaticMeshShape(dataArray); + } + break; } if (shape) { if (glm::length2(info.getOffset()) > MIN_SHAPE_OFFSET * MIN_SHAPE_OFFSET) { @@ -228,3 +312,14 @@ void ShapeFactory::deleteShape(btCollisionShape* shape) { } delete shape; } + +// the dataArray must be created before we create the StaticMeshShape +ShapeFactory::StaticMeshShape::StaticMeshShape(btTriangleIndexVertexArray* dataArray) +: btBvhTriangleMeshShape(dataArray, true), _dataArray(dataArray) { + assert(dataArray); +} + +ShapeFactory::StaticMeshShape::~StaticMeshShape() { + deleteStaticMeshArray(_dataArray); + _dataArray = nullptr; +} diff --git a/libraries/physics/src/ShapeFactory.h b/libraries/physics/src/ShapeFactory.h index 1ba2bdb619..6202612eb9 100644 --- a/libraries/physics/src/ShapeFactory.h +++ b/libraries/physics/src/ShapeFactory.h @@ -20,9 +20,22 @@ // translates between ShapeInfo and btShape namespace ShapeFactory { - btConvexHullShape* createConvexHull(const QVector& points); btCollisionShape* createShapeFromInfo(const ShapeInfo& info); void deleteShape(btCollisionShape* shape); + + //btTriangleIndexVertexArray* createStaticMeshArray(const ShapeInfo& info); + //void deleteStaticMeshArray(btTriangleIndexVertexArray* dataArray); + + class StaticMeshShape : public btBvhTriangleMeshShape { + public: + StaticMeshShape() = delete; + StaticMeshShape(btTriangleIndexVertexArray* dataArray); + ~StaticMeshShape(); + + private: + // the StaticMeshShape owns its vertex/index data + btTriangleIndexVertexArray* _dataArray; + }; }; #endif // hifi_ShapeFactory_h diff --git a/libraries/physics/src/ShapeManager.cpp b/libraries/physics/src/ShapeManager.cpp index 4231d1eb60..4fa660239c 100644 --- a/libraries/physics/src/ShapeManager.cpp +++ b/libraries/physics/src/ShapeManager.cpp @@ -32,15 +32,13 @@ btCollisionShape* ShapeManager::getShape(const ShapeInfo& info) { if (info.getType() == SHAPE_TYPE_NONE) { return NULL; } - if (info.getType() != SHAPE_TYPE_COMPOUND) { - // Very small or large non-compound objects are not supported. - float diagonal = 4.0f * glm::length2(info.getHalfExtents()); - const float MIN_SHAPE_DIAGONAL_SQUARED = 3.0e-4f; // 1 cm cube - if (diagonal < MIN_SHAPE_DIAGONAL_SQUARED) { - // qCDebug(physics) << "ShapeManager::getShape -- not making shape due to size" << diagonal; - return NULL; - } + const float MIN_SHAPE_DIAGONAL_SQUARED = 3.0e-4f; // 1 cm cube + if (4.0f * glm::length2(info.getHalfExtents()) < MIN_SHAPE_DIAGONAL_SQUARED) { + // tiny shapes are not supported + // qCDebug(physics) << "ShapeManager::getShape -- not making shape due to size" << diagonal; + return NULL; } + DoubleHashKey key = info.getHash(); ShapeReference* shapeRef = _shapeMap.find(key); if (shapeRef) { @@ -66,8 +64,8 @@ bool ShapeManager::releaseShapeByKey(const DoubleHashKey& key) { shapeRef->refCount--; if (shapeRef->refCount == 0) { _pendingGarbage.push_back(key); - const int MAX_GARBAGE_CAPACITY = 255; - if (_pendingGarbage.size() > MAX_GARBAGE_CAPACITY) { + const int MAX_SHAPE_GARBAGE_CAPACITY = 255; + if (_pendingGarbage.size() > MAX_SHAPE_GARBAGE_CAPACITY) { collectGarbage(); } } diff --git a/libraries/plugins/src/plugins/DisplayPlugin.cpp b/libraries/plugins/src/plugins/DisplayPlugin.cpp index a217041f4e..747c72c08e 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.cpp +++ b/libraries/plugins/src/plugins/DisplayPlugin.cpp @@ -1,28 +1,6 @@ #include "DisplayPlugin.h" #include -#include - -#include "PluginContainer.h" - -bool DisplayPlugin::activate() { - if (isHmd() && (getHmdScreen() >= 0)) { - _container->showDisplayPluginsTools(); - } - return Parent::activate(); -} - -void DisplayPlugin::deactivate() { - _container->showDisplayPluginsTools(false); - if (!_container->currentDisplayActions().isEmpty()) { - auto menu = _container->getPrimaryMenu(); - foreach(auto itemInfo, _container->currentDisplayActions()) { - menu->removeMenuItem(itemInfo.first, itemInfo.second); - } - _container->currentDisplayActions().clear(); - } - Parent::deactivate(); -} int64_t DisplayPlugin::getPaintDelayUsecs() const { std::lock_guard lock(_paintDelayMutex); diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 19f803e11e..f0ba762ecb 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -16,16 +16,18 @@ #include #include #include -class QImage; #include #include +#include #include "Plugin.h" +class QImage; + enum Eye { - Left, - Right + Left = (int)bilateral::Side::Left, + Right = (int)bilateral::Side::Right }; /* @@ -56,7 +58,73 @@ namespace gpu { using TexturePointer = std::shared_ptr; } -class DisplayPlugin : public Plugin { +// Stereo display functionality +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// displayPlugin->isStereo returns true +class StereoDisplay { +public: + // Stereo specific methods + virtual glm::mat4 getEyeProjection(Eye eye, const glm::mat4& baseProjection) const { + return baseProjection; + } + + virtual glm::mat4 getCullingProjection(const glm::mat4& baseProjection) const { + return baseProjection; + } + + virtual float getIPD() const { return AVERAGE_HUMAN_IPD; } +}; + +// HMD display functionality +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// displayPlugin->isHmd returns true +class HmdDisplay : public StereoDisplay { +public: + // HMD specific methods + // TODO move these into another class? + virtual glm::mat4 getEyeToHeadTransform(Eye eye) const { + static const glm::mat4 transform; return transform; + } + + // returns a copy of the most recent head pose, computed via updateHeadPose + virtual glm::mat4 getHeadPose() const { + return glm::mat4(); + } + + // Needed for timewarp style features + virtual void setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm::mat4& pose) { + // NOOP + } + + virtual void abandonCalibration() {} + + virtual void resetSensors() {} + + enum Hand { + LeftHand = 0x01, + RightHand = 0x02, + }; + + enum class HandLaserMode { + None, // Render no hand lasers + Overlay, // Render hand lasers only if they intersect with the UI layer, and stop at the UI layer + }; + + virtual bool setHandLaser( + uint32_t hands, // Bits from the Hand enum + HandLaserMode mode, // Mode in which to render + const vec4& color = vec4(1), // The color of the rendered laser + const vec3& direction = vec3(0, 0, -1) // The direction in which to render the hand lasers + ) { + return false; + } + + virtual bool suppressKeyboard() { return false; } + virtual void unsuppressKeyboard() {}; + virtual bool isKeyboardVisible() { return false; } +}; + +class DisplayPlugin : public Plugin, public HmdDisplay { Q_OBJECT using Parent = Plugin; public: @@ -64,8 +132,6 @@ public: Present = QEvent::User + 1 }; - bool activate() override; - void deactivate() override; virtual bool isHmd() const { return false; } virtual int getHmdScreen() const { return -1; } /// By default, all HMDs are stereo @@ -117,42 +183,12 @@ public: return QRect(0, 0, recommendedSize.x, recommendedSize.y); } - // Stereo specific methods - virtual glm::mat4 getEyeProjection(Eye eye, const glm::mat4& baseProjection) const { - return baseProjection; - } - - virtual glm::mat4 getCullingProjection(const glm::mat4& baseProjection) const { - return baseProjection; - } - - // Fetch the most recently displayed image as a QImage virtual QImage getScreenshot() const = 0; - // HMD specific methods - // TODO move these into another class? - virtual glm::mat4 getEyeToHeadTransform(Eye eye) const { - static const glm::mat4 transform; return transform; - } - // will query the underlying hmd api to compute the most recent head pose virtual bool beginFrameRender(uint32_t frameIndex) { return true; } - // returns a copy of the most recent head pose, computed via updateHeadPose - virtual glm::mat4 getHeadPose() const { - return glm::mat4(); - } - - // Needed for timewarp style features - virtual void setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm::mat4& pose) { - // NOOP - } - - virtual float getIPD() const { return AVERAGE_HUMAN_IPD; } - - virtual void abandonCalibration() {} - virtual void resetSensors() {} virtual float devicePixelRatio() { return 1.0f; } // Rate at which we present to the display device virtual float presentRate() const { return -1.0f; } @@ -160,6 +196,7 @@ public: virtual float newFramePresentRate() const { return -1.0f; } // Rate at which rendered frames are being skipped virtual float droppedFrameRate() const { return -1.0f; } + uint32_t presentCount() const { return _presentedFrameIndex; } // Time since last call to incrementPresentCount (only valid if DEBUG_PAINT_DELAY is defined) int64_t getPaintDelayUsecs() const; @@ -168,6 +205,7 @@ public: static const QString& MENU_PATH(); + signals: void recommendedFramebufferSizeChanged(const QSize & size); diff --git a/libraries/plugins/src/plugins/Plugin.h b/libraries/plugins/src/plugins/Plugin.h index fb5bf0ba55..0452c7fbfe 100644 --- a/libraries/plugins/src/plugins/Plugin.h +++ b/libraries/plugins/src/plugins/Plugin.h @@ -15,6 +15,7 @@ #include "Forward.h" class Plugin : public QObject { + Q_OBJECT public: /// \return human-readable name virtual const QString& getName() const = 0; @@ -63,6 +64,13 @@ public: virtual void saveSettings() const {} virtual void loadSettings() {} +signals: + // These signals should be emitted when a device is first known to be available. In some cases this will + // be in `init()`, in other cases, like Neuron, this isn't known until activation. + // SDL2 isn't a device itself, but can have 0+ subdevices. subdeviceConnected is used in this case. + void deviceConnected(QString pluginName) const; + void subdeviceConnected(QString pluginName, QString subdeviceName) const; + protected: bool _active { false }; PluginContainer* _container { nullptr }; diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 7161132c5e..29658eeb6b 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -14,10 +14,12 @@ #include #include +#include +#include + #include "RuntimePlugin.h" #include "DisplayPlugin.h" #include "InputPlugin.h" -#include "PluginContainer.h" PluginManager* PluginManager::getInstance() { @@ -120,6 +122,15 @@ static DisplayPluginList displayPlugins; const DisplayPluginList& PluginManager::getDisplayPlugins() { static std::once_flag once; + static auto deviceAddedCallback = [](QString deviceName) { + qDebug() << "Added device: " << deviceName; + UserActivityLogger::getInstance().connectedDevice("display", deviceName); + }; + static auto subdeviceAddedCallback = [](QString pluginName, QString deviceName) { + qDebug() << "Added subdevice: " << deviceName; + UserActivityLogger::getInstance().connectedDevice("display", pluginName + " | " + deviceName); + }; + std::call_once(once, [&] { // Grab the built in plugins displayPlugins = ::getDisplayPlugins(); @@ -133,9 +144,10 @@ const DisplayPluginList& PluginManager::getDisplayPlugins() { } } } - auto& container = PluginContainer::getInstance(); for (auto plugin : displayPlugins) { - plugin->setContainer(&container); + connect(plugin.get(), &Plugin::deviceConnected, this, deviceAddedCallback, Qt::QueuedConnection); + connect(plugin.get(), &Plugin::subdeviceConnected, this, subdeviceAddedCallback, Qt::QueuedConnection); + plugin->setContainer(_container); plugin->init(); } @@ -156,6 +168,15 @@ void PluginManager::disableDisplayPlugin(const QString& name) { const InputPluginList& PluginManager::getInputPlugins() { static InputPluginList inputPlugins; static std::once_flag once; + static auto deviceAddedCallback = [](QString deviceName) { + qDebug() << "Added device: " << deviceName; + UserActivityLogger::getInstance().connectedDevice("input", deviceName); + }; + static auto subdeviceAddedCallback = [](QString pluginName, QString deviceName) { + qDebug() << "Added subdevice: " << deviceName; + UserActivityLogger::getInstance().connectedDevice("input", pluginName + " | " + deviceName); + }; + std::call_once(once, [&] { inputPlugins = ::getInputPlugins(); @@ -171,9 +192,10 @@ const InputPluginList& PluginManager::getInputPlugins() { } } - auto& container = PluginContainer::getInstance(); for (auto plugin : inputPlugins) { - plugin->setContainer(&container); + connect(plugin.get(), &Plugin::deviceConnected, this, deviceAddedCallback, Qt::QueuedConnection); + connect(plugin.get(), &Plugin::subdeviceConnected, this, subdeviceAddedCallback, Qt::QueuedConnection); + plugin->setContainer(_container); plugin->init(); } }); diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h index 2a94e6490b..7903bdd724 100644 --- a/libraries/plugins/src/plugins/PluginManager.h +++ b/libraries/plugins/src/plugins/PluginManager.h @@ -26,4 +26,7 @@ public: void disableDisplays(const QStringList& displays); void disableInputs(const QStringList& inputs); void saveSettings(); + void setContainer(PluginContainer* container) { _container = container; } +private: + PluginContainer* _container { nullptr }; }; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index ded1184c24..0470a238fc 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1002,7 +1002,7 @@ void Model::scaleToFit() { Extents modelMeshExtents = getUnscaledMeshExtents(); // size is our "target size in world space" - // we need to set our model scale so that the extents of the mesh, fit in a cube that size... + // we need to set our model scale so that the extents of the mesh, fit in a box that size... glm::vec3 meshDimensions = modelMeshExtents.maximum - modelMeshExtents.minimum; glm::vec3 rescaleDimensions = _scaleToFitDimensions / meshDimensions; setScaleInternal(rescaleDimensions); diff --git a/libraries/shared/src/GPUIdent.cpp b/libraries/shared/src/GPUIdent.cpp index 19838964a4..02f92d87e7 100644 --- a/libraries/shared/src/GPUIdent.cpp +++ b/libraries/shared/src/GPUIdent.cpp @@ -122,7 +122,7 @@ GPUIdent* GPUIdent::ensureQuery(const QString& vendor, const QString& renderer) } if (count > bestCount) { bestCount = count; - _name = sString; + _name = QString(sString).trimmed(); hr = spInstance->Get(CComBSTR(_T("DriverVersion")), 0, &var, 0, 0); if (hr == S_OK) { diff --git a/libraries/shared/src/NumericalConstants.h b/libraries/shared/src/NumericalConstants.h index ca18d8ad5e..d37e1e31c5 100644 --- a/libraries/shared/src/NumericalConstants.h +++ b/libraries/shared/src/NumericalConstants.h @@ -39,6 +39,9 @@ const quint64 NSECS_PER_MSEC = 1000000; const quint64 USECS_PER_MSEC = 1000; const quint64 MSECS_PER_SECOND = 1000; const quint64 USECS_PER_SECOND = USECS_PER_MSEC * MSECS_PER_SECOND; +const quint64 SECS_PER_MINUTE = 60; +const quint64 MINS_PER_HOUR = 60; +const quint64 SECS_PER_HOUR = SECS_PER_MINUTE * MINS_PER_HOUR; const int BITS_IN_BYTE = 8; const int BYTES_PER_KILOBYTE = 1000; diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index 9c1e5c3816..e0f4cc18b2 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -16,19 +16,23 @@ #include "NumericalConstants.h" // for MILLIMETERS_PER_METER void ShapeInfo::clear() { - _type = SHAPE_TYPE_NONE; - _halfExtents = _offset = glm::vec3(0.0f); + _url.clear(); + _pointCollection.clear(); + _triangleIndices.clear(); + _halfExtents = glm::vec3(0.0f); + _offset = glm::vec3(0.0f); _doubleHashKey.clear(); + _type = SHAPE_TYPE_NONE; } void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString url) { _type = type; + _halfExtents = halfExtents; switch(type) { case SHAPE_TYPE_NONE: _halfExtents = glm::vec3(0.0f); break; case SHAPE_TYPE_BOX: - _halfExtents = halfExtents; break; case SHAPE_TYPE_SPHERE: { // sphere radius is max of halfExtents @@ -37,11 +41,10 @@ void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString break; } case SHAPE_TYPE_COMPOUND: + case SHAPE_TYPE_STATIC_MESH: _url = QUrl(url); - _halfExtents = halfExtents; break; default: - _halfExtents = halfExtents; break; } _doubleHashKey.clear(); @@ -61,9 +64,9 @@ void ShapeInfo::setSphere(float radius) { _doubleHashKey.clear(); } -void ShapeInfo::setConvexHulls(const QVector>& points) { - _points = points; - _type = (_points.size() > 0) ? SHAPE_TYPE_COMPOUND : SHAPE_TYPE_NONE; +void ShapeInfo::setPointCollection(const ShapeInfo::PointCollection& pointCollection) { + _pointCollection = pointCollection; + _type = (_pointCollection.size() > 0) ? SHAPE_TYPE_COMPOUND : SHAPE_TYPE_NONE; _doubleHashKey.clear(); } @@ -83,15 +86,15 @@ uint32_t ShapeInfo::getNumSubShapes() const { if (_type == SHAPE_TYPE_NONE) { return 0; } else if (_type == SHAPE_TYPE_COMPOUND) { - return _points.size(); + return _pointCollection.size(); } return 1; } -int ShapeInfo::getMaxNumPoints() const { +int ShapeInfo::getLargestSubshapePointCount() const { int numPoints = 0; - for (int i = 0; i < _points.size(); ++i) { - int n = _points[i].size(); + for (int i = 0; i < _pointCollection.size(); ++i) { + int n = _pointCollection[i].size(); if (n > numPoints) { numPoints = n; } @@ -178,34 +181,31 @@ const DoubleHashKey& ShapeInfo::getHash() const { // NOTE: we cache the key so we only ever need to compute it once for any valid ShapeInfo instance. if (_doubleHashKey.isNull() && _type != SHAPE_TYPE_NONE) { bool useOffset = glm::length2(_offset) > MIN_SHAPE_OFFSET * MIN_SHAPE_OFFSET; - // The key is not yet cached therefore we must compute it! To this end we bypass the const-ness - // of this method by grabbing a non-const pointer to "this" and a non-const reference to _doubleHashKey. - ShapeInfo* thisPtr = const_cast(this); - DoubleHashKey& key = thisPtr->_doubleHashKey; + // The key is not yet cached therefore we must compute it. // compute hash1 // TODO?: provide lookup table for hash/hash2 of _type rather than recompute? uint32_t primeIndex = 0; - key.computeHash((uint32_t)_type, primeIndex++); - - // compute hash1 - uint32_t hash = key.getHash(); + _doubleHashKey.computeHash((uint32_t)_type, primeIndex++); + + // compute hash1 + uint32_t hash = _doubleHashKey.getHash(); for (int j = 0; j < 3; ++j) { // NOTE: 0.49f is used to bump the float up almost half a millimeter // so the cast to int produces a round() effect rather than a floor() hash ^= DoubleHashKey::hashFunction( - (uint32_t)(_halfExtents[j] * MILLIMETERS_PER_METER + copysignf(1.0f, _halfExtents[j]) * 0.49f), + (uint32_t)(_halfExtents[j] * MILLIMETERS_PER_METER + copysignf(1.0f, _halfExtents[j]) * 0.49f), primeIndex++); if (useOffset) { hash ^= DoubleHashKey::hashFunction( - (uint32_t)(_offset[j] * MILLIMETERS_PER_METER + copysignf(1.0f, _offset[j]) * 0.49f), + (uint32_t)(_offset[j] * MILLIMETERS_PER_METER + copysignf(1.0f, _offset[j]) * 0.49f), primeIndex++); } } - key.setHash(hash); - + _doubleHashKey.setHash(hash); + // compute hash2 - hash = key.getHash2(); + hash = _doubleHashKey.getHash2(); for (int j = 0; j < 3; ++j) { // NOTE: 0.49f is used to bump the float up almost half a millimeter // so the cast to int produces a round() effect rather than a floor() @@ -222,16 +222,18 @@ const DoubleHashKey& ShapeInfo::getHash() const { hash += ~(floatHash << 10); hash = (hash << 16) | (hash >> 16); } - key.setHash2(hash); + _doubleHashKey.setHash2(hash); - QString url = _url.toString(); - if (!url.isEmpty()) { - // fold the urlHash into both parts - QByteArray baUrl = url.toLocal8Bit(); - const char *cUrl = baUrl.data(); - uint32_t urlHash = qChecksum(cUrl, baUrl.count()); - key.setHash(key.getHash() ^ urlHash); - key.setHash2(key.getHash2() ^ urlHash); + if (_type == SHAPE_TYPE_COMPOUND || _type == SHAPE_TYPE_STATIC_MESH) { + QString url = _url.toString(); + if (!url.isEmpty()) { + // fold the urlHash into both parts + QByteArray baUrl = url.toLocal8Bit(); + const char *cUrl = baUrl.data(); + uint32_t urlHash = qChecksum(cUrl, baUrl.count()); + _doubleHashKey.setHash(_doubleHashKey.getHash() ^ urlHash); + _doubleHashKey.setHash2(_doubleHashKey.getHash2() ^ urlHash); + } } } return _doubleHashKey; diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index c853666d90..96132a4b23 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -30,26 +30,32 @@ enum ShapeType { SHAPE_TYPE_NONE, SHAPE_TYPE_BOX, SHAPE_TYPE_SPHERE, - SHAPE_TYPE_PLANE, - SHAPE_TYPE_COMPOUND, SHAPE_TYPE_CAPSULE_X, SHAPE_TYPE_CAPSULE_Y, SHAPE_TYPE_CAPSULE_Z, SHAPE_TYPE_CYLINDER_X, SHAPE_TYPE_CYLINDER_Y, SHAPE_TYPE_CYLINDER_Z, + SHAPE_TYPE_HULL, + SHAPE_TYPE_PLANE, + SHAPE_TYPE_COMPOUND, SHAPE_TYPE_STATIC_MESH }; class ShapeInfo { public: + + using PointList = QVector; + using PointCollection = QVector; + using TriangleIndices = QVector; + void clear(); void setParams(ShapeType type, const glm::vec3& halfExtents, QString url=""); void setBox(const glm::vec3& halfExtents); void setSphere(float radius); - void setConvexHulls(const QVector>& points); + void setPointCollection(const PointCollection& pointCollection); void setCapsuleY(float radius, float halfHeight); void setOffset(const glm::vec3& offset); @@ -57,13 +63,15 @@ public: const glm::vec3& getHalfExtents() const { return _halfExtents; } const glm::vec3& getOffset() const { return _offset; } - - QVector>& getPoints() { return _points; } - const QVector>& getPoints() const { return _points; } uint32_t getNumSubShapes() const; - void appendToPoints (const QVector& newPoints) { _points << newPoints; } - int getMaxNumPoints() const; + PointCollection& getPointCollection() { return _pointCollection; } + const PointCollection& getPointCollection() const { return _pointCollection; } + + TriangleIndices& getTriangleIndices() { return _triangleIndices; } + const TriangleIndices& getTriangleIndices() const { return _triangleIndices; } + + int getLargestSubshapePointCount() const; float computeVolume() const; @@ -74,12 +82,13 @@ public: const DoubleHashKey& getHash() const; protected: - ShapeType _type = SHAPE_TYPE_NONE; + QUrl _url; // url for model of convex collision hulls + PointCollection _pointCollection; + TriangleIndices _triangleIndices; glm::vec3 _halfExtents = glm::vec3(0.0f); glm::vec3 _offset = glm::vec3(0.0f); - DoubleHashKey _doubleHashKey; - QVector> _points; // points for convex collision hulls - QUrl _url; // url for model of convex collision hulls + mutable DoubleHashKey _doubleHashKey; + ShapeType _type = SHAPE_TYPE_NONE; }; #endif // hifi_ShapeInfo_h diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index b80fac637c..edb6fe437d 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -28,6 +28,7 @@ #ifdef Q_OS_WIN #include "CPUIdent.h" +#include #endif @@ -843,3 +844,29 @@ void printSystemInformation() { (envVariables.contains(env) ? " = " + envVariables.value(env) : " NOT FOUND"); } } + +bool getMemoryInfo(MemoryInfo& info) { +#ifdef Q_OS_WIN + MEMORYSTATUSEX ms; + ms.dwLength = sizeof(ms); + if (!GlobalMemoryStatusEx(&ms)) { + return false; + } + + info.totalMemoryBytes = ms.ullTotalPhys; + info.availMemoryBytes = ms.ullAvailPhys; + info.usedMemoryBytes = ms.ullTotalPhys - ms.ullAvailPhys; + + + PROCESS_MEMORY_COUNTERS_EX pmc; + if (!GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&pmc), sizeof(pmc))) { + return false; + } + info.processUsedMemoryBytes = pmc.PrivateUsage; + info.processPeakUsedMemoryBytes = pmc.PeakPagefileUsage; + + return true; +#endif + + return false; +} \ No newline at end of file diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index 042396f474..f3e5625484 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -204,4 +204,14 @@ void disableQtBearerPoll(); void printSystemInformation(); +struct MemoryInfo { + uint64_t totalMemoryBytes; + uint64_t availMemoryBytes; + uint64_t usedMemoryBytes; + uint64_t processUsedMemoryBytes; + uint64_t processPeakUsedMemoryBytes; +}; + +bool getMemoryInfo(MemoryInfo& info); + #endif // hifi_SharedUtil_h diff --git a/libraries/shared/src/shared/Bilateral.h b/libraries/shared/src/shared/Bilateral.h new file mode 100644 index 0000000000..c4daf60177 --- /dev/null +++ b/libraries/shared/src/shared/Bilateral.h @@ -0,0 +1,49 @@ +// +// Created by Bradley Austin Davis 2015/10/09 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +namespace bilateral { + enum class Side { + Left = 0, + Right = 1 + }; + + using Indices = Side; + + enum class Bits { + Left = 0x01, + Right = 0x02 + }; + + inline uint8_t bit(Side side) { + switch (side) { + case Side::Left: + return 0x01; + case Side::Right: + return 0x02; + } + return UINT8_MAX; + } + + inline uint8_t index(Side side) { + switch (side) { + case Side::Left: + return 0; + case Side::Right: + return 1; + } + return UINT8_MAX; + } + + template + void for_each_side(F f) { + f(Side::Left); + f(Side::Right); + } +} diff --git a/libraries/ui-plugins/CMakeLists.txt b/libraries/ui-plugins/CMakeLists.txt new file mode 100644 index 0000000000..9ce189b117 --- /dev/null +++ b/libraries/ui-plugins/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME ui-plugins) +setup_hifi_library(OpenGL) +link_hifi_libraries(shared plugins ui) diff --git a/libraries/plugins/src/plugins/PluginContainer.cpp b/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp similarity index 100% rename from libraries/plugins/src/plugins/PluginContainer.cpp rename to libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp diff --git a/libraries/plugins/src/plugins/PluginContainer.h b/libraries/ui-plugins/src/ui-plugins/PluginContainer.h similarity index 94% rename from libraries/plugins/src/plugins/PluginContainer.h rename to libraries/ui-plugins/src/ui-plugins/PluginContainer.h index e1d1a212e2..74ac834057 100644 --- a/libraries/plugins/src/plugins/PluginContainer.h +++ b/libraries/ui-plugins/src/ui-plugins/PluginContainer.h @@ -16,7 +16,7 @@ #include #include -#include "Forward.h" +#include class QAction; class GLWidget; @@ -63,8 +63,8 @@ public: virtual GLWidget* getPrimaryWidget() = 0; virtual MainWindow* getPrimaryWindow() = 0; virtual QOpenGLContext* getPrimaryContext() = 0; - virtual bool isForeground() = 0; - virtual const DisplayPluginPointer getActiveDisplayPlugin() const = 0; + virtual bool isForeground() const = 0; + virtual DisplayPluginPointer getActiveDisplayPlugin() const = 0; /// settings interface bool getBoolSetting(const QString& settingName, bool defaultValue); @@ -84,3 +84,4 @@ protected: std::map _exclusiveGroups; QRect _savedGeometry { 10, 120, 800, 600 }; }; + diff --git a/libraries/ui/src/ErrorDialog.cpp b/libraries/ui/src/ErrorDialog.cpp index ab36ef8d36..fcd73b4cc0 100644 --- a/libraries/ui/src/ErrorDialog.cpp +++ b/libraries/ui/src/ErrorDialog.cpp @@ -22,10 +22,6 @@ QString ErrorDialog::text() const { return _text; } -void ErrorDialog::setVisible(bool v) { - OffscreenQmlDialog::setVisible(v); -} - void ErrorDialog::setText(const QString& arg) { if (arg != _text) { _text = arg; diff --git a/libraries/ui/src/ErrorDialog.h b/libraries/ui/src/ErrorDialog.h index 665090da1a..38954714a7 100644 --- a/libraries/ui/src/ErrorDialog.h +++ b/libraries/ui/src/ErrorDialog.h @@ -30,7 +30,6 @@ public: QString text() const; public slots: - virtual void setVisible(bool v); void setText(const QString& arg); signals: diff --git a/libraries/ui/src/OffscreenQmlDialog.cpp b/libraries/ui/src/OffscreenQmlDialog.cpp index 43514c4761..2d1ca20876 100644 --- a/libraries/ui/src/OffscreenQmlDialog.cpp +++ b/libraries/ui/src/OffscreenQmlDialog.cpp @@ -17,7 +17,7 @@ OffscreenQmlDialog::~OffscreenQmlDialog() { } void OffscreenQmlDialog::hide() { - static_cast(parent())->setVisible(false); + parent()->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, false); } QString OffscreenQmlDialog::title() const { diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index dfd9056703..1a7d4b2328 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -121,32 +121,28 @@ void OffscreenUi::show(const QUrl& url, const QString& name, std::functionfindChild(name); } + if (item) { - item->setVisible(true); + QQmlProperty(item, OFFSCREEN_VISIBILITY_PROPERTY).write(true); } } void OffscreenUi::toggle(const QUrl& url, const QString& name, std::function f) { QQuickItem* item = getRootItem()->findChild(name); - // Already loaded? - if (item) { - emit showDesktop(); - item->setVisible(!item->isVisible()); + if (!item) { + show(url, name, f); return; } - load(url, f); - item = getRootItem()->findChild(name); - if (item && !item->isVisible()) { - emit showDesktop(); - item->setVisible(true); - } + // Already loaded, so just flip the bit + QQmlProperty shownProperty(item, OFFSCREEN_VISIBILITY_PROPERTY); + shownProperty.write(!shownProperty.read().toBool()); } void OffscreenUi::hide(const QString& name) { QQuickItem* item = getRootItem()->findChild(name); if (item) { - item->setVisible(false); + QQmlProperty(item, OFFSCREEN_VISIBILITY_PROPERTY).write(false); } } @@ -345,6 +341,20 @@ QVariant OffscreenUi::inputDialog(const Icon icon, const QString& title, const Q return waitForInputDialogResult(createInputDialog(icon, title, label, current)); } +void OffscreenUi::togglePinned() { + bool invokeResult = QMetaObject::invokeMethod(_desktop, "togglePinned"); + if (!invokeResult) { + qWarning() << "Failed to toggle window visibility"; + } +} + +void OffscreenUi::setPinned(bool pinned) { + bool invokeResult = QMetaObject::invokeMethod(_desktop, "setPinned", Q_ARG(QVariant, pinned)); + if (!invokeResult) { + qWarning() << "Failed to set window visibility"; + } +} + void OffscreenUi::addMenuInitializer(std::function f) { if (!_vrMenu) { _queuedMenuInitializers.push_back(f); @@ -482,10 +492,9 @@ void OffscreenUi::unfocusWindows() { Q_ASSERT(invokeResult); } -void OffscreenUi::toggleMenu(const QPoint& screenPosition) { +void OffscreenUi::toggleMenu(const QPoint& screenPosition) { // caller should already have mapped using getReticlePosition emit showDesktop(); // we really only want to do this if you're showing the menu, but for now this works - auto virtualPos = mapToVirtualScreen(screenPosition, nullptr); - QMetaObject::invokeMethod(_desktop, "toggleMenu", Q_ARG(QVariant, virtualPos)); + QMetaObject::invokeMethod(_desktop, "toggleMenu", Q_ARG(QVariant, screenPosition)); } diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 5a16b49491..e1d552c978 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -28,6 +28,8 @@ class VrMenu; +#define OFFSCREEN_VISIBILITY_PROPERTY "shown" + class OffscreenUi : public OffscreenQmlSurface, public Dependency { Q_OBJECT @@ -44,6 +46,13 @@ public: void setNavigationFocused(bool focused); void unfocusWindows(); void toggleMenu(const QPoint& screenCoordinates); + + + // Setting pinned to true will hide all overlay elements on the desktop that don't have a pinned flag + void setPinned(bool pinned = true); + + void togglePinned(); + bool eventFilter(QObject* originalDestination, QEvent* event) override; void addMenuInitializer(std::function f); diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index 396d716cda..c0eba4abf3 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -118,7 +118,7 @@ void QmlWindowClass::initQml(QVariantMap properties) { } bool visible = !properties.contains(VISIBILE_PROPERTY) || properties[VISIBILE_PROPERTY].toBool(); - object->setProperty(VISIBILE_PROPERTY, visible); + object->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, visible); object->setProperty(SOURCE_PROPERTY, _source); // Forward messages received from QML on to the script @@ -163,8 +163,7 @@ void QmlWindowClass::setVisible(bool visible) { QMetaObject::invokeMethod(targetWindow, "showTabForUrl", Qt::QueuedConnection, Q_ARG(QVariant, _source), Q_ARG(QVariant, visible)); } else { DependencyManager::get()->executeOnUiThread([=] { - targetWindow->setVisible(visible); - //emit visibilityChanged(visible); + targetWindow->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, visible); }); } } diff --git a/libraries/ui/src/Tooltip.cpp b/libraries/ui/src/Tooltip.cpp index 3c0902b378..94e04f34b6 100644 --- a/libraries/ui/src/Tooltip.cpp +++ b/libraries/ui/src/Tooltip.cpp @@ -47,10 +47,6 @@ void Tooltip::setImageURL(const QString& imageURL) { } } -void Tooltip::setVisible(bool visible) { - QQuickItem::setVisible(visible); -} - QString Tooltip::showTip(const QString& title, const QString& description) { const QString newTipId = QUuid().createUuid().toString(); diff --git a/libraries/ui/src/Tooltip.h b/libraries/ui/src/Tooltip.h index d1c7330a74..5e884a7aea 100644 --- a/libraries/ui/src/Tooltip.h +++ b/libraries/ui/src/Tooltip.h @@ -39,8 +39,6 @@ public: static void closeTip(const QString& tipId); public slots: - virtual void setVisible(bool v); - void setTitle(const QString& title); void setDescription(const QString& description); void setImageURL(const QString& imageURL); diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index 0a4bc7f8d2..e41472a8c5 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -387,6 +387,8 @@ bool NeuronPlugin::activate() { } else { qCDebug(inputplugins) << "NeuronPlugin: success connecting to " << _serverAddress.c_str() << ":" << _serverPort; + emit deviceConnected(getName()); + BRRegisterAutoSyncParmeter(_socketRef, Cmd_CombinationMode); return true; } diff --git a/plugins/hifiSdl2/src/SDL2Manager.cpp b/plugins/hifiSdl2/src/SDL2Manager.cpp index 09e783864c..b9a19658e2 100644 --- a/plugins/hifiSdl2/src/SDL2Manager.cpp +++ b/plugins/hifiSdl2/src/SDL2Manager.cpp @@ -66,6 +66,7 @@ void SDL2Manager::init() { auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(joystick); emit joystickAdded(joystick.get()); + emit subdeviceConnected(getName(), SDL_GameControllerName(controller)); } } } @@ -157,6 +158,7 @@ void SDL2Manager::pluginUpdate(float deltaTime, const controller::InputCalibrati _openJoysticks[id] = joystick; userInputMapper->registerDevice(joystick); emit joystickAdded(joystick.get()); + emit subdeviceConnected(getName(), SDL_GameControllerName(controller)); } } else if (event.type == SDL_CONTROLLERDEVICEREMOVED) { if (_openJoysticks.contains(event.cdevice.which)) { diff --git a/plugins/hifiSixense/CMakeLists.txt b/plugins/hifiSixense/CMakeLists.txt index 589b5b8964..f907d7865f 100644 --- a/plugins/hifiSixense/CMakeLists.txt +++ b/plugins/hifiSixense/CMakeLists.txt @@ -8,5 +8,5 @@ set(TARGET_NAME hifiSixense) setup_hifi_plugin(Script Qml Widgets) -link_hifi_libraries(shared controllers ui plugins input-plugins) +link_hifi_libraries(shared controllers ui plugins ui-plugins input-plugins) target_sixense() diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index 9ea79a8b96..baf13f1fae 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -28,7 +28,7 @@ #include #include #include -#include +#include #include #include @@ -66,14 +66,8 @@ const QString SHOW_DEBUG_RAW = "Debug Draw Raw Data"; const QString SHOW_DEBUG_CALIBRATED = "Debug Draw Calibrated Data"; bool SixenseManager::isSupported() const { -#ifdef HAVE_SIXENSE - -#if defined(Q_OS_OSX) - return QSysInfo::macVersion() <= QSysInfo::MV_MAVERICKS; -#else +#if defined(HAVE_SIXENSE) && !defined(Q_OS_OSX) return true; -#endif - #else return false; #endif @@ -83,6 +77,7 @@ bool SixenseManager::activate() { InputPlugin::activate(); #ifdef HAVE_SIXENSE + #if !defined(Q_OS_LINUX) _container->addMenu(MENU_PATH); _container->addMenuItem(PluginType::INPUT_PLUGIN, MENU_PATH, TOGGLE_SMOOTH, [this] (bool clicked) { setSixenseFilter(clicked); }, @@ -95,6 +90,7 @@ bool SixenseManager::activate() { _container->addMenuItem(PluginType::INPUT_PLUGIN, MENU_PATH, SHOW_DEBUG_CALIBRATED, [this] (bool clicked) { _inputDevice->setDebugDrawCalibrated(clicked); }, true, false); + #endif auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(_inputDevice); @@ -112,8 +108,10 @@ void SixenseManager::deactivate() { InputPlugin::deactivate(); #ifdef HAVE_SIXENSE + #if !defined(Q_OS_LINUX) _container->removeMenuItem(MENU_NAME, TOGGLE_SMOOTH); _container->removeMenu(MENU_PATH); + #endif _inputDevice->_poseStateMap.clear(); @@ -137,6 +135,13 @@ void SixenseManager::setSixenseFilter(bool filter) { void SixenseManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { BAIL_IF_NOT_LOADED +#ifdef HAVE_SIXENSE + static bool sixenseHasBeenConnected { false }; + if (!sixenseHasBeenConnected && sixenseIsBaseConnected(0)) { + sixenseHasBeenConnected = true; + emit deviceConnected(getName()); + } + auto userInputMapper = DependencyManager::get(); userInputMapper->withLock([&, this]() { _inputDevice->update(deltaTime, inputCalibrationData); @@ -146,6 +151,7 @@ void SixenseManager::pluginUpdate(float deltaTime, const controller::InputCalibr _container->requestReset(); _inputDevice->_requestReset = false; } +#endif } void SixenseManager::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { diff --git a/plugins/hifiSixense/src/SixenseSupportOSX.cpp b/plugins/hifiSixense/src/SixenseSupportOSX.cpp deleted file mode 100644 index fce2ea023b..0000000000 --- a/plugins/hifiSixense/src/SixenseSupportOSX.cpp +++ /dev/null @@ -1,155 +0,0 @@ -// -// SixenseSupportOSX.cpp -// libraries/input-plugins/src/input-plugins -// -// Created by Clement on 10/20/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -// Mock implementation of sixense.h to hide dynamic linking on OS X -#if defined(__APPLE__) && defined(HAVE_SIXENSE) -#include - -#include - -#include -#include -#include - -#ifndef SIXENSE_LIB_FILENAME -#define SIXENSE_LIB_FILENAME QCoreApplication::applicationDirPath() + "/../Frameworks/libsixense_x64" -#endif - -using Library = std::unique_ptr; -static Library SIXENSE; - -struct Callable { - template - int operator() (Args&&... args){ - return reinterpret_cast(function)(std::forward(args)...); - } - QFunctionPointer function; -}; - -Callable resolve(const Library& library, const char* name) { - Q_ASSERT_X(library && library->isLoaded(), __FUNCTION__, "Sixense library not loaded"); - auto function = library->resolve(name); - Q_ASSERT_X(function, __FUNCTION__, std::string("Could not resolve ").append(name).c_str()); - return Callable { function }; -} -#define FORWARD resolve(SIXENSE, __FUNCTION__) - - -void loadSixense() { - Q_ASSERT_X(!(SIXENSE && SIXENSE->isLoaded()), __FUNCTION__, "Sixense library already loaded"); - SIXENSE.reset(new QLibrary(SIXENSE_LIB_FILENAME)); - Q_CHECK_PTR(SIXENSE); - - if (SIXENSE->load()){ - qDebug() << "Loaded sixense library for hydra support -" << SIXENSE->fileName(); - } else { - qDebug() << "Sixense library at" << SIXENSE->fileName() << "failed to load:" << SIXENSE->errorString(); - qDebug() << "Continuing without hydra support."; - } -} -void unloadSixense() { - SIXENSE->unload(); -} - - -// sixense.h wrapper for OSX dynamic linking -int sixenseInit() { - loadSixense(); - if (!SIXENSE || !SIXENSE->isLoaded()) { - return SIXENSE_FAILURE; - } - return FORWARD(); -} -int sixenseExit() { - auto returnCode = FORWARD(); - unloadSixense(); - return returnCode; -} - -int sixenseGetMaxBases() { - return FORWARD(); -} -int sixenseSetActiveBase(int i) { - return FORWARD(i); -} -int sixenseIsBaseConnected(int i) { - return FORWARD(i); -} - -int sixenseGetMaxControllers() { - return FORWARD(); -} -int sixenseIsControllerEnabled(int which) { - return FORWARD(which); -} -int sixenseGetNumActiveControllers() { - return FORWARD(); -} - -int sixenseGetHistorySize() { - return FORWARD(); -} - -int sixenseGetData(int which, int index_back, sixenseControllerData* data) { - return FORWARD(which, index_back, data); -} -int sixenseGetAllData(int index_back, sixenseAllControllerData* data) { - return FORWARD(index_back, data); -} -int sixenseGetNewestData(int which, sixenseControllerData* data) { - return FORWARD(which, data); -} -int sixenseGetAllNewestData(sixenseAllControllerData* data) { - return FORWARD(data); -} - -int sixenseSetHemisphereTrackingMode(int which_controller, int state) { - return FORWARD(which_controller, state); -} -int sixenseGetHemisphereTrackingMode(int which_controller, int* state) { - return FORWARD(which_controller, state); -} -int sixenseAutoEnableHemisphereTracking(int which_controller) { - return FORWARD(which_controller); -} - -int sixenseSetHighPriorityBindingEnabled(int on_or_off) { - return FORWARD(on_or_off); -} -int sixenseGetHighPriorityBindingEnabled(int* on_or_off) { - return FORWARD(on_or_off); -} - -int sixenseTriggerVibration(int controller_id, int duration_100ms, int pattern_id) { - return FORWARD(controller_id, duration_100ms, pattern_id); -} - -int sixenseSetFilterEnabled(int on_or_off) { - return FORWARD(on_or_off); -} -int sixenseGetFilterEnabled(int* on_or_off) { - return FORWARD(on_or_off); -} - -int sixenseSetFilterParams(float near_range, float near_val, float far_range, float far_val) { - return FORWARD(near_range, near_val, far_range, far_val); -} -int sixenseGetFilterParams(float* near_range, float* near_val, float* far_range, float* far_val) { - return FORWARD(near_range, near_val, far_range, far_val); -} - -int sixenseSetBaseColor(unsigned char red, unsigned char green, unsigned char blue) { - return FORWARD(red, green, blue); -} -int sixenseGetBaseColor(unsigned char* red, unsigned char* green, unsigned char* blue) { - return FORWARD(red, green, blue); -} -#endif diff --git a/plugins/hifiSpacemouse/src/SpacemouseManager.cpp b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp index 3d9b93ff44..4641799b79 100644 --- a/plugins/hifiSpacemouse/src/SpacemouseManager.cpp +++ b/plugins/hifiSpacemouse/src/SpacemouseManager.cpp @@ -18,7 +18,6 @@ #include #include -#include #include const QString SpacemouseManager::NAME { "Spacemouse" }; @@ -59,7 +58,6 @@ bool SpacemouseManager::activate() { if (instance->getDeviceID() == controller::Input::INVALID_DEVICE) { auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(instance); - UserActivityLogger::getInstance().connectedDevice("controller", NAME); } return true; } @@ -330,7 +328,6 @@ bool SpacemouseManager::RawInputEventFilter(void* msg, long* result) { auto userInputMapper = DependencyManager::get(); if (Is3dmouseAttached() && instance->getDeviceID() == controller::Input::INVALID_DEVICE) { userInputMapper->registerDevice(instance); - UserActivityLogger::getInstance().connectedDevice("controller", "Spacemouse"); } else if (!Is3dmouseAttached() && instance->getDeviceID() != controller::Input::INVALID_DEVICE) { userInputMapper->removeDevice(instance->getDeviceID()); @@ -857,7 +854,7 @@ void SpacemouseManager::init() { if (Is3dmouseAttached() && instance->getDeviceID() == controller::Input::INVALID_DEVICE) { auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(instance); - UserActivityLogger::getInstance().connectedDevice("controller", "Spacemouse"); + emit deviceConnected(getName()); } //let one axis be dominant //ConnexionClientControl(fConnexionClientID, kConnexionCtlSetSwitches, kConnexionSwitchDominant | kConnexionSwitchEnableAll, NULL); diff --git a/plugins/oculus/CMakeLists.txt b/plugins/oculus/CMakeLists.txt index a91690ecdd..778be08dcf 100644 --- a/plugins/oculus/CMakeLists.txt +++ b/plugins/oculus/CMakeLists.txt @@ -13,7 +13,7 @@ if (WIN32) set(TARGET_NAME oculus) setup_hifi_plugin(Multimedia) - link_hifi_libraries(shared gl gpu controllers ui plugins display-plugins input-plugins audio-client networking) + link_hifi_libraries(shared gl gpu controllers ui plugins ui-plugins display-plugins input-plugins audio-client networking) include_hifi_library_headers(octree) diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index e9f8545cff..e26a48b89c 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -8,6 +8,8 @@ #include "OculusBaseDisplayPlugin.h" #include +#include +#include #include "OculusHelpers.h" @@ -24,9 +26,26 @@ bool OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { auto trackingState = ovr_GetTrackingState(_session, _currentRenderFrameInfo.predictedDisplayTime, ovrTrue); _currentRenderFrameInfo.renderPose = toGlm(trackingState.HeadPose.ThePose); _currentRenderFrameInfo.presentPose = _currentRenderFrameInfo.renderPose; - Lock lock(_mutex); - _frameInfos[frameIndex] = _currentRenderFrameInfo; - return true; + + std::array handPoses; + // Make controller poses available to the presentation thread + ovr_for_each_hand([&](ovrHandType hand) { + static const auto REQUIRED_HAND_STATUS = ovrStatus_OrientationTracked & ovrStatus_PositionTracked; + if (REQUIRED_HAND_STATUS != (trackingState.HandStatusFlags[hand] & REQUIRED_HAND_STATUS)) { + return; + } + + auto correctedPose = ovrControllerPoseToHandPose(hand, trackingState.HandPoses[hand]); + static const glm::quat HAND_TO_LASER_ROTATION = glm::rotation(Vectors::UNIT_Z, Vectors::UNIT_NEG_Y); + handPoses[hand] = glm::translate(glm::mat4(), correctedPose.translation) * glm::mat4_cast(correctedPose.rotation * HAND_TO_LASER_ROTATION); + }); + + withRenderThreadLock([&] { + _uiModelTransform = DependencyManager::get()->getModelTransform(); + _handPoses = handPoses; + _frameInfos[frameIndex] = _currentRenderFrameInfo; + }); + return Parent::beginFrameRender(frameIndex); } bool OculusBaseDisplayPlugin::isSupported() const { diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 9f0e76363b..b3b1b20b2b 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -13,7 +13,7 @@ #include -#include +#include #include #include @@ -243,91 +243,13 @@ void OculusControllerManager::TouchDevice::focusOutEvent() { void OculusControllerManager::TouchDevice::handlePose(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, ovrHandType hand, const ovrPoseStatef& handPose) { - // When the sensor-to-world rotation is identity the coordinate axes look like this: - // - // user - // forward - // -z - // | - // y| user - // y o----x right - // o-----x user - // | up - // | - // z - // - // Rift - - // From ABOVE the hand canonical axes looks like this: - // - // | | | | y | | | | - // | | | | | | | | | - // | | | | | - // |left | / x---- + \ |right| - // | _/ z \_ | - // | | | | - // | | | | - // - - // So when the user is in Rift space facing the -zAxis with hands outstretched and palms down - // the rotation to align the Touch axes with those of the hands is: - // - // touchToHand = halfTurnAboutY * quaterTurnAboutX - - // Due to how the Touch controllers fit into the palm there is an offset that is different for each hand. - // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that - // the combination (measurement * offset) is identity at this orientation. - // - // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) - // - // An approximate offset for the Touch can be obtained by inspection: - // - // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) * glm::angleAxis(PI/4.0f, xAxis)) - // - // So the full equation is: - // - // Q = combinedMeasurement * touchToHand - // - // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) - // - // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) - auto poseId = hand == ovrHand_Left ? controller::LEFT_HAND : controller::RIGHT_HAND; auto& pose = _poseStateMap[poseId]; - - static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); - static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); - static const glm::quat touchToHand = yFlip * quarterX; - - static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); - static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); - static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); - - static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ * eighthX) * touchToHand; - static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ * eighthX) * touchToHand; - - static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches - static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET * 2.0f); - static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; - static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; - - auto translationOffset = (hand == ovrHand_Left ? leftTranslationOffset : rightTranslationOffset); - auto rotationOffset = (hand == ovrHand_Left ? leftRotationOffset : rightRotationOffset); - - glm::quat rotation = toGlm(handPose.ThePose.Orientation); - - pose.translation = toGlm(handPose.ThePose.Position); - pose.translation += rotation * translationOffset; - pose.rotation = rotation * rotationOffset; - pose.angularVelocity = toGlm(handPose.AngularVelocity); - pose.velocity = toGlm(handPose.LinearVelocity); - pose.valid = true; - + pose = ovrControllerPoseToHandPose(hand, handPose); // transform into avatar frame glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; pose = pose.transform(controllerToAvatar); + } bool OculusControllerManager::TouchDevice::triggerHapticPulse(float strength, float duration, controller::Hand hand) { diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index 1006d69f06..2b2ec5bdb0 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -28,6 +28,12 @@ bool OculusDisplayPlugin::internalActivate() { return result; } +void OculusDisplayPlugin::init() { + Plugin::init(); + + emit deviceConnected(getName()); +} + void OculusDisplayPlugin::cycleDebugOutput() { if (_session) { currentDebugMode = static_cast((currentDebugMode + 1) % ovrPerfHud_Count); diff --git a/plugins/oculus/src/OculusDisplayPlugin.h b/plugins/oculus/src/OculusDisplayPlugin.h index d6cd6f6f3d..ed6e0d13ea 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.h +++ b/plugins/oculus/src/OculusDisplayPlugin.h @@ -17,6 +17,8 @@ class OculusDisplayPlugin : public OculusBaseDisplayPlugin { public: const QString& getName() const override { return NAME; } + void init() override; + QString getPreferredAudioInDevice() const override; QString getPreferredAudioOutDevice() const override; diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index 6ddace684b..49c14c8d66 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -14,6 +14,10 @@ #include #include #include +#include + +#include +#include using Mutex = std::mutex; using Lock = std::unique_lock; @@ -50,6 +54,13 @@ bool oculusAvailable() { static std::once_flag once; static bool result { false }; std::call_once(once, [&] { + + static const QString DEBUG_FLAG("HIFI_DEBUG_OPENVR"); + static bool enableDebugOpenVR = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); + if (enableDebugOpenVR) { + return; + } + ovrDetectResult detect = ovr_Detect(0); if (!detect.IsOculusServiceRunning || !detect.IsOculusHMDConnected) { return; @@ -191,3 +202,88 @@ void SwapFramebufferWrapper::onBind(oglplus::Framebuffer::Target target) { void SwapFramebufferWrapper::onUnbind(oglplus::Framebuffer::Target target) { glFramebufferTexture2D(toEnum(target), GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); } + + +controller::Pose ovrControllerPoseToHandPose( + ovrHandType hand, + const ovrPoseStatef& handPose) { + // When the sensor-to-world rotation is identity the coordinate axes look like this: + // + // user + // forward + // -z + // | + // y| user + // y o----x right + // o-----x user + // | up + // | + // z + // + // Rift + + // From ABOVE the hand canonical axes looks like this: + // + // | | | | y | | | | + // | | | | | | | | | + // | | | | | + // |left | / x---- + \ |right| + // | _/ z \_ | + // | | | | + // | | | | + // + + // So when the user is in Rift space facing the -zAxis with hands outstretched and palms down + // the rotation to align the Touch axes with those of the hands is: + // + // touchToHand = halfTurnAboutY * quaterTurnAboutX + + // Due to how the Touch controllers fit into the palm there is an offset that is different for each hand. + // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that + // the combination (measurement * offset) is identity at this orientation. + // + // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) + // + // An approximate offset for the Touch can be obtained by inspection: + // + // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) * glm::angleAxis(PI/4.0f, xAxis)) + // + // So the full equation is: + // + // Q = combinedMeasurement * touchToHand + // + // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) + // + // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) + static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); + static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); + static const glm::quat touchToHand = yFlip * quarterX; + + static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); + + static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ * eighthX) * touchToHand; + static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ * eighthX) * touchToHand; + + static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches + static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET * 2.0f); + static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; + static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; + + auto translationOffset = (hand == ovrHand_Left ? leftTranslationOffset : rightTranslationOffset); + auto rotationOffset = (hand == ovrHand_Left ? leftRotationOffset : rightRotationOffset); + + glm::quat rotation = toGlm(handPose.ThePose.Orientation); + + controller::Pose pose; + pose.translation = toGlm(handPose.ThePose.Position); + pose.translation += rotation * translationOffset; + pose.rotation = rotation * rotationOffset; + pose.angularVelocity = toGlm(handPose.AngularVelocity); + pose.velocity = toGlm(handPose.LinearVelocity); + pose.valid = true; + return pose; +} \ No newline at end of file diff --git a/plugins/oculus/src/OculusHelpers.h b/plugins/oculus/src/OculusHelpers.h index 2f13c45466..66cdccf15a 100644 --- a/plugins/oculus/src/OculusHelpers.h +++ b/plugins/oculus/src/OculusHelpers.h @@ -13,6 +13,7 @@ #include #include +#include void logWarning(const char* what); void logFatal(const char* what); @@ -128,3 +129,7 @@ protected: private: ovrSession _session; }; + +controller::Pose ovrControllerPoseToHandPose( + ovrHandType hand, + const ovrPoseStatef& handPose); diff --git a/plugins/oculusLegacy/CMakeLists.txt b/plugins/oculusLegacy/CMakeLists.txt index a4e00013f1..c1f2c6249f 100644 --- a/plugins/oculusLegacy/CMakeLists.txt +++ b/plugins/oculusLegacy/CMakeLists.txt @@ -12,7 +12,7 @@ if (APPLE) set(TARGET_NAME oculusLegacy) setup_hifi_plugin() - link_hifi_libraries(shared gl gpu plugins ui display-plugins input-plugins) + link_hifi_libraries(shared gl gpu plugins ui ui-plugins display-plugins input-plugins) include_hifi_library_headers(octree) diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 8e044fbc16..2c032f7005 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -26,7 +26,7 @@ #include #include -#include "plugins/PluginContainer.h" +#include #include "OculusHelpers.h" using namespace oglplus; @@ -36,18 +36,26 @@ const QString OculusLegacyDisplayPlugin::NAME("Oculus Rift"); OculusLegacyDisplayPlugin::OculusLegacyDisplayPlugin() { } +void OculusLegacyDisplayPlugin::init() { + Plugin::init(); + + emit deviceConnected(getName()); +} + void OculusLegacyDisplayPlugin::resetSensors() { ovrHmd_RecenterPose(_hmd); } bool OculusLegacyDisplayPlugin::beginFrameRender(uint32_t frameIndex) { + _currentRenderFrameInfo = FrameInfo(); _currentRenderFrameInfo.predictedDisplayTime = _currentRenderFrameInfo.sensorSampleTime = ovr_GetTimeInSeconds(); _trackingState = ovrHmd_GetTrackingState(_hmd, _currentRenderFrameInfo.predictedDisplayTime); _currentRenderFrameInfo.rawRenderPose = _currentRenderFrameInfo.renderPose = toGlm(_trackingState.HeadPose.ThePose); - Lock lock(_mutex); - _frameInfos[frameIndex] = _currentRenderFrameInfo; - return true; + withRenderThreadLock([&]{ + _frameInfos[frameIndex] = _currentRenderFrameInfo; + }); + return Parent::beginFrameRender(frameIndex); } bool OculusLegacyDisplayPlugin::isSupported() const { diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h index 453a6f9168..6ffc1a7f44 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.h @@ -23,6 +23,8 @@ public: bool isSupported() const override; const QString& getName() const override { return NAME; } + void init() override; + int getHmdScreen() const override; // Stereo specific methods diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt index 1ba8d05b92..8263e87767 100644 --- a/plugins/openvr/CMakeLists.txt +++ b/plugins/openvr/CMakeLists.txt @@ -12,7 +12,7 @@ if (WIN32) set(TARGET_NAME openvr) setup_hifi_plugin(OpenGL Script Qml Widgets) link_hifi_libraries(shared gl networking controllers ui - plugins display-plugins input-plugins script-engine + plugins display-plugins ui-plugins input-plugins script-engine render-utils model gpu render model-networking fbx) include_hifi_library_headers(octree) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 3e7e5abbf3..cfb374e3bd 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -18,9 +18,11 @@ #include #include +#include #include -#include +#include #include +#include #include #include "OpenVrHelpers.h" @@ -29,19 +31,29 @@ Q_DECLARE_LOGGING_CATEGORY(displayplugins) const QString OpenVrDisplayPlugin::NAME("OpenVR (Vive)"); const QString StandingHMDSensorMode = "Standing HMD Sensor Mode"; // this probably shouldn't be hardcoded here -static vr::IVRCompositor* _compositor{ nullptr }; +static vr::IVRCompositor* _compositor { nullptr }; vr::TrackedDevicePose_t _trackedDevicePose[vr::k_unMaxTrackedDeviceCount]; + mat4 _trackedDevicePoseMat4[vr::k_unMaxTrackedDeviceCount]; vec3 _trackedDeviceLinearVelocities[vr::k_unMaxTrackedDeviceCount]; vec3 _trackedDeviceAngularVelocities[vr::k_unMaxTrackedDeviceCount]; + static mat4 _sensorResetMat; static std::array VR_EYES { { vr::Eye_Left, vr::Eye_Right } }; +bool _openVrDisplayActive { false }; bool OpenVrDisplayPlugin::isSupported() const { return openVrSupported(); } +void OpenVrDisplayPlugin::init() { + Plugin::init(); + + emit deviceConnected(getName()); +} + bool OpenVrDisplayPlugin::internalActivate() { + _openVrDisplayActive = true; _container->setIsOptionChecked(StandingHMDSensorMode, true); if (!_system) { @@ -57,16 +69,14 @@ bool OpenVrDisplayPlugin::internalActivate() { // left + right eyes _renderTargetSize.x *= 2; - { - Lock lock(_poseMutex); + withRenderThreadLock([&] { openvr_for_each_eye([&](vr::Hmd_Eye eye) { _eyeOffsets[eye] = toGlm(_system->GetEyeToHeadTransform(eye)); _eyeProjections[eye] = toGlm(_system->GetProjectionMatrix(eye, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP, vr::API_OpenGL)); }); // FIXME Calculate the proper combined projection by using GetProjectionRaw values from both eyes _cullingProjection = _eyeProjections[0]; - - } + }); _compositor = vr::VRCompositor(); Q_ASSERT(_compositor); @@ -86,7 +96,9 @@ bool OpenVrDisplayPlugin::internalActivate() { glm::vec3 uiPos(0.0f, UI_HEIGHT, UI_RADIUS - (0.5f * zSize) - UI_Z_OFFSET); _sensorResetMat = glm::inverse(createMatFromQuatAndPos(glm::quat(), uiPos)); } else { - qDebug() << "OpenVR: error could not get chaperone pointer"; + #if DEV_BUILD + qDebug() << "OpenVR: error could not get chaperone pointer"; + #endif } return Parent::internalActivate(); @@ -94,6 +106,7 @@ bool OpenVrDisplayPlugin::internalActivate() { void OpenVrDisplayPlugin::internalDeactivate() { Parent::internalDeactivate(); + _openVrDisplayActive = false; _container->setIsOptionChecked(StandingHMDSensorMode, false); if (_system) { // Invalidate poses. It's fine if someone else sets these shared values, but we're about to stop updating them, and @@ -110,7 +123,7 @@ void OpenVrDisplayPlugin::internalDeactivate() { void OpenVrDisplayPlugin::customizeContext() { // Display plugins in DLLs must initialize glew locally static std::once_flag once; - std::call_once(once, []{ + std::call_once(once, [] { glewExperimental = true; GLenum err = glewInit(); glGetError(); // clear the potential error from glewExperimental @@ -120,9 +133,10 @@ void OpenVrDisplayPlugin::customizeContext() { } void OpenVrDisplayPlugin::resetSensors() { - Lock lock(_poseMutex); - glm::mat4 m = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); - _sensorResetMat = glm::inverse(cancelOutRollAndPitch(m)); + withRenderThreadLock([&] { + glm::mat4 m = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); + _sensorResetMat = glm::inverse(cancelOutRollAndPitch(m)); + }); } @@ -144,6 +158,24 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, _currentRenderFrameInfo.predictedDisplayTime, _trackedDevicePose, vr::k_unMaxTrackedDeviceCount); + + vr::TrackedDeviceIndex_t handIndices[2] { vr::k_unTrackedDeviceIndexInvalid, vr::k_unTrackedDeviceIndexInvalid }; + { + vr::TrackedDeviceIndex_t controllerIndices[2] ; + auto trackedCount = _system->GetSortedTrackedDeviceIndicesOfClass(vr::TrackedDeviceClass_Controller, controllerIndices, 2); + // Find the left and right hand controllers, if they exist + for (uint32_t i = 0; i < std::min(trackedCount, 2); ++i) { + if (_trackedDevicePose[i].bPoseIsValid) { + auto role = _system->GetControllerRoleForTrackedDeviceIndex(controllerIndices[i]); + if (vr::TrackedControllerRole_LeftHand == role) { + handIndices[0] = controllerIndices[i]; + } else if (vr::TrackedControllerRole_RightHand == role) { + handIndices[1] = controllerIndices[i]; + } + } + } + } + // copy and process predictedTrackedDevicePoses for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { _trackedDevicePoseMat4[i] = _sensorResetMat * toGlm(_trackedDevicePose[i].mDeviceToAbsoluteTracking); @@ -153,18 +185,39 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _currentRenderFrameInfo.rawRenderPose = toGlm(_trackedDevicePose[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking); _currentRenderFrameInfo.renderPose = _trackedDevicePoseMat4[vr::k_unTrackedDeviceIndex_Hmd]; - Lock lock(_mutex); - _frameInfos[frameIndex] = _currentRenderFrameInfo; - return true; + bool keyboardVisible = isOpenVrKeyboardShown(); + + std::array handPoses; + if (!keyboardVisible) { + for (int i = 0; i < 2; ++i) { + if (handIndices[i] == vr::k_unTrackedDeviceIndexInvalid) { + continue; + } + auto deviceIndex = handIndices[i]; + const mat4& mat = _trackedDevicePoseMat4[deviceIndex]; + const vec3& linearVelocity = _trackedDeviceLinearVelocities[deviceIndex]; + const vec3& angularVelocity = _trackedDeviceAngularVelocities[deviceIndex]; + auto correctedPose = openVrControllerPoseToHandPose(i == 0, mat, linearVelocity, angularVelocity); + static const glm::quat HAND_TO_LASER_ROTATION = glm::rotation(Vectors::UNIT_Z, Vectors::UNIT_NEG_Y); + handPoses[i] = glm::translate(glm::mat4(), correctedPose.translation) * glm::mat4_cast(correctedPose.rotation * HAND_TO_LASER_ROTATION); + } + } + + withRenderThreadLock([&] { + _uiModelTransform = DependencyManager::get()->getModelTransform(); + // Make controller poses available to the presentation thread + _handPoses = handPoses; + _frameInfos[frameIndex] = _currentRenderFrameInfo; + }); + return Parent::beginFrameRender(frameIndex); } void OpenVrDisplayPlugin::hmdPresent() { - PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentPresentFrameIndex) // Flip y-axis since GL UV coords are backwards. - static vr::VRTextureBounds_t leftBounds{ 0, 0, 0.5f, 1 }; - static vr::VRTextureBounds_t rightBounds{ 0.5f, 0, 1, 1 }; + static vr::VRTextureBounds_t leftBounds { 0, 0, 0.5f, 1 }; + static vr::VRTextureBounds_t rightBounds { 0.5f, 0, 1, 1 }; vr::Texture_t texture { (void*)oglplus::GetName(_compositeFramebuffer->color), vr::API_OpenGL, vr::ColorSpace_Auto }; @@ -185,6 +238,10 @@ bool OpenVrDisplayPlugin::isHmdMounted() const { } void OpenVrDisplayPlugin::updatePresentPose() { + mat4 sensorResetMat; + withPresentThreadLock([&] { + sensorResetMat = _sensorResetMat; + }); { float fSecondsSinceLastVsync; _system->GetTimeSinceLastVsync(&fSecondsSinceLastVsync, nullptr); @@ -196,9 +253,32 @@ void OpenVrDisplayPlugin::updatePresentPose() { _system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, fPredictedSecondsFromNow, &pose, 1); _currentPresentFrameInfo.rawPresentPose = toGlm(pose.mDeviceToAbsoluteTracking); } - _currentPresentFrameInfo.presentPose = _sensorResetMat * _currentPresentFrameInfo.rawPresentPose; + _currentPresentFrameInfo.presentPose = sensorResetMat * _currentPresentFrameInfo.rawPresentPose; mat3 renderRotation(_currentPresentFrameInfo.rawRenderPose); mat3 presentRotation(_currentPresentFrameInfo.rawPresentPose); _currentPresentFrameInfo.presentReprojection = glm::mat3(glm::inverse(renderRotation) * presentRotation); } +bool OpenVrDisplayPlugin::suppressKeyboard() { + if (isOpenVrKeyboardShown()) { + return false; + } + if (!_keyboardSupressionCount.fetch_add(1)) { + disableOpenVrKeyboard(); + } + return true; +} + +void OpenVrDisplayPlugin::unsuppressKeyboard() { + if (_keyboardSupressionCount == 0) { + qWarning() << "Attempted to unsuppress a keyboard that was not suppressed"; + return; + } + if (1 == _keyboardSupressionCount.fetch_sub(1)) { + enableOpenVrKeyboard(_container); + } +} + +bool OpenVrDisplayPlugin::isKeyboardVisible() { + return isOpenVrKeyboardShown(); +} diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index fda5e37c2a..fca4dab9e9 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -21,6 +21,8 @@ public: bool isSupported() const override; const QString& getName() const override { return NAME; } + void init() override; + float getTargetFrameRate() const override { return TARGET_RATE_OpenVr; } void customizeContext() override; @@ -30,6 +32,10 @@ public: bool beginFrameRender(uint32_t frameIndex) override; void cycleDebugOutput() override { _lockCurrentTexture = !_lockCurrentTexture; } + bool suppressKeyboard() override; + void unsuppressKeyboard() override; + bool isKeyboardVisible() override; + protected: bool internalActivate() override; void internalDeactivate() override; @@ -39,9 +45,10 @@ protected: bool isHmdMounted() const override; void postPreview() override; + private: vr::IVRSystem* _system { nullptr }; std::atomic _hmdActivityLevel { vr::k_EDeviceActivityLevel_Unknown }; + std::atomic _keyboardSupressionCount{ 0 }; static const QString NAME; - mutable Mutex _poseMutex; }; diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 3c765c3fa8..399712d920 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -14,8 +14,16 @@ #include #include #include +#include +#include #include +#include +#include +#include +#include +#include +#include "../../interface/src/Menu.h" Q_DECLARE_LOGGING_CATEGORY(displayplugins) Q_LOGGING_CATEGORY(displayplugins, "hifi.plugins.display") @@ -56,17 +64,25 @@ vr::IVRSystem* acquireOpenVrSystem() { if (hmdPresent) { Lock lock(mutex); if (!activeHmd) { - qCDebug(displayplugins) << "OpenVR: No vr::IVRSystem instance active, building"; + #if DEV_BUILD + qCDebug(displayplugins) << "OpenVR: No vr::IVRSystem instance active, building"; + #endif vr::EVRInitError eError = vr::VRInitError_None; activeHmd = vr::VR_Init(&eError, vr::VRApplication_Scene); - qCDebug(displayplugins) << "OpenVR display: HMD is " << activeHmd << " error is " << eError; + #if DEV_BUILD + qCDebug(displayplugins) << "OpenVR display: HMD is " << activeHmd << " error is " << eError; + #endif } if (activeHmd) { - qCDebug(displayplugins) << "OpenVR: incrementing refcount"; + #if DEV_BUILD + qCDebug(displayplugins) << "OpenVR: incrementing refcount"; + #endif ++refCount; } } else { - qCDebug(displayplugins) << "OpenVR: no hmd present"; + #if DEV_BUILD + qCDebug(displayplugins) << "OpenVR: no hmd present"; + #endif } return activeHmd; } @@ -74,16 +90,160 @@ vr::IVRSystem* acquireOpenVrSystem() { void releaseOpenVrSystem() { if (activeHmd) { Lock lock(mutex); - qCDebug(displayplugins) << "OpenVR: decrementing refcount"; + #if DEV_BUILD + qCDebug(displayplugins) << "OpenVR: decrementing refcount"; + #endif --refCount; if (0 == refCount) { - qCDebug(displayplugins) << "OpenVR: zero refcount, deallocate VR system"; + #if DEV_BUILD + qCDebug(displayplugins) << "OpenVR: zero refcount, deallocate VR system"; + #endif vr::VR_Shutdown(); activeHmd = nullptr; } } } +static char textArray[8192]; + +static QMetaObject::Connection _focusConnection, _focusTextConnection, _overlayMenuConnection; +extern bool _openVrDisplayActive; +static vr::IVROverlay* _overlay { nullptr }; +static QObject* _keyboardFocusObject { nullptr }; +static QString _existingText; +static Qt::InputMethodHints _currentHints; +extern vr::TrackedDevicePose_t _trackedDevicePose[vr::k_unMaxTrackedDeviceCount]; +static bool _keyboardShown { false }; +static bool _overlayRevealed { false }; +static const uint32_t SHOW_KEYBOARD_DELAY_MS = 400; + +void showOpenVrKeyboard(bool show = true) { + if (!_overlay) { + return; + } + + if (show) { + // To avoid flickering the keyboard when a text element is only briefly selected, + // show the keyboard asynchrnously after a very short delay, but only after we check + // that the current focus object is still one that is text enabled + QTimer::singleShot(SHOW_KEYBOARD_DELAY_MS, [] { + auto offscreenUi = DependencyManager::get(); + auto currentFocus = offscreenUi->getWindow()->focusObject(); + QInputMethodQueryEvent query(Qt::ImEnabled | Qt::ImQueryInput | Qt::ImHints); + qApp->sendEvent(currentFocus, &query); + // Current focus isn't text enabled, bail early. + if (!query.value(Qt::ImEnabled).toBool()) { + return; + } + // We're going to show the keyboard now... + _keyboardFocusObject = currentFocus; + _currentHints = Qt::InputMethodHints(query.value(Qt::ImHints).toUInt()); + vr::EGamepadTextInputMode inputMode = vr::k_EGamepadTextInputModeNormal; + if (_currentHints & Qt::ImhHiddenText) { + inputMode = vr::k_EGamepadTextInputModePassword; + } + vr::EGamepadTextInputLineMode lineMode = vr::k_EGamepadTextInputLineModeSingleLine; + if (_currentHints & Qt::ImhMultiLine) { + lineMode = vr::k_EGamepadTextInputLineModeMultipleLines; + } + _existingText = query.value(Qt::ImSurroundingText).toString(); + + auto showKeyboardResult = _overlay->ShowKeyboard(inputMode, lineMode, "Keyboard", 1024, + _existingText.toLocal8Bit().toStdString().c_str(), false, 0); + + if (vr::VROverlayError_None == showKeyboardResult) { + _keyboardShown = true; + // Try to position the keyboard slightly below where the user is looking. + mat4 headPose = cancelOutRollAndPitch(toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking)); + mat4 keyboardTransform = glm::translate(headPose, vec3(0, -0.5, -1)); + keyboardTransform = keyboardTransform * glm::rotate(mat4(), 3.14159f / 4.0f, vec3(-1, 0, 0)); + auto keyboardTransformVr = toOpenVr(keyboardTransform); + _overlay->SetKeyboardTransformAbsolute(vr::ETrackingUniverseOrigin::TrackingUniverseStanding, &keyboardTransformVr); + } + }); + } else { + _keyboardFocusObject = nullptr; + if (_keyboardShown) { + _overlay->HideKeyboard(); + _keyboardShown = false; + } + } +} + +void finishOpenVrKeyboardInput() { + auto offscreenUi = DependencyManager::get(); + auto chars = _overlay->GetKeyboardText(textArray, 8192); + auto newText = QString(QByteArray(textArray, chars)); + _keyboardFocusObject->setProperty("text", newText); + //// TODO modify the new text to match the possible input hints: + //// ImhDigitsOnly ImhFormattedNumbersOnly ImhUppercaseOnly ImhLowercaseOnly + //// ImhDialableCharactersOnly ImhEmailCharactersOnly ImhUrlCharactersOnly ImhLatinOnly + //QInputMethodEvent event(_existingText, QList()); + //event.setCommitString(newText, 0, _existingText.size()); + //qApp->sendEvent(_keyboardFocusObject, &event); + // Simulate an enter press on the top level window to trigger the action + if (0 == (_currentHints & Qt::ImhMultiLine)) { + qApp->sendEvent(offscreenUi->getWindow(), &QKeyEvent(QEvent::KeyPress, Qt::Key_Return, Qt::KeyboardModifiers(), QString("\n"))); + qApp->sendEvent(offscreenUi->getWindow(), &QKeyEvent(QEvent::KeyRelease, Qt::Key_Return, Qt::KeyboardModifiers())); + } +} + +static const QString DEBUG_FLAG("HIFI_DISABLE_STEAM_VR_KEYBOARD"); +bool disableSteamVrKeyboard = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); + +void enableOpenVrKeyboard(PluginContainer* container) { + if (disableSteamVrKeyboard) { + return; + } + auto offscreenUi = DependencyManager::get(); + _overlay = vr::VROverlay(); + + + auto menu = container->getPrimaryMenu(); + auto action = menu->getActionForOption(MenuOption::Overlays); + + // When the overlays are revealed, suppress the keyboard from appearing on text focus for a tenth of a second. + _overlayMenuConnection = QObject::connect(action, &QAction::triggered, [action] { + if (action->isChecked()) { + _overlayRevealed = true; + const int KEYBOARD_DELAY_MS = 100; + QTimer::singleShot(KEYBOARD_DELAY_MS, [&] { _overlayRevealed = false; }); + } + }); + + _focusConnection = QObject::connect(offscreenUi->getWindow(), &QQuickWindow::focusObjectChanged, [](QObject* object) { + if (object != _keyboardFocusObject) { + showOpenVrKeyboard(false); + } + }); + + _focusTextConnection = QObject::connect(offscreenUi.data(), &OffscreenUi::focusTextChanged, [](bool focusText) { + if (_openVrDisplayActive) { + if (_overlayRevealed) { + // suppress at most one text focus event + _overlayRevealed = false; + return; + } + showOpenVrKeyboard(focusText); + } + }); +} + + +void disableOpenVrKeyboard() { + if (disableSteamVrKeyboard) { + return; + } + QObject::disconnect(_overlayMenuConnection); + QObject::disconnect(_focusTextConnection); + QObject::disconnect(_focusConnection); +} + +bool isOpenVrKeyboardShown() { + return _keyboardShown; +} + + void handleOpenVrEvents() { if (!activeHmd) { return; @@ -101,10 +261,105 @@ void handleOpenVrEvents() { QMetaObject::invokeMethod(qApp, "quit"); break; + case vr::VREvent_KeyboardDone: + finishOpenVrKeyboardInput(); + + // FALL THROUGH + case vr::VREvent_KeyboardClosed: + _keyboardFocusObject = nullptr; + _keyboardShown = false; + break; + default: break; } - qDebug() << "OpenVR: Event " << activeHmd->GetEventTypeNameFromEnum((vr::EVREventType)event.eventType) << "(" << event.eventType << ")"; + #if DEV_BUILD + qDebug() << "OpenVR: Event " << activeHmd->GetEventTypeNameFromEnum((vr::EVREventType)event.eventType) << "(" << event.eventType << ")"; + #endif } } + +controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity) { + // When the sensor-to-world rotation is identity the coordinate axes look like this: + // + // user + // forward + // -z + // | + // y| user + // y o----x right + // o-----x user + // | up + // | + // z + // + // Rift + + // From ABOVE the hand canonical axes looks like this: + // + // | | | | y | | | | + // | | | | | | | | | + // | | | | | + // |left | / x---- + \ |right| + // | _/ z \_ | + // | | | | + // | | | | + // + + // So when the user is in Rift space facing the -zAxis with hands outstretched and palms down + // the rotation to align the Touch axes with those of the hands is: + // + // touchToHand = halfTurnAboutY * quaterTurnAboutX + + // Due to how the Touch controllers fit into the palm there is an offset that is different for each hand. + // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that + // the combination (measurement * offset) is identity at this orientation. + // + // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) + // + // An approximate offset for the Touch can be obtained by inspection: + // + // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) * glm::angleAxis(PI/4.0f, xAxis)) + // + // So the full equation is: + // + // Q = combinedMeasurement * touchToHand + // + // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) + // + // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) + static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); + static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); + static const glm::quat touchToHand = yFlip * quarterX; + + static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); + + static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ * eighthX) * touchToHand; + static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ * eighthX) * touchToHand; + + static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches + static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET * 2.0f); + static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; + static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; + + auto translationOffset = (isLeftHand ? leftTranslationOffset : rightTranslationOffset); + auto rotationOffset = (isLeftHand ? leftRotationOffset : rightRotationOffset); + + glm::vec3 position = extractTranslation(mat); + glm::quat rotation = glm::normalize(glm::quat_cast(mat)); + + position += rotation * translationOffset; + rotation = rotation * rotationOffset; + + // transform into avatar frame + auto result = controller::Pose(position, rotation); + // handle change in velocity due to translationOffset + result.velocity = linearVelocity + glm::cross(angularVelocity, position - extractTranslation(mat)); + result.angularVelocity = angularVelocity; + return result; +} diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index 1e5914844c..19c9cbfff5 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -12,12 +12,19 @@ #include #include +#include +#include + bool openVrSupported(); vr::IVRSystem* acquireOpenVrSystem(); void releaseOpenVrSystem(); void handleOpenVrEvents(); bool openVrQuitRequested(); +void enableOpenVrKeyboard(PluginContainer* container); +void disableOpenVrKeyboard(); +bool isOpenVrKeyboardShown(); + template void openvr_for_each_eye(F f) { @@ -41,3 +48,15 @@ inline mat4 toGlm(const vr::HmdMatrix34_t& m) { m.m[0][3], m.m[1][3], m.m[2][3], 1.0f); return result; } + +inline vr::HmdMatrix34_t toOpenVr(const mat4& m) { + vr::HmdMatrix34_t result; + for (uint8_t i = 0; i < 3; ++i) { + for (uint8_t j = 0; j < 4; ++j) { + result.m[i][j] = m[j][i]; + } + } + return result; +} + +controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity); diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index b862aacb06..4a515978c3 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -11,8 +11,6 @@ #include "ViveControllerManager.h" -#include - #include #include #include @@ -20,8 +18,9 @@ #include #include #include -#include +#include #include +#include #include @@ -38,10 +37,6 @@ vr::IVRSystem* acquireOpenVrSystem(); void releaseOpenVrSystem(); -static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches -static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET / 2.0f, - CONTROLLER_LENGTH_OFFSET * 2.0f); static const char* CONTROLLER_MODEL_STRING = "vr_controller_05_wireless_b"; static const QString MENU_PARENT = "Avatar"; @@ -68,6 +63,8 @@ bool ViveControllerManager::activate() { } Q_ASSERT(_system); + enableOpenVrKeyboard(_container); + // OpenVR provides 3d mesh representations of the controllers // Disabled controller rendering code /* @@ -131,6 +128,8 @@ bool ViveControllerManager::activate() { void ViveControllerManager::deactivate() { InputPlugin::deactivate(); + disableOpenVrKeyboard(); + _container->removeMenuItem(MENU_NAME, RENDER_CONTROLLERS); _container->removeMenu(MENU_PATH); @@ -229,7 +228,6 @@ void ViveControllerManager::pluginUpdate(float deltaTime, const controller::Inpu if (!_registeredWithInputMapper && _inputDevice->_trackedControllers > 0) { userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; - UserActivityLogger::getInstance().connectedDevice("spatial_controller", "steamVR"); } } @@ -237,6 +235,12 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle _poseStateMap.clear(); _buttonPressedMap.clear(); + // While the keyboard is open, we defer strictly to the keyboard values + if (isOpenVrKeyboardShown()) { + _axisStateMap.clear(); + return; + } + PerformanceTimer perfTimer("ViveControllerManager::update"); auto leftHandDeviceIndex = _system->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_LeftHand); @@ -295,20 +299,24 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u } // pseudo buttons the depend on both of the above for-loops - partitionTouchpad(controller::LS, controller::LX, controller::LY, controller::LS_CENTER, controller::LS_OUTER); - partitionTouchpad(controller::RS, controller::RX, controller::RY, controller::RS_CENTER, controller::RS_OUTER); + partitionTouchpad(controller::LS, controller::LX, controller::LY, controller::LS_CENTER, controller::LS_X, controller::LS_Y); + partitionTouchpad(controller::RS, controller::RX, controller::RY, controller::RS_CENTER, controller::RS_X, controller::RS_Y); } } } -void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPseudoButton, int outerPseudoButton) { +void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPseudoButton, int xPseudoButton, int yPseudoButton) { // Populate the L/RS_CENTER/OUTER pseudo buttons, corresponding to a partition of the L/RS space based on the X/Y values. const float CENTER_DEADBAND = 0.6f; + const float DIAGONAL_DIVIDE_IN_RADIANS = PI / 4.0f; if (_buttonPressedMap.find(sButton) != _buttonPressedMap.end()) { float absX = abs(_axisStateMap[xAxis]); float absY = abs(_axisStateMap[yAxis]); - bool isCenter = (absX < CENTER_DEADBAND) && (absY < CENTER_DEADBAND); // square deadband - _buttonPressedMap.insert(isCenter ? centerPseudoButton : outerPseudoButton); + glm::vec2 cartesianQuadrantI(absX, absY); + float angle = glm::atan(cartesianQuadrantI.y / cartesianQuadrantI.x); + float radius = glm::length(cartesianQuadrantI); + bool isCenter = radius < CENTER_DEADBAND; + _buttonPressedMap.insert(isCenter ? centerPseudoButton : ((angle < DIAGONAL_DIVIDE_IN_RADIANS) ? xPseudoButton :yPseudoButton)); } } @@ -375,86 +383,11 @@ void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand) { - // When the sensor-to-world rotation is identity the coordinate axes look like this: - // - // user - // forward - // -z - // | - // y| user - // y o----x right - // o-----x user - // | up - // | - // z - // - // Vive - // - - // From ABOVE the hand canonical axes looks like this: - // - // | | | | y | | | | - // | | | | | | | | | - // | | | | | - // |left | / x---- + \ |right| - // | _/ z \_ | - // | | | | - // | | | | - // - - // So when the user is standing in Vive space facing the -zAxis with hands outstretched and palms down - // the rotation to align the Vive axes with those of the hands is: - // - // QviveToHand = halfTurnAboutY * quaterTurnAboutX - - // Due to how the Vive controllers fit into the palm there is an offset that is different for each hand. - // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that - // the combination (measurement * offset) is identity at this orientation. - // - // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) - // - // An approximate offset for the Vive can be obtained by inspection: - // - // Qoffset = glm::inverse(glm::angleAxis(sign * PI/4.0f, zAxis) * glm::angleAxis(PI/2.0f, xAxis)) - // - // So the full equation is: - // - // Q = combinedMeasurement * viveToHand - // - // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) - // - // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) - - static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); - static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); - static const glm::quat viveToHand = yFlip * quarterX; - - static const glm::quat leftQuaterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); - static const glm::quat rightQuaterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); - static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); - - static const glm::quat leftRotationOffset = glm::inverse(leftQuaterZ * eighthX) * viveToHand; - static const glm::quat rightRotationOffset = glm::inverse(rightQuaterZ * eighthX) * viveToHand; - - static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; - static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; - - auto translationOffset = (isLeftHand ? leftTranslationOffset : rightTranslationOffset); - auto rotationOffset = (isLeftHand ? leftRotationOffset : rightRotationOffset); - - glm::vec3 position = extractTranslation(mat); - glm::quat rotation = glm::normalize(glm::quat_cast(mat)); - - position += rotation * translationOffset; - rotation = rotation * rotationOffset; + auto pose = openVrControllerPoseToHandPose(isLeftHand, mat, linearVelocity, angularVelocity); // transform into avatar frame glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; - auto avatarPose = controller::Pose(position, rotation); - // handle change in velocity due to translationOffset - avatarPose.velocity = linearVelocity + glm::cross(angularVelocity, position - extractTranslation(mat)); - avatarPose.angularVelocity = angularVelocity; - _poseStateMap[isLeftHand ? controller::LEFT_HAND : controller::RIGHT_HAND] = avatarPose.transform(controllerToAvatar); + _poseStateMap[isLeftHand ? controller::LEFT_HAND : controller::RIGHT_HAND] = pose.transform(controllerToAvatar); } bool ViveControllerManager::InputDevice::triggerHapticPulse(float strength, float duration, controller::Hand hand) { @@ -524,9 +457,11 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI makePair(RS, "RS"), // Differentiate where we are in the touch pad click makePair(LS_CENTER, "LSCenter"), - makePair(LS_OUTER, "LSOuter"), + makePair(LS_X, "LSX"), + makePair(LS_Y, "LSY"), makePair(RS_CENTER, "RSCenter"), - makePair(RS_OUTER, "RSOuter"), + makePair(RS_X, "RSX"), + makePair(RS_Y, "RSY"), // triggers makePair(LT, "LT"), diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 3a2ef1573f..95ff2f881a 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -64,7 +64,7 @@ private: void handleAxisEvent(float deltaTime, uint32_t axis, float x, float y, bool isLeftHand); void handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand); - void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int outerPseudoButton); + void partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int xPseudoButton, int yPseudoButton); class FilteredStick { public: diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 2a050d183e..2bf908ab57 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -12,6 +12,9 @@ Script.load("system/progress.js"); Script.load("system/away.js"); Script.load("system/users.js"); +Script.load("system/mute.js"); +Script.load("system/goto.js"); +Script.load("system/hmd.js"); Script.load("system/examples.js"); Script.load("system/edit.js"); Script.load("system/selectAudioDevice.js"); @@ -21,3 +24,4 @@ Script.load("system/controllers/handControllerPointer.js"); Script.load("system/controllers/squeezeHands.js"); Script.load("system/controllers/grab.js"); Script.load("system/dialTone.js"); +Script.load("system/firstPersonHMD.js"); diff --git a/scripts/developer/tests/toolbarTest.js b/scripts/developer/tests/toolbarTest.js new file mode 100644 index 0000000000..e21fbd8e19 --- /dev/null +++ b/scripts/developer/tests/toolbarTest.js @@ -0,0 +1,118 @@ +var isActive = false; + +var toolBar = (function() { + var that = {}, + toolBar, + activeButton, + newModelButton, + newCubeButton, + newSphereButton, + newLightButton, + newTextButton, + newWebButton, + newZoneButton, + newParticleButton + + var toolIconUrl = Script.resolvePath("../../system/assets/images/tools/"); + + function initialize() { + print("Toolbars: " + Toolbars); + toolBar = Toolbars.getToolbar("highfidelity.edit.toolbar"); + print("Toolbar: " + toolBar); + activeButton = toolBar.addButton({ + objectName: "activeButton", + imageURL: toolIconUrl + "edit-01.svg", + visible: true, + alpha: 0.9, + }); + + print("Button " + activeButton); + print("Button signal " + activeButton.clicked); + activeButton.clicked.connect(function(){ + print("Clicked on button " + isActive); + that.setActive(!isActive); + }); + + newModelButton = toolBar.addButton({ + objectName: "newModelButton", + imageURL: toolIconUrl + "model-01.svg", + alpha: 0.9, + visible: false + }); + + newCubeButton = toolBar.addButton({ + objectName: "newCubeButton", + imageURL: toolIconUrl + "cube-01.svg", + alpha: 0.9, + visible: false + }); + + newSphereButton = toolBar.addButton({ + objectName: "newSphereButton", + imageURL: toolIconUrl + "sphere-01.svg", + alpha: 0.9, + visible: false + }); + + newLightButton = toolBar.addButton({ + objectName: "newLightButton", + imageURL: toolIconUrl + "light-01.svg", + alpha: 0.9, + visible: false + }); + + newTextButton = toolBar.addButton({ + objectName: "newTextButton", + imageURL: toolIconUrl + "text-01.svg", + alpha: 0.9, + visible: false + }); + + newWebButton = toolBar.addButton({ + objectName: "newWebButton", + imageURL: toolIconUrl + "web-01.svg", + alpha: 0.9, + visible: false + }); + + newZoneButton = toolBar.addButton({ + objectName: "newZoneButton", + imageURL: toolIconUrl + "zone-01.svg", + alpha: 0.9, + visible: false + }); + + newParticleButton = toolBar.addButton({ + objectName: "newParticleButton", + imageURL: toolIconUrl + "particle-01.svg", + alpha: 0.9, + visible: false + }); + + that.setActive(false); + newModelButton.clicked(); + } + + that.setActive = function(active) { + if (active != isActive) { + isActive = active; + that.showTools(isActive); + } + }; + + // Sets visibility of tool buttons, excluding the power button + that.showTools = function(doShow) { + newModelButton.writeProperty('visible', doShow); + newCubeButton.writeProperty('visible', doShow); + newSphereButton.writeProperty('visible', doShow); + newLightButton.writeProperty('visible', doShow); + newTextButton.writeProperty('visible', doShow); + newWebButton.writeProperty('visible', doShow); + newZoneButton.writeProperty('visible', doShow); + newModelButton.writeProperty('visible', doShow); + newParticleButton.writeProperty('visible', doShow); + }; + + initialize(); + return that; +}()); diff --git a/scripts/system/assets/images/tools/hmd-switch-01.svg b/scripts/system/assets/images/tools/hmd-switch-01.svg new file mode 100644 index 0000000000..31389d355c --- /dev/null +++ b/scripts/system/assets/images/tools/hmd-switch-01.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/tools/microphone.svg b/scripts/system/assets/images/tools/microphone.svg new file mode 100644 index 0000000000..bd5e8afac7 --- /dev/null +++ b/scripts/system/assets/images/tools/microphone.svg @@ -0,0 +1,13 @@ + + + image/svg+xml + + + Layer 1 + + + + Mute + + + \ No newline at end of file diff --git a/scripts/system/away.js b/scripts/system/away.js index 38b0f13c00..04263d4223 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -158,6 +158,8 @@ function goAway() { return; } + UserActivityLogger.toggledAway(true); + isAway = true; print('going "away"'); wasMuted = AudioDevice.getMuted(); @@ -176,9 +178,11 @@ function goAway() { // tell the Reticle, we want to stop capturing the mouse until we come back Reticle.allowMouseCapture = false; - if (HMD.active) { - Reticle.visible = false; - } + // Allow users to find their way to other applications, our menus, etc. + // For desktop, that means we want the reticle visible. + // For HMD, the hmd preview will show the system mouse because of allowMouseCapture, + // but we want to turn off our Reticle so that we don't get two in preview and a stuck one in headset. + Reticle.visible = !HMD.active; wasHmdMounted = safeGetHMDMounted(); // always remember the correct state avatarPosition = MyAvatar.position; @@ -189,6 +193,9 @@ function goActive() { if (!isAway) { return; } + + UserActivityLogger.toggledAway(false); + isAway = false; print('going "active"'); if (!wasMuted) { diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 986a4c0722..f86ff158f6 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -10,11 +10,10 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, randFloat, randInt, pointInExtents, vec3equal, setEntityCustomData, getEntityCustomData */ +/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr */ Script.include("/~/system/libraries/utils.js"); - // // add lines where the hand ray picking is happening // @@ -39,6 +38,10 @@ var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only var PICK_WITH_HAND_RAY = true; +var DRAW_GRAB_BOXES = false; +var DRAW_HAND_SPHERES = false; +var DROP_WITHOUT_SHAKE = false; + // // distant manipulation // @@ -47,9 +50,7 @@ var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified -var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did var MOVE_WITH_HEAD = true; // experimental head-control of distantly held objects -var FAR_TO_NEAR_GRAB_PADDING_FACTOR = 1.2; var NO_INTERSECT_COLOR = { red: 10, @@ -69,29 +70,29 @@ var LINE_ENTITY_DIMENSIONS = { var LINE_LENGTH = 500; var PICK_MAX_DISTANCE = 500; // max length of pick-ray + // // near grabbing // -var GRAB_RADIUS = 0.06; // if the ray misses but an object is this close, it will still be selected +var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping. + var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position -var NEAR_PICK_MAX_DISTANCE = 0.3; // max length of pick-ray for close grabbing to be selected + +var NEAR_GRAB_RADIUS = 0.15; // radius used for palm vs object for near grabbing. +var NEAR_GRAB_MAX_DISTANCE = 1.0; // you cannot grab objects that are this far away from your hand + +var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing. + var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed -var SHOW_GRAB_SPHERE = false; // draw a green sphere to show the grab search position and size var CHECK_TOO_FAR_UNEQUIP_TIME = 1.0; // seconds -// -// equip -// - -var EQUIP_SPRING_SHUTOFF_DISTANCE = 0.05; -var EQUIP_SPRING_TIMEFRAME = 0.4; // how quickly objects move to their new position - // // other constants // +var HOTSPOT_DRAW_DISTANCE = 10; var RIGHT_HAND = 1; var LEFT_HAND = 0; @@ -133,8 +134,6 @@ var DEFAULT_GRABBABLE_DATA = { disableReleaseVelocity: false }; - - // sometimes we want to exclude objects from being picked var USE_BLACKLIST = true; var blacklist = []; @@ -147,29 +146,20 @@ var USE_ENTITY_LINES_FOR_MOVING = false; var USE_OVERLAY_LINES_FOR_MOVING = false; var USE_PARTICLE_BEAM_FOR_MOVING = true; - var USE_SPOTLIGHT = false; var USE_POINTLIGHT = false; +var FORBIDDEN_GRAB_NAMES = ["Grab Debug Entity", "grab pointer"]; +var FORBIDDEN_GRAB_TYPES = ['Unknown', 'Light', 'PolyLine', 'Zone']; + // states for the state machine var STATE_OFF = 0; var STATE_SEARCHING = 1; -var STATE_HOLD_SEARCHING = 2; -var STATE_DISTANCE_HOLDING = 3; -var STATE_CONTINUE_DISTANCE_HOLDING = 4; -var STATE_NEAR_GRABBING = 5; -var STATE_CONTINUE_NEAR_GRABBING = 6; -var STATE_NEAR_TRIGGER = 7; -var STATE_CONTINUE_NEAR_TRIGGER = 8; -var STATE_FAR_TRIGGER = 9; -var STATE_CONTINUE_FAR_TRIGGER = 10; -var STATE_RELEASE = 11; -var STATE_EQUIP = 12; -var STATE_HOLD = 13; -var STATE_CONTINUE_HOLD = 14; -var STATE_CONTINUE_EQUIP = 15; -var STATE_WAITING_FOR_RELEASE_THUMB_RELEASE = 16; -var STATE_WAITING_FOR_EQUIP_THUMB_RELEASE = 17; +var STATE_DISTANCE_HOLDING = 2; +var STATE_NEAR_GRABBING = 3; +var STATE_NEAR_TRIGGER = 4; +var STATE_FAR_TRIGGER = 5; +var STATE_HOLD = 6; // "collidesWith" is specified by comma-separated list of group names // the possible group names are: static, dynamic, kinematic, myAvatar, otherAvatar @@ -179,47 +169,47 @@ var COLLIDES_WITH_WHILE_MULTI_GRABBED = "dynamic"; var HEART_BEAT_INTERVAL = 5 * MSECS_PER_SEC; var HEART_BEAT_TIMEOUT = 15 * MSECS_PER_SEC; -function stateToName(state) { - switch (state) { - case STATE_OFF: - return "off"; - case STATE_SEARCHING: - return "searching"; - case STATE_HOLD_SEARCHING: - return "hold_searching"; - case STATE_DISTANCE_HOLDING: - return "distance_holding"; - case STATE_CONTINUE_DISTANCE_HOLDING: - return "continue_distance_holding"; - case STATE_NEAR_GRABBING: - return "near_grabbing"; - case STATE_CONTINUE_NEAR_GRABBING: - return "continue_near_grabbing"; - case STATE_NEAR_TRIGGER: - return "near_trigger"; - case STATE_CONTINUE_NEAR_TRIGGER: - return "continue_near_trigger"; - case STATE_FAR_TRIGGER: - return "far_trigger"; - case STATE_CONTINUE_FAR_TRIGGER: - return "continue_far_trigger"; - case STATE_RELEASE: - return "release"; - case STATE_EQUIP: - return "equip"; - case STATE_HOLD: - return "hold"; - case STATE_CONTINUE_HOLD: - return "continue_hold"; - case STATE_CONTINUE_EQUIP: - return "continue_equip"; - case STATE_WAITING_FOR_EQUIP_THUMB_RELEASE: - return "waiting_for_equip_thumb_release"; - case STATE_WAITING_FOR_RELEASE_THUMB_RELEASE: - return "waiting_for_release_thumb_release"; - } +var CONTROLLER_STATE_MACHINE = {}; - return "unknown"; +CONTROLLER_STATE_MACHINE[STATE_OFF] = { + name: "off", + enterMethod: "offEnter", + updateMethod: "off" +}; +CONTROLLER_STATE_MACHINE[STATE_SEARCHING] = { + name: "searching", + updateMethod: "search", + enterMethod: "searchEnter", + exitMethod: "searchExit" +}; +CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { + name: "distance_holding", + enterMethod: "distanceHoldingEnter", + updateMethod: "distanceHolding" +}; +CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { + name: "near_grabbing", + enterMethod: "nearGrabbingEnter", + updateMethod: "nearGrabbing" +}; +CONTROLLER_STATE_MACHINE[STATE_HOLD] = { + name: "hold", + enterMethod: "nearGrabbingEnter", + updateMethod: "nearGrabbing" +}; +CONTROLLER_STATE_MACHINE[STATE_NEAR_TRIGGER] = { + name: "trigger", + enterMethod: "nearTriggerEnter", + updateMethod: "nearTrigger" +}; +CONTROLLER_STATE_MACHINE[STATE_FAR_TRIGGER] = { + name: "far_trigger", + enterMethod: "farTriggerEnter", + updateMethod: "farTrigger" +}; + +function stateToName(state) { + return CONTROLLER_STATE_MACHINE[state] ? CONTROLLER_STATE_MACHINE[state].name : "???"; } function getTag() { @@ -249,6 +239,92 @@ function entityIsGrabbedByOther(entityID) { return false; } +function propsArePhysical(props) { + if (!props.dynamic) { + return false; + } + var isPhysical = (props.shapeType && props.shapeType != 'none'); + return isPhysical; +} + +// If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here, +// and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. +var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; +function isIn2DMode() { + // In this version, we make our own determination of whether we're aimed a HUD element, + // because other scripts (such as handControllerPointer) might be using some other visualization + // instead of setting Reticle.visible. + return EXTERNALLY_MANAGED_2D_MINOR_MODE && (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position)); +} +function restore2DMode() { + if (!EXTERNALLY_MANAGED_2D_MINOR_MODE) { + Reticle.setVisible(true); + } +} + +// EntityPropertiesCache is a helper class that contains a cache of entity properties. +// the hope is to prevent excess calls to Entity.getEntityProperties() +function EntityPropertiesCache() { + this.cache = {}; +} +EntityPropertiesCache.prototype.clear = function() { + this.cache = {}; +}; +EntityPropertiesCache.prototype.findEntities = function(position, radius) { + var entities = Entities.findEntities(position, radius); + var _this = this; + entities.forEach(function (x) { + _this.updateEntity(x); + }); +}; +EntityPropertiesCache.prototype.updateEntity = function(entityID) { + var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES); + + // convert props.userData from a string to an object. + var userData = {}; + if (props.userData) { + try { + userData = JSON.parse(props.userData); + } catch(err) { + print("WARNING: malformed userData on " + entityID + ", name = " + props.name + ", error = " + err); + } + } + props.userData = userData; + + this.cache[entityID] = props; +}; +EntityPropertiesCache.prototype.getEntities = function() { + return Object.keys(this.cache); +} +EntityPropertiesCache.prototype.getProps = function(entityID) { + var obj = this.cache[entityID] + return obj ? obj : undefined; +}; +EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) { + var props = this.cache[entityID]; + if (props) { + return props.userData.grabbableKey ? props.userData.grabbableKey : DEFAULT_GRABBABLE_DATA; + } else { + return undefined; + } +}; +EntityPropertiesCache.prototype.getGrabProps = function(entityID) { + var props = this.cache[entityID]; + if (props) { + return props.userData.grabKey ? props.userData.grabKey : {}; + } else { + return undefined; + } +}; +EntityPropertiesCache.prototype.getWearableProps = function(entityID) { + var props = this.cache[entityID]; + if (props) { + return props.userData.wearable ? props.userData.wearable : {}; + } else { + return undefined; + } +}; + function MyController(hand) { this.hand = hand; if (this.hand === RIGHT_HAND) { @@ -277,7 +353,6 @@ function MyController(hand) { //for visualizations this.overlayLine = null; this.particleBeamObject = null; - this.grabSphere = null; //for lights this.spotlight = null; @@ -285,6 +360,8 @@ function MyController(hand) { this.overlayLine = null; this.searchSphere = null; + this.waitForTriggerRelease = false; + // how far from camera to search intersection? var DEFAULT_SEARCH_SPHERE_DISTANCE = 1000; this.intersectionDistance = 0.0; @@ -297,57 +374,36 @@ function MyController(hand) { this.lastPickTime = 0; this.lastUnequipCheckTime = 0; + this.entityPropertyCache = new EntityPropertiesCache(); + var _this = this; - this.update = function() { + var suppressedIn2D = [STATE_OFF, STATE_SEARCHING]; + this.ignoreInput = function () { + // We've made the decision to use 'this' for new code, even though it is fragile, + // in order to keep/ the code uniform without making any no-op line changes. + return (-1 !== suppressedIn2D.indexOf(this.state)) && isIn2DMode(); + }; + + this.update = function(deltaTime) { this.updateSmoothedTrigger(); - switch (this.state) { - case STATE_OFF: - this.off(); - break; - case STATE_SEARCHING: - case STATE_HOLD_SEARCHING: - this.search(); - break; - case STATE_DISTANCE_HOLDING: - this.distanceHolding(); - break; - case STATE_CONTINUE_DISTANCE_HOLDING: - this.continueDistanceHolding(); - break; - case STATE_NEAR_GRABBING: - case STATE_EQUIP: - case STATE_HOLD: - this.nearGrabbing(); - break; - case STATE_WAITING_FOR_EQUIP_THUMB_RELEASE: - this.waitingForEquipThumbRelease(); - break; - case STATE_WAITING_FOR_RELEASE_THUMB_RELEASE: - this.waitingForReleaseThumbRelease(); - break; - case STATE_CONTINUE_NEAR_GRABBING: - case STATE_CONTINUE_HOLD: - case STATE_CONTINUE_EQUIP: - this.continueNearGrabbing(); - break; - case STATE_NEAR_TRIGGER: - this.nearTrigger(); - break; - case STATE_CONTINUE_NEAR_TRIGGER: - this.continueNearTrigger(); - break; - case STATE_FAR_TRIGGER: - this.farTrigger(); - break; - case STATE_CONTINUE_FAR_TRIGGER: - this.continueFarTrigger(); - break; - case STATE_RELEASE: - this.release(); - break; + if (this.ignoreInput()) { + this.turnOffVisualizations(); + return; + } + + if (CONTROLLER_STATE_MACHINE[this.state]) { + var updateMethodName = CONTROLLER_STATE_MACHINE[this.state].updateMethod; + var updateMethod = this[updateMethodName]; + if (updateMethod) { + updateMethod.call(this, deltaTime); + } else { + print("WARNING: could not find updateMethod for state " + stateToName(this.state)); + } + } else { + print("WARNING: could not find state " + this.state + " in state machine"); } }; @@ -356,13 +412,37 @@ function MyController(hand) { Entities.callEntityMethod(this.grabbedEntity, entityMethodName, args); } - this.setState = function(newState) { - this.grabSphereOff(); + this.setState = function(newState, reason) { + if (WANT_DEBUG || WANT_DEBUG_STATE) { - print("STATE (" + this.hand + "): " + stateToName(this.state) + " --> " + - stateToName(newState) + ", hand: " + this.hand); + var oldStateName = stateToName(this.state); + var newStateName = stateToName(newState); + print("STATE (" + this.hand + "): " + newStateName + " <-- " + oldStateName + ", reason = " + reason); } + + // exit the old state + if (CONTROLLER_STATE_MACHINE[this.state]) { + var exitMethodName = CONTROLLER_STATE_MACHINE[this.state].exitMethod; + var exitMethod = this[exitMethodName]; + if (exitMethod) { + exitMethod.call(this); + } + } else { + print("WARNING: could not find state " + this.state + " in state machine"); + } + this.state = newState; + + // enter the new state + if (CONTROLLER_STATE_MACHINE[newState]) { + var enterMethodName = CONTROLLER_STATE_MACHINE[newState].enterMethod; + var enterMethod = this[enterMethodName]; + if (enterMethod) { + enterMethod.call(this); + } + } else { + print("WARNING: could not find newState " + newState + " in state machine"); + } }; this.debugLine = function(closePoint, farPoint, color) { @@ -425,6 +505,8 @@ function MyController(hand) { color: color, alpha: SEARCH_SPHERE_ALPHA, solid: true, + ignoreRayIntersection: true, + drawInFront: true, // Even when burried inside of something, show it. visible: true } this.searchSphere = Overlays.addOverlay("sphere", sphereProperties); @@ -438,37 +520,6 @@ function MyController(hand) { } } - this.grabSphereOn = function() { - var color = {red: 0, green: 255, blue: 0}; - if (this.grabSphere === null) { - var sphereProperties = { - position: this.getHandPosition(), - size: GRAB_RADIUS*2, - color: color, - alpha: 0.1, - solid: true, - visible: true - } - this.grabSphere = Overlays.addOverlay("sphere", sphereProperties); - } else { - Overlays.editOverlay(this.grabSphere, { - position: this.getHandPosition(), - size: GRAB_RADIUS*2, - color: color, - alpha: 0.1, - solid: true, - visible: true - }); - } - } - - this.grabSphereOff = function() { - if (this.grabSphere !== null) { - Overlays.deleteOverlay(this.grabSphere); - this.grabSphere = null; - } - }; - this.overlayLineOn = function(closePoint, farPoint, color) { if (this.overlayLine === null) { var lineProperties = { @@ -477,19 +528,21 @@ function MyController(hand) { end: farPoint, color: color, ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. visible: true, alpha: 1 }; this.overlayLine = Overlays.addOverlay("line3d", lineProperties); } else { - var success = Overlays.editOverlay(this.overlayLine, { + Overlays.editOverlay(this.overlayLine, { lineWidth: 5, start: closePoint, end: farPoint, color: color, visible: true, ignoreRayIntersection: true, // always ignore this + drawInFront: true, // Even when burried inside of something, show it. alpha: 1 }); } @@ -598,13 +651,6 @@ function MyController(hand) { }) }; - this.renewParticleBeamLifetime = function() { - var props = Entities.getEntityProperties(this.particleBeamObject, "age"); - Entities.editEntity(this.particleBeamObject, { - lifetime: TEMPORARY_PARTICLE_BEAM_LIFETIME + props.age // renew lifetime - }) - } - this.evalLightWorldTransform = function(modelPos, modelRot) { var MODEL_LIGHT_POSITION = { @@ -625,7 +671,7 @@ function MyController(hand) { }; }; - this.handleSpotlight = function(parentID, position) { + this.handleSpotlight = function(parentID) { var LIFETIME = 100; var modelProperties = Entities.getEntityProperties(parentID, ['position', 'rotation']); @@ -649,7 +695,7 @@ function MyController(hand) { exponent: 0.3, cutoff: 20, lifetime: LIFETIME, - position: lightTransform.p, + position: lightTransform.p }; if (this.spotlight === null) { @@ -657,12 +703,12 @@ function MyController(hand) { } else { Entities.editEntity(this.spotlight, { //without this, this light would maintain rotation with its parent - rotation: Quat.fromPitchYawRollDegrees(-90, 0, 0), + rotation: Quat.fromPitchYawRollDegrees(-90, 0, 0) }) } }; - this.handlePointLight = function(parentID, position) { + this.handlePointLight = function(parentID) { var LIFETIME = 100; var modelProperties = Entities.getEntityProperties(parentID, ['position', 'rotation']); @@ -686,13 +732,11 @@ function MyController(hand) { exponent: 0.3, cutoff: 20, lifetime: LIFETIME, - position: lightTransform.p, + position: lightTransform.p }; if (this.pointlight === null) { this.pointlight = Entities.addEntity(lightProperties); - } else { - } }; @@ -738,14 +782,6 @@ function MyController(hand) { } }; - this.propsArePhysical = function(props) { - if (!props.dynamic) { - return false; - } - var isPhysical = (props.shapeType && props.shapeType != 'none'); - return isPhysical; - } - this.turnOffVisualizations = function() { if (USE_ENTITY_LINES_FOR_SEARCHING === true || USE_ENTITY_LINES_FOR_MOVING === true) { this.lineOff(); @@ -759,8 +795,7 @@ function MyController(hand) { this.particleBeamOff(); } this.searchSphereOff(); - - Reticle.setVisible(true); + restore2DMode(); }; @@ -820,275 +855,512 @@ function MyController(hand) { }; this.off = function() { - if (this.triggerSmoothedSqueezed() || this.secondarySqueezed()) { + + if (this.triggerSmoothedReleased()) { + this.waitForTriggerRelease = false; + } + if (!this.waitForTriggerRelease && this.triggerSmoothedSqueezed()) { this.lastPickTime = 0; var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; this.startingHandRotation = Controller.getPoseValue(controllerHandInput).rotation; if (this.triggerSmoothedSqueezed()) { - this.setState(STATE_SEARCHING); - } else if (this.secondarySqueezed()) { - this.setState(STATE_HOLD_SEARCHING); + this.setState(STATE_SEARCHING, "trigger squeeze detected"); } } }; + this.createHotspots = function () { + var props, overlay; + + var HAND_EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; + var HAND_EQUIP_SPHERE_ALPHA = 0.7; + var HAND_EQUIP_SPHERE_RADIUS = 0.01; + + var HAND_GRAB_SPHERE_COLOR = { red: 90, green: 90, blue: 255 }; + var HAND_GRAB_SPHERE_ALPHA = 0.3; + var HAND_GRAB_SPHERE_RADIUS = NEAR_GRAB_RADIUS; + + var EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 }; + var EQUIP_SPHERE_ALPHA = 0.3; + + var GRAB_BOX_COLOR = { red: 90, green: 90, blue: 255 }; + var GRAB_BOX_ALPHA = 0.1; + + this.hotspotOverlays = []; + + if (DRAW_HAND_SPHERES) { + // add tiny green sphere around the palm. + var handPosition = this.getHandPosition(); + overlay = Overlays.addOverlay("sphere", { + position: handPosition, + size: HAND_EQUIP_SPHERE_RADIUS * 2, + color: HAND_EQUIP_SPHERE_COLOR, + alpha: HAND_EQUIP_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); + + this.hotspotOverlays.push({ + entityID: undefined, + overlay: overlay, + type: "hand" + }); + + // add larger blue sphere around the palm. + overlay = Overlays.addOverlay("sphere", { + position: handPosition, + size: HAND_GRAB_SPHERE_RADIUS * 2, + color: HAND_GRAB_SPHERE_COLOR, + alpha: HAND_GRAB_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); + + this.hotspotOverlays.push({ + entityID: undefined, + overlay: overlay, + type: "hand" + }); + } + + // find entities near the avatar that might be equipable. + this.entityPropertyCache.clear(); + this.entityPropertyCache.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); + + var _this = this; + this.entityPropertyCache.getEntities().forEach(function (entityID) { + if (_this.entityIsEquippableWithoutDistanceCheck(entityID)) { + props = _this.entityPropertyCache.getProps(entityID); + + overlay = Overlays.addOverlay("sphere", { + rotation: props.rotation, + position: props.position, + size: EQUIP_RADIUS * 2, + color: EQUIP_SPHERE_COLOR, + alpha: EQUIP_SPHERE_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); + + _this.hotspotOverlays.push({ + entityID: entityID, + overlay: overlay, + type: "equip" + }); + } + + if (DRAW_GRAB_BOXES && _this.entityIsGrabbable(entityID)) { + props = _this.entityPropertyCache.getProps(entityID); + + overlay = Overlays.addOverlay("cube", { + rotation: props.rotation, + position: props.position, + size: props.dimensions, //{x: props.dimensions.x, y: props.dimensions.y, z: props.dimensions.z}, + color: GRAB_BOX_COLOR, + alpha: GRAB_BOX_ALPHA, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false + }); + + _this.hotspotOverlays.push({ + entityID: entityID, + overlay: overlay, + type: "near" + }); + } + }); + }; + + this.updateHotspots = function() { + var _this = this; + var props; + this.hotspotOverlays.forEach(function (overlayInfo) { + if (overlayInfo.type === "hand") { + Overlays.editOverlay(overlayInfo.overlay, { position: _this.getHandPosition() }); + } else if (overlayInfo.type === "equip") { + _this.entityPropertyCache.updateEntity(overlayInfo.entityID); + props = _this.entityPropertyCache.getProps(overlayInfo.entityID); + Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation }); + } else if (overlayInfo.type === "near") { + _this.entityPropertyCache.updateEntity(overlayInfo.entityID); + props = _this.entityPropertyCache.getProps(overlayInfo.entityID); + Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation }); + } + }); + }; + + this.destroyHotspots = function() { + this.hotspotOverlays.forEach(function (overlayInfo) { + Overlays.deleteOverlay(overlayInfo.overlay); + }); + this.hotspotOverlays = []; + }; + + this.searchEnter = function() { + this.createHotspots(); + }; + + this.searchExit = function() { + this.destroyHotspots(); + }; + + /// + // Performs ray pick test from the hand controller into the world + // @param {number} which hand to use, RIGHT_HAND or LEFT_HAND + // @returns {object} returns object with two keys entityID and distance + // + this.calcRayPickInfo = function(hand) { + + var pose = Controller.getPoseValue((hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); + var worldHandPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position); + var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation); + + var pickRay = { + origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, + direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), + Quat.getFront(Camera.orientation), + HAND_HEAD_MIX_RATIO), + length: PICK_MAX_DISTANCE + }; + + var result = { + entityID: null, + searchRay: pickRay, + distance: PICK_MAX_DISTANCE + }; + + var now = Date.now(); + if (now - this.lastPickTime < MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { + return result; + } + this.lastPickTime = now; + + var directionNormalized = Vec3.normalize(pickRay.direction); + var directionBacked = Vec3.multiply(directionNormalized, PICK_BACKOFF_DISTANCE); + var pickRayBacked = { + origin: Vec3.subtract(pickRay.origin, directionBacked), + direction: pickRay.direction + }; + + var intersection; + if (USE_BLACKLIST === true && blacklist.length !== 0) { + intersection = Entities.findRayIntersection(pickRayBacked, true, [], blacklist); + } else { + intersection = Entities.findRayIntersection(pickRayBacked, true); + } + + var overlayIntersection = Overlays.findRayIntersection(pickRayBacked); + if (!intersection.intersects || (overlayIntersection.intersects && (intersection.distance > overlayIntersection.distance))) { + intersection = overlayIntersection; + } + + if (intersection.intersects) { + return { entityID: intersection.entityID, + searchRay: pickRay, + distance: Vec3.distance(pickRay.origin, intersection.intersection) } + } else { + return result; + } + }; + + this.entityWantsTrigger = function (entityID) { + var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); + return grabbableProps && grabbableProps.wantsTrigger; + }; + + this.entityIsEquippableWithoutDistanceCheck = function (entityID) { + var props = this.entityPropertyCache.getProps(entityID); + var handPosition = props.position; + return this.entityIsEquippableWithDistanceCheck(entityID, handPosition); + }; + + this.entityIsEquippableWithDistanceCheck = function (entityID, handPosition) { + var props = this.entityPropertyCache.getProps(entityID); + var distance = Vec3.distance(props.position, handPosition); + var grabProps = this.entityPropertyCache.getGrabProps(entityID); + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; + if (refCount > 0) { + if (debug) { + print("equip is skipping '" + props.name + "': it is already grabbed"); + } + return false; + } + + if (distance > EQUIP_RADIUS) { + if (debug) { + print("equip is skipping '" + props.name + "': too far away, " + distance + " meters"); + } + return false; + } + + var wearableProps = this.entityPropertyCache.getWearableProps(entityID); + if (!wearableProps || !wearableProps.joints) { + if (debug) { + print("equip is skipping '" + props.name + "': no wearable attach-point"); + } + return false; + } + + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (!wearableProps.joints[handJointName]) { + if (debug) { + print("equip is skipping '" + props.name + "': no wearable joint for " + handJointName); + } + return false; + } + + return true; + }; + + this.entityIsGrabbable = function (entityID) { + var grabbableProps = this.entityPropertyCache.getGrabbableProps(entityID); + var grabProps = this.entityPropertyCache.getGrabProps(entityID); + var props = this.entityPropertyCache.getProps(entityID); + var physical = propsArePhysical(props); + var grabbable = false; + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + if (physical) { + // physical things default to grabbable + grabbable = true; + } else { + // non-physical things default to non-grabbable unless they are already grabbed + if ("refCount" in grabProps && grabProps.refCount > 0) { + grabbable = true; + } else { + grabbable = false; + } + } + + if (grabbableProps.hasOwnProperty("grabbable")) { + grabbable = grabbableProps.grabbable; + } + + if (!grabbable && !grabbableProps.wantsTrigger) { + if (debug) { + print("grab is skipping '" + props.name + "': not grabbable."); + } + return false; + } + if (FORBIDDEN_GRAB_TYPES.indexOf(props.type) >= 0) { + if (debug) { + print("grab is skipping '" + props.name + "': forbidden entity type."); + } + return false; + } + if (props.locked && !grabbableProps.wantsTrigger) { + if (debug) { + print("grab is skipping '" + props.name + "': locked and not triggerable."); + } + return false; + } + if (FORBIDDEN_GRAB_NAMES.indexOf(props.name) >= 0) { + if (debug) { + print("grab is skipping '" + props.name + "': forbidden name."); + } + return false; + } + + return true; + }; + + this.entityIsDistanceGrabbable = function(entityID, handPosition) { + if (!this.entityIsGrabbable(entityID)) { + return false; + } + + var props = this.entityPropertyCache.getProps(entityID); + var distance = Vec3.distance(props.position, handPosition); + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + // we can't distance-grab non-physical + var isPhysical = propsArePhysical(props); + if (!isPhysical) { + if (debug) { + print("distance grab is skipping '" + props.name + "': not physical"); + } + return false; + } + + if (distance > PICK_MAX_DISTANCE) { + // too far away, don't grab + if (debug) { + print("distance grab is skipping '" + props.name + "': too far away."); + } + return false; + } + + if (entityIsGrabbedByOther(entityID)) { + // don't distance grab something that is already grabbed. + if (debug) { + print("distance grab is skipping '" + props.name + "': already grabbed by another."); + } + return false; + } + + return true; + }; + + this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) { + + if (!this.entityIsGrabbable(entityID)) { + return false; + } + + var props = this.entityPropertyCache.getProps(entityID); + var distance = Vec3.distance(props.position, handPosition); + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + + if (distance > maxDistance) { + // too far away, don't grab + if (debug) { + print(" grab is skipping '" + props.name + "': too far away."); + } + return false; + } + + return true; + }; + this.search = function() { + var _this = this; + var name; + + this.updateHotspots(); + this.grabbedEntity = null; this.isInitialGrab = false; this.shouldResetParentOnRelease = false; this.checkForStrayChildren(); - if (this.state == STATE_SEARCHING && this.triggerSmoothedReleased()) { - this.setState(STATE_RELEASE); - return; - } - if (this.state == STATE_HOLD_SEARCHING && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + if (this.triggerSmoothedReleased()) { + this.setState(STATE_OFF, "trigger released"); return; } - // the trigger is being pressed, so do a ray test to see what we are hitting var handPosition = this.getHandPosition(); - if (SHOW_GRAB_SPHERE) { - this.grabSphereOn(); - } + this.entityPropertyCache.clear(); + this.entityPropertyCache.findEntities(handPosition, NEAR_GRAB_RADIUS); + var candidateEntities = this.entityPropertyCache.getEntities(); - var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; - var currentHandRotation = Controller.getPoseValue(controllerHandInput).rotation; - var currentControllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, - Controller.getPoseValue(controllerHandInput).translation), - MyAvatar.position); - var handDeltaRotation = Quat.multiply(currentHandRotation, Quat.inverse(this.startingHandRotation)); + var equippableEntities = candidateEntities.filter(function (entity) { + return _this.entityIsEquippableWithDistanceCheck(entity, handPosition); + }); - var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? - Controller.Standard.RightHand : Controller.Standard.LeftHand); - var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation); - - var distantPickRay = { - origin: PICK_WITH_HAND_RAY ? currentControllerPosition : Camera.position, - direction: PICK_WITH_HAND_RAY ? Quat.getUp(controllerRotation) : Vec3.mix(Quat.getUp(controllerRotation), - Quat.getFront(Camera.orientation), - HAND_HEAD_MIX_RATIO), - length: PICK_MAX_DISTANCE - }; - - // Pick at some maximum rate, not always - var pickRays = []; - var now = Date.now(); - if (now - this.lastPickTime > MSECS_PER_SEC / PICKS_PER_SECOND_PER_HAND) { - pickRays = [distantPickRay]; - this.lastPickTime = now; - } - - rayPickedCandidateEntities = []; // the list of candidates to consider grabbing - - this.intersectionDistance = 0.0; - for (var index = 0; index < pickRays.length; ++index) { - var pickRay = pickRays[index]; - var directionNormalized = Vec3.normalize(pickRay.direction); - var directionBacked = Vec3.multiply(directionNormalized, PICK_BACKOFF_DISTANCE); - var pickRayBacked = { - origin: Vec3.subtract(pickRay.origin, directionBacked), - direction: pickRay.direction - }; - - var intersection; - - if (USE_BLACKLIST === true && blacklist.length !== 0) { - intersection = Entities.findRayIntersection(pickRayBacked, true, [], blacklist); - } else { - intersection = Entities.findRayIntersection(pickRayBacked, true); - } - - if (intersection.intersects) { - rayPickedCandidateEntities.push(intersection.entityID); - this.intersectionDistance = Vec3.distance(pickRay.origin, intersection.intersection); - } - } - - nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); - candidateEntities = rayPickedCandidateEntities.concat(nearPickedCandidateEntities); - - var forbiddenNames = ["Grab Debug Entity", "grab pointer"]; - var forbiddenTypes = ['Unknown', 'Light', 'PolyLine', 'Zone']; - - var minDistance = PICK_MAX_DISTANCE; - var i, props, distance, grabbableData; - this.grabbedEntity = null; - for (i = 0; i < candidateEntities.length; i++) { - var grabbableDataForCandidate = - getEntityCustomData(GRABBABLE_DATA_KEY, candidateEntities[i], DEFAULT_GRABBABLE_DATA); - var grabDataForCandidate = getEntityCustomData(GRAB_USER_DATA_KEY, candidateEntities[i], {}); - var propsForCandidate = Entities.getEntityProperties(candidateEntities[i], GRABBABLE_PROPERTIES); - var near = (nearPickedCandidateEntities.indexOf(candidateEntities[i]) >= 0); - - var isPhysical = this.propsArePhysical(propsForCandidate); - var grabbable; - if (isPhysical) { - // physical things default to grabbable - grabbable = true; - } else { - // non-physical things default to non-grabbable unless they are already grabbed - if ("refCount" in grabDataForCandidate && grabDataForCandidate.refCount > 0) { - grabbable = true; - } else { - grabbable = false; - } - } - - if ("grabbable" in grabbableDataForCandidate) { - // if userData indicates that this is grabbable or not, override the default. - grabbable = grabbableDataForCandidate.grabbable; - } - - if (!grabbable && !grabbableDataForCandidate.wantsTrigger) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': not grabbable."); - } - continue; - } - if (forbiddenTypes.indexOf(propsForCandidate.type) >= 0) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': forbidden entity type."); - } - continue; - } - if (propsForCandidate.locked && !grabbableDataForCandidate.wantsTrigger) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': locked and not triggerable."); - } - continue; - } - if (forbiddenNames.indexOf(propsForCandidate.name) >= 0) { - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': forbidden name."); - } - continue; - } - - distance = Vec3.distance(propsForCandidate.position, handPosition); - if (distance > PICK_MAX_DISTANCE) { - // too far away, don't grab - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': too far away."); - } - continue; - } - if (propsForCandidate.parentID != NULL_UUID && this.state == STATE_HOLD_SEARCHING) { - // don't allow a double-equip - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': it's a child"); - } - continue; - } - - if (this.state == STATE_SEARCHING && - !isPhysical && distance > NEAR_PICK_MAX_DISTANCE && !near && !grabbableDataForCandidate.wantsTrigger) { - // we can't distance-grab non-physical - if (WANT_DEBUG_SEARCH_NAME && propsForCandidate.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': not physical and too far for near-grab"); - } - continue; - } - - if (distance < minDistance) { - this.grabbedEntity = candidateEntities[i]; - minDistance = distance; - props = propsForCandidate; - grabbableData = grabbableDataForCandidate; - } - } - if ((this.grabbedEntity !== null) && (this.triggerSmoothedGrab() || this.secondarySqueezed())) { - // We are squeezing enough to grab, and we've found an entity that we'll try to do something with. - var near = (nearPickedCandidateEntities.indexOf(this.grabbedEntity) >= 0) || minDistance <= NEAR_PICK_MAX_DISTANCE; - var isPhysical = this.propsArePhysical(props); - - // near or far trigger - if (grabbableData.wantsTrigger) { - this.setState(near ? STATE_NEAR_TRIGGER : STATE_FAR_TRIGGER); + var entity; + if (equippableEntities.length > 0) { + // sort by distance + equippableEntities.sort(function (a, b) { + var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition); + var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition); + return aDistance - bDistance; + }); + entity = equippableEntities[0]; + if (this.triggerSmoothedGrab()) { + this.grabbedEntity = entity; + this.setState(STATE_HOLD, "eqipping '" + this.entityPropertyCache.getProps(entity).name + "'"); return; + } else { + // TODO: highlight the equippable object? } - // near grab with action or equip - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); - var refCount = ("refCount" in grabData) ? grabData.refCount : 0; - if (near && (refCount < 1 || entityHasActions(this.grabbedEntity))) { - if (this.state == STATE_SEARCHING) { - this.setState(STATE_NEAR_GRABBING); - } else { // (this.state == STATE_HOLD_SEARCHING) - // if there was already an action, we'll need to set the parent back to null once we release - this.shouldResetParentOnRelease = true; - this.previousParentID = props.parentID; - this.previousParentJointIndex = props.parentJointIndex; - this.setState(STATE_HOLD); - } - return; + } + + var grabbableEntities = candidateEntities.filter(function (entity) { + return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_MAX_DISTANCE); + }); + + var rayPickInfo = this.calcRayPickInfo(this.hand); + if (rayPickInfo.entityID) { + this.intersectionDistance = rayPickInfo.distance; + this.entityPropertyCache.updateEntity(rayPickInfo.entityID); + if (this.entityIsGrabbable(rayPickInfo.entityID) && rayPickInfo.distance < NEAR_GRAB_PICK_RADIUS) { + grabbableEntities.push(rayPickInfo.entityID); } - // far grab - if (isPhysical && !near) { - if (entityIsGrabbedByOther(this.grabbedEntity)) { - // don't distance grab something that is already grabbed. - if (WANT_DEBUG_SEARCH_NAME && props.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': already grabbed by another."); - } + } else { + this.intersectionDistance = 0; + } + + if (grabbableEntities.length > 0) { + // sort by distance + grabbableEntities.sort(function (a, b) { + var aDistance = Vec3.distance(_this.entityPropertyCache.getProps(a).position, handPosition); + var bDistance = Vec3.distance(_this.entityPropertyCache.getProps(b).position, handPosition); + return aDistance - bDistance; + }); + entity = grabbableEntities[0]; + name = this.entityPropertyCache.getProps(entity).name; + this.grabbedEntity = entity; + if (this.entityWantsTrigger(entity)) { + if (this.triggerSmoothedGrab()) { + this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'"); return; + } else { + // TODO: highlight the near-triggerable object? } - this.temporaryPositionOffset = null; - if (!this.hasPresetOffsets()) { - // We want to give a temporary position offset to this object so it is pulled close to hand - var intersectionPointToCenterDistance = Vec3.length(Vec3.subtract(intersection.intersection, - intersection.properties.position)); - var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - var handJointPosition = MyAvatar.getJointPosition(handJointIndex); - this.temporaryPositionOffset = - Vec3.normalize(Vec3.subtract(intersection.properties.position, handJointPosition)); - this.temporaryPositionOffset = Vec3.multiply(this.temporaryPositionOffset, - intersectionPointToCenterDistance * - FAR_TO_NEAR_GRAB_PADDING_FACTOR); - } - this.setState(STATE_DISTANCE_HOLDING); - - this.searchSphereOff(); - return; - } - - // else this thing isn't physical. grab it by reparenting it (but not if we've already - // grabbed it). - if (refCount < 1) { - if (this.state == STATE_SEARCHING) { - this.setState(STATE_NEAR_GRABBING); - } else { // this.state == STATE_HOLD_SEARCHING) - this.setState(STATE_HOLD); - } - return; } else { - // it's not physical and it's already held via parenting. go ahead and grab it, but - // save off the current parent and joint. this wont always be right if there are more than - // two grabs and the order of release isn't opposite of the order of grabs. - this.shouldResetParentOnRelease = true; - this.previousParentID = props.parentID; - this.previousParentJointIndex = props.parentJointIndex; - if (this.state == STATE_SEARCHING) { - this.setState(STATE_NEAR_GRABBING); - } else { // (this.state == STATE_HOLD_SEARCHING) - this.setState(STATE_HOLD); + if (this.triggerSmoothedGrab()) { + + var props = this.entityPropertyCache.getProps(entity); + var grabProps = this.entityPropertyCache.getGrabProps(entity); + var refCount = grabProps.refCount ? grabProps.refCount : 0; + if (refCount >= 1) { + // if another person is holding the object, remember to restore the + // parent info, when we are finished grabbing it. + this.shouldResetParentOnRelease = true; + this.previousParentID = props.parentID; + this.previousParentJointIndex = props.parentJointIndex; + } + + this.setState(STATE_NEAR_GRABBING, "near grab '" + name + "'"); + return; + } else { + // TODO: highlight the grabbable object? } - return; } - if (WANT_DEBUG_SEARCH_NAME && props.name == WANT_DEBUG_SEARCH_NAME) { - print("grab is skipping '" + WANT_DEBUG_SEARCH_NAME + "': fell through."); + return; + } + + if (rayPickInfo.entityID) { + entity = rayPickInfo.entityID; + name = this.entityPropertyCache.getProps(entity).name; + if (this.entityWantsTrigger(entity)) { + if (this.triggerSmoothedGrab()) { + this.grabbedEntity = entity; + this.setState(STATE_FAR_TRIGGER, "far trigger '" + name + "'"); + return; + } else { + // TODO: highlight the far-triggerable object? + } + } else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) { + if (this.triggerSmoothedGrab()) { + this.grabbedEntity = entity; + this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); + return; + } else { + // TODO: highlight the far-grabbable object? + } } } //search line visualizations if (USE_ENTITY_LINES_FOR_SEARCHING === true) { - this.lineOn(distantPickRay.origin, Vec3.multiply(distantPickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); + this.lineOn(rayPickInfo.searchRay.origin, Vec3.multiply(rayPickInfo.searchRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR); } - this.searchIndicatorOn(distantPickRay); + this.searchIndicatorOn(rayPickInfo.searchRay); Reticle.setVisible(false); - }; this.distanceGrabTimescale = function(mass, distance) { @@ -1105,7 +1377,7 @@ function MyController(hand) { return (dimensions.x * dimensions.y * dimensions.z) * density; } - this.distanceHolding = function() { + this.distanceHoldingEnter = function() { // controller pose is in avatar frame var avatarControllerPose = @@ -1154,7 +1426,6 @@ function MyController(hand) { this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); if (this.actionID !== null) { - this.setState(STATE_CONTINUE_DISTANCE_HOLDING); this.activateEntity(this.grabbedEntity, grabbedProperties, false); this.callEntityMethodOnGrabbed("startDistanceGrab"); } @@ -1165,10 +1436,10 @@ function MyController(hand) { this.previousControllerRotation = controllerRotation; }; - this.continueDistanceHolding = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + this.distanceHolding = function() { + if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("releaseGrab"); + this.setState(STATE_OFF, "trigger released"); return; } @@ -1186,7 +1457,7 @@ function MyController(hand) { var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); var now = Date.now(); - var deltaTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds + var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds this.currentObjectTime = now; // the action was set up when this.distanceHolding was called. update the targets. @@ -1200,11 +1471,12 @@ function MyController(hand) { var handMoved = Vec3.multiply(Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar), radius); - // double delta controller rotation - var handChange = Quat.multiply(Quat.slerp(this.previousControllerRotation, - controllerRotation, - DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), - Quat.inverse(this.previousControllerRotation)); + /// double delta controller rotation + // var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did + // var handChange = Quat.multiply(Quat.slerp(this.previousControllerRotation, + // controllerRotation, + // DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR), + // Quat.inverse(this.previousControllerRotation)); // update the currentObject position and rotation. this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved); @@ -1220,17 +1492,17 @@ function MyController(hand) { // Update radialVelocity var lastVelocity = Vec3.subtract(controllerPositionVSAvatar, this.previousControllerPositionVSAvatar); - lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaTime); + lastVelocity = Vec3.multiply(lastVelocity, 1.0 / deltaObjectTime); var newRadialVelocity = Vec3.dot(lastVelocity, Vec3.normalize(Vec3.subtract(grabbedProperties.position, controllerPosition))); var VELOCITY_AVERAGING_TIME = 0.016; - this.grabRadialVelocity = (deltaTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity + - (1.0 - (deltaTime / VELOCITY_AVERAGING_TIME)) * this.grabRadialVelocity; + this.grabRadialVelocity = (deltaObjectTime / VELOCITY_AVERAGING_TIME) * newRadialVelocity + + (1.0 - (deltaObjectTime / VELOCITY_AVERAGING_TIME)) * this.grabRadialVelocity; var RADIAL_GRAB_AMPLIFIER = 10.0; if (Math.abs(this.grabRadialVelocity) > 0.0) { - this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaTime * this.grabRadius * RADIAL_GRAB_AMPLIFIER); + this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * this.grabRadius * RADIAL_GRAB_AMPLIFIER); } var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(controllerRotation)); @@ -1258,26 +1530,26 @@ function MyController(hand) { } } - var defaultConstraintData = { - axisStart: false, - axisEnd: false, - } - - var constraintData = getEntityCustomData('lightModifierKey', this.grabbedEntity, defaultConstraintData); - var clampedVector; - var targetPosition; - if (constraintData.axisStart !== false) { - clampedVector = this.projectVectorAlongAxis(this.currentObjectPosition, - constraintData.axisStart, - constraintData.axisEnd); - targetPosition = clampedVector; - } else { - targetPosition = { - x: this.currentObjectPosition.x, - y: this.currentObjectPosition.y, - z: this.currentObjectPosition.z - } - } + // var defaultConstraintData = { + // axisStart: false, + // axisEnd: false + // } + // + // var constraintData = getEntityCustomData('lightModifierKey', this.grabbedEntity, defaultConstraintData); + // var clampedVector; + // var targetPosition; + // if (constraintData.axisStart !== false) { + // clampedVector = this.projectVectorAlongAxis(this.currentObjectPosition, + // constraintData.axisStart, + // constraintData.axisEnd); + // targetPosition = clampedVector; + // } else { + // targetPosition = { + // x: this.currentObjectPosition.x, + // y: this.currentObjectPosition.y, + // z: this.currentObjectPosition.z + // } + // } var handPosition = this.getHandPosition(); @@ -1382,23 +1654,56 @@ function MyController(hand) { } } - this.nearGrabbing = function() { - var now = Date.now(); + this.dropGestureReset = function() { + this.fastHandMoveDetected = false; + this.fastHandMoveTimer = 0; + }; - if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) { - this.setState(STATE_RELEASE); - this.callEntityMethodOnGrabbed("releaseGrab"); - return; + this.dropGestureProcess = function(deltaTime) { + var pose = Controller.getPoseValue((this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand); + var worldHandVelocity = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity); + var worldHandRotation = Quat.multiply(MyAvatar.orientation, pose.rotation); + + if (this.fastHandMoveDetected) { + this.fastHandMoveTimer -= deltaTime; } - if (this.state == STATE_HOLD && this.secondaryReleased()) { - this.setState(STATE_RELEASE); - this.callEntityMethodOnGrabbed("releaseGrab"); - return; + if (this.fastHandMoveTimer < 0) { + this.fastHandMoveDetected = false; } + var FAST_HAND_SPEED_REST_TIME = 1; // sec + var FAST_HAND_SPEED_THRESHOLD = 0.4; // m/sec + if (Vec3.length(worldHandVelocity) > FAST_HAND_SPEED_THRESHOLD) { + this.fastHandMoveDetected = true; + this.fastHandMoveTimer = FAST_HAND_SPEED_REST_TIME; + } + + var localHandUpAxis = this.hand === RIGHT_HAND ? {x: 1, y: 0, z: 0} : {x: -1, y: 0, z: 0}; + var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis); + var DOWN = {x: 0, y: -1, z: 0}; + var ROTATION_THRESHOLD = Math.cos(Math.PI / 8); + + var handIsUpsideDown = false; + if (Vec3.dot(worldHandUpAxis, DOWN) > ROTATION_THRESHOLD) { + handIsUpsideDown = true; + } + + var WANT_DEBUG = false; + if (WANT_DEBUG) { + print("zAxis = " + worldHandUpAxis.x + ", " + worldHandUpAxis.y + ", " + worldHandUpAxis.z); + print("dot = " + Vec3.dot(worldHandUpAxis, DOWN) + ", ROTATION_THRESHOLD = " + ROTATION_THRESHOLD); + print("handMove = " + this.fastHandMoveDetected + ", handIsUpsideDown = " + handIsUpsideDown); + } + + return (DROP_WITHOUT_SHAKE || this.fastHandMoveDetected) && handIsUpsideDown; + }; + + this.nearGrabbingEnter = function() { this.lineOff(); this.overlayLineOff(); + this.dropGestureReset(); + if (this.entityActivated) { var saveGrabbedID = this.grabbedEntity; this.release(); @@ -1413,7 +1718,7 @@ function MyController(hand) { var handPosition = this.getHandPosition(); var hasPresetPosition = false; - if ((this.state == STATE_EQUIP || this.state == STATE_HOLD) && this.hasPresetOffsets()) { + if (this.state == STATE_HOLD && this.hasPresetOffsets()) { var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); // if an object is "equipped" and has a predefined offset, use it. this.ignoreIK = grabbableData.ignoreIK ? grabbableData.ignoreIK : false; @@ -1429,13 +1734,13 @@ function MyController(hand) { var currentObjectPosition = grabbedProperties.position; var offset = Vec3.subtract(currentObjectPosition, handPosition); this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset); - if (this.temporaryPositionOffset && (this.state == STATE_EQUIP)) { + if (this.temporaryPositionOffset) { this.offsetPosition = this.temporaryPositionOffset; // hasPresetPosition = true; } } - var isPhysical = this.propsArePhysical(grabbedProperties) || entityHasActions(this.grabbedEntity); + var isPhysical = propsArePhysical(grabbedProperties) || entityHasActions(this.grabbedEntity); if (isPhysical && this.state == STATE_NEAR_GRABBING) { // grab entity via action if (!this.setupHoldAction()) { @@ -1449,7 +1754,7 @@ function MyController(hand) { // grab entity via parenting this.actionID = null; var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - reparentProps = { + var reparentProps = { parentID: MyAvatar.sessionUUID, parentJointIndex: handJointIndex } @@ -1473,21 +1778,10 @@ function MyController(hand) { if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("startNearGrab"); - } else { // this.state == STATE_EQUIP || this.state == STATE_HOLD + } else { // this.state == STATE_HOLD this.callEntityMethodOnGrabbed("startEquip"); } - if (this.state == STATE_NEAR_GRABBING) { - // near grabbing - this.setState(STATE_CONTINUE_NEAR_GRABBING); - } else if (this.state == STATE_HOLD) { - // holding - this.setState(STATE_CONTINUE_HOLD); - } else { // (this.state == STATE_EQUIP) - // equipping - this.setState(STATE_CONTINUE_EQUIP); - } - this.currentHandControllerTipPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition; this.currentObjectTime = Date.now(); @@ -1498,31 +1792,28 @@ function MyController(hand) { this.currentAngularVelocity = ZERO_VEC; }; - this.continueNearGrabbing = function() { - if (this.state == STATE_CONTINUE_NEAR_GRABBING && this.triggerSmoothedReleased()) { - this.setState(STATE_RELEASE); + this.nearGrabbing = function(deltaTime) { + + var dropDetected = this.dropGestureProcess(deltaTime); + + if (this.state == STATE_NEAR_GRABBING && this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("releaseGrab"); + this.setState(STATE_OFF, "trigger released"); return; } - if (this.state == STATE_CONTINUE_HOLD && this.secondaryReleased()) { - this.setState(STATE_RELEASE); - this.callEntityMethodOnGrabbed("releaseEquip"); - return; - } - if (this.state == STATE_CONTINUE_EQUIP && this.thumbPressed()) { - this.setState(STATE_WAITING_FOR_RELEASE_THUMB_RELEASE); - this.callEntityMethodOnGrabbed("releaseEquip"); - return; - } - if (this.state == STATE_CONTINUE_NEAR_GRABBING && this.thumbPressed()) { - this.setState(STATE_WAITING_FOR_EQUIP_THUMB_RELEASE); - this.callEntityMethodOnGrabbed("releaseGrab"); - this.callEntityMethodOnGrabbed("startEquip"); - return; - } - if (this.state == STATE_CONTINUE_HOLD && this.thumbPressed()) { - this.setState(STATE_WAITING_FOR_EQUIP_THUMB_RELEASE); - return; + + if (this.state == STATE_HOLD) { + if (dropDetected && this.triggerSmoothedGrab()) { + this.callEntityMethodOnGrabbed("releaseEquip"); + this.setState(STATE_OFF, "drop gesture detected"); + return; + } + + if (this.thumbPressed()) { + this.callEntityMethodOnGrabbed("releaseEquip"); + this.setState(STATE_OFF, "drop via thumb press"); + return; + } } this.heartBeat(this.grabbedEntity); @@ -1530,8 +1821,8 @@ function MyController(hand) { var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position", "rotation"]); if (!props.position) { // server may have reset, taking our equipped entity with it. move back to "off" stte - this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("releaseGrab"); + this.setState(STATE_OFF, "entity has no position property"); return; } @@ -1541,21 +1832,22 @@ function MyController(hand) { this.lastUnequipCheckTime = now; if (props.parentID == MyAvatar.sessionUUID && - Vec3.length(props.localPosition) > NEAR_PICK_MAX_DISTANCE * 2.0) { + Vec3.length(props.localPosition) > NEAR_GRAB_MAX_DISTANCE) { var handPosition = this.getHandPosition(); // the center of the equipped object being far from the hand isn't enough to autoequip -- we also // need to fail the findEntities test. - nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); + var nearPickedCandidateEntities = Entities.findEntities(handPosition, NEAR_GRAB_RADIUS); if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + props.parentID + " " + vec3toStr(props.position)); - this.setState(STATE_RELEASE); - if (this.state == STATE_CONTINUE_NEAR_GRABBING) { + + if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("releaseGrab"); - } else { // (this.state == STATE_CONTINUE_EQUIP || this.state == STATE_CONTINUE_HOLD) + } else { // this.state == STATE_HOLD this.callEntityMethodOnGrabbed("releaseEquip"); } + this.setState(STATE_OFF, "held object too far away"); return; } } @@ -1569,20 +1861,18 @@ function MyController(hand) { // from the palm. var handControllerPosition = (this.hand === RIGHT_HAND) ? MyAvatar.rightHandPosition : MyAvatar.leftHandPosition; - var now = Date.now(); - var deltaPosition = Vec3.subtract(handControllerPosition, this.currentHandControllerTipPosition); // meters - var deltaTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds + var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds - if (deltaTime > 0.0) { + if (deltaObjectTime > 0.0) { var worldDeltaPosition = Vec3.subtract(props.position, this.currentObjectPosition); var previousEulers = Quat.safeEulerAngles(this.currentObjectRotation); var newEulers = Quat.safeEulerAngles(props.rotation); var worldDeltaRotation = Vec3.subtract(newEulers, previousEulers); - this.currentVelocity = Vec3.multiply(worldDeltaPosition, 1.0 / deltaTime); - this.currentAngularVelocity = Vec3.multiply(worldDeltaRotation, Math.PI / (deltaTime * 180.0)); + this.currentVelocity = Vec3.multiply(worldDeltaPosition, 1.0 / deltaObjectTime); + this.currentAngularVelocity = Vec3.multiply(worldDeltaRotation, Math.PI / (deltaObjectTime * 180.0)); this.currentObjectPosition = props.position; this.currentObjectRotation = props.rotation; @@ -1591,11 +1881,10 @@ function MyController(hand) { this.currentHandControllerTipPosition = handControllerPosition; this.currentObjectTime = now; - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); - if (this.state === STATE_CONTINUE_EQUIP || this.state === STATE_CONTINUE_HOLD) { + if (this.state === STATE_HOLD) { this.callEntityMethodOnGrabbed("continueEquip"); } - if (this.state == STATE_CONTINUE_NEAR_GRABBING) { + if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("continueNearGrab"); } @@ -1621,50 +1910,27 @@ function MyController(hand) { } }; - this.waitingForEquipThumbRelease = function() { - if (this.thumbReleased() && this.triggerSmoothedReleased()) { - this.setState(STATE_EQUIP); - } + this.nearTriggerEnter = function() { + this.callEntityMethodOnGrabbed("startNearTrigger"); }; - this.waitingForReleaseThumbRelease = function() { - if (this.thumbReleased() && this.triggerSmoothedReleased()) { - this.setState(STATE_RELEASE); - } + + this.farTriggerEnter = function() { + this.callEntityMethodOnGrabbed("startFarTrigger"); }; this.nearTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); - this.callEntityMethodOnGrabbed("stopNearTrigger"); - return; - } - this.callEntityMethodOnGrabbed("startNearTrigger"); - this.setState(STATE_CONTINUE_NEAR_TRIGGER); - }; - - this.farTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); - this.callEntityMethodOnGrabbed("stopFarTrigger"); - return; - } - this.callEntityMethodOnGrabbed("startFarTrigger"); - this.setState(STATE_CONTINUE_FAR_TRIGGER); - }; - - this.continueNearTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopNearTrigger"); + this.setState(STATE_OFF, "trigger released"); return; } this.callEntityMethodOnGrabbed("continueNearTrigger"); }; - this.continueFarTrigger = function() { - if (this.triggerSmoothedReleased() && this.secondaryReleased()) { - this.setState(STATE_RELEASE); + this.farTrigger = function() { + if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopFarTrigger"); + this.setState(STATE_OFF, "trigger released"); return; } @@ -1680,8 +1946,8 @@ function MyController(hand) { if (intersection.accurate) { this.lastPickTime = now; if (intersection.entityID != this.grabbedEntity) { - this.setState(STATE_RELEASE); this.callEntityMethodOnGrabbed("stopFarTrigger"); + this.setState(STATE_OFF, "laser moved off of entity"); return; } if (intersection.intersects) { @@ -1694,6 +1960,10 @@ function MyController(hand) { this.callEntityMethodOnGrabbed("continueFarTrigger"); }; + this.offEnter = function() { + this.release(); + }; + this.release = function() { this.turnLightsOff(); this.turnOffVisualizations(); @@ -1717,7 +1987,6 @@ function MyController(hand) { this.deactivateEntity(this.grabbedEntity, noVelocity); this.actionID = null; - this.setState(STATE_OFF); Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'release', @@ -1726,6 +1995,10 @@ function MyController(hand) { })); this.grabbedEntity = null; + + if (this.triggerSmoothedGrab()) { + this.waitForTriggerRelease = true; + } }; this.cleanup = function() { @@ -1827,6 +2100,8 @@ function MyController(hand) { } this.deactivateEntity = function(entityID, noVelocity) { + var deactiveProps; + if (!this.entityActivated) { return; } @@ -1836,7 +2111,7 @@ function MyController(hand) { if (data && data["refCount"]) { data["refCount"] = data["refCount"] - 1; if (data["refCount"] < 1) { - var deactiveProps = { + deactiveProps = { gravity: data["gravity"], collidesWith: data["collidesWith"], collisionless: data["collisionless"], @@ -1849,10 +2124,9 @@ function MyController(hand) { // it looks like the dropped thing should fall, give it a little velocity. var props = Entities.getEntityProperties(entityID, ["parentID", "velocity", "dynamic", "shapeType"]) var parentID = props.parentID; - var forceVelocity = false; var doSetVelocity = false; - if (parentID != NULL_UUID && deactiveProps.parentID == NULL_UUID && this.propsArePhysical(props)) { + if (parentID != NULL_UUID && deactiveProps.parentID == NULL_UUID && propsArePhysical(props)) { // TODO: EntityScriptingInterface::convertLocationToScriptSemantics should be setting up // props.velocity to be a world-frame velocity and localVelocity to be vs parent. Until that // is done, we use a measured velocity here so that things held via a bumper-grab / parenting-grab @@ -1884,7 +2158,7 @@ function MyController(hand) { // the parent causes it to go off in the wrong direction. This is a bug that should // be fixed. Entities.editEntity(entityID, { - velocity: this.currentVelocity, + velocity: this.currentVelocity // angularVelocity: this.currentAngularVelocity }); } @@ -1892,7 +2166,7 @@ function MyController(hand) { data = null; } else if (this.shouldResetParentOnRelease) { // we parent-grabbed this from another parent grab. try to put it back where we found it. - var deactiveProps = { + deactiveProps = { parentID: this.previousParentID, parentJointIndex: this.previousParentJointIndex, velocity: {x: 0.0, y: 0.0, z: 0.0}, @@ -1909,29 +2183,7 @@ function MyController(hand) { } setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); }; - - this.checkNewlyLoaded = function(loadedEntityID) { - if (this.state == STATE_OFF || - this.state == STATE_SEARCHING || - this.state == STATE_HOLD_SEARCHING) { - var loadedProps = Entities.getEntityProperties(loadedEntityID); - if (loadedProps.parentID != MyAvatar.sessionUUID) { - return; - } - var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - if (loadedProps.parentJointIndex != handJointIndex) { - return; - } - print("--- handControllerGrab found loaded entity ---"); - // an entity has been loaded and it's where this script would have equipped something, so switch states. - this.grabbedEntity = loadedEntityID; - this.activateEntity(this.grabbedEntity, loadedProps, true); - this.isInitialGrab = true; - this.callEntityMethodOnGrabbed("startEquip"); - this.setState(STATE_CONTINUE_EQUIP); - } - } -}; +} var rightController = new MyController(RIGHT_HAND); var leftController = new MyController(LEFT_HAND); @@ -1955,12 +2207,12 @@ Controller.enableMapping(MAPPING_NAME); //the section below allows the grab script to listen for messages that disable either one or both hands. useful for two handed items var handToDisable = 'none'; -function update() { +function update(deltaTime) { if (handToDisable !== LEFT_HAND && handToDisable !== 'both') { - leftController.update(); + leftController.update(deltaTime); } if (handToDisable !== RIGHT_HAND && handToDisable !== 'both') { - rightController.update(); + rightController.update(deltaTime); } } @@ -1969,7 +2221,8 @@ Messages.subscribe('Hifi-Hand-Grab'); Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.subscribe('Hifi-Object-Manipulation'); -handleHandMessages = function(channel, message, sender) { +var handleHandMessages = function(channel, message, sender) { + var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Hand-Disabler') { if (message === 'left') { @@ -1983,17 +2236,19 @@ handleHandMessages = function(channel, message, sender) { } } else if (channel === 'Hifi-Hand-Grab') { try { - var data = JSON.parse(message); + data = JSON.parse(message); var selectedController = (data.hand === 'left') ? leftController : rightController; selectedController.release(); - selectedController.setState(STATE_EQUIP); + selectedController.setState(STATE_HOLD, "Hifi-Hand-Grab msg received"); selectedController.grabbedEntity = data.entityID; - } catch (e) {} + } catch (e) { + print("WARNING: error parsing Hifi-Hand-Grab message"); + } } else if (channel === 'Hifi-Hand-RayPick-Blacklist') { try { - var data = JSON.parse(message); + data = JSON.parse(message); var action = data.action; var id = data.id; var index = blacklist.indexOf(id); @@ -2007,23 +2262,8 @@ handleHandMessages = function(channel, message, sender) { } } - } catch (e) {} - } else if (channel === 'Hifi-Object-Manipulation') { - if (sender !== MyAvatar.sessionUUID) { - return; - } - - var parsedMessage = null; - try { - parsedMessage = JSON.parse(message); } catch (e) { - print('error parsing Hifi-Object-Manipulation message'); - return; - } - - if (parsedMessage.action === 'loaded') { - rightController.checkNewlyLoaded(parsedMessage['grabbedEntity']); - leftController.checkNewlyLoaded(parsedMessage['grabbedEntity']); + print("WARNING: error parsing Hifi-Hand-RayPick-Blacklist message"); } } } @@ -2036,6 +2276,39 @@ function cleanup() { leftController.cleanup(); Controller.disableMapping(MAPPING_NAME); Reticle.setVisible(true); + Menu.removeMenuItem("Developer > Hands", "Drop Without Shake"); } + Script.scriptEnding.connect(cleanup); -Script.update.connect(update); \ No newline at end of file +Script.update.connect(update); + +if (!Menu.menuExists("Developer > Grab Script")) { + Menu.addMenu("Developer > Grab Script"); +} + +Menu.addMenuItem({ + menuName: "Developer > Grab Script", + menuItemName: "Drop Without Shake", + isCheckable: true, + isChecked: DROP_WITHOUT_SHAKE +}); + +Menu.addMenuItem({ + menuName: "Developer > Grab Script", + menuItemName: "Draw Grab Boxes", + isCheckable: true, + isChecked: DRAW_GRAB_BOXES +}); + +function handleMenuItemEvent(menuItem) { + if (menuItem === "Drop Without Shake") { + DROP_WITHOUT_SHAKE = Menu.isOptionChecked("Drop Without Shake"); + } + if (menuItem === "Draw Grab Boxes") { + DRAW_GRAB_BOXES = Menu.isOptionChecked("Draw Grab Boxes"); + DRAW_HAND_SPHERES = DRAW_GRAB_BOXES; + } +} + +Menu.menuItemEvent.connect(handleMenuItemEvent); + diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index ca3b5e8cf2..a0f1f47b3c 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -1,6 +1,6 @@ "use strict"; /*jslint vars: true, plusplus: true*/ -/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Menu, ScriptDiscoveryService, Window, Vec3, Quat, print */ +/*globals Script, Overlays, Controller, Reticle, HMD, Camera, Entities, MyAvatar, Settings, Menu, ScriptDiscoveryService, Window, Vec3, Quat, print*/ // // handControllerPointer.js @@ -14,19 +14,16 @@ // // Control the "mouse" using hand controller. (HMD and desktop.) -// For now: -// Hydra thumb button 3 is left-mouse, button 4 is right-mouse. -// A click in the center of the vive thumb pad is left mouse. Vive menu button is context menu (right mouse). // First-person only. // Starts right handed, but switches to whichever is free: Whichever hand was NOT most recently squeezed. // (For now, the thumb buttons on both controllers are always on.) -// When over a HUD element, the reticle is shown where the active hand controller beam intersects the HUD. -// Otherwise, the active hand controller shows a red ball where a click will act. - +// When partially squeezing over a HUD element, a laser or the reticle is shown where the active hand +// controller beam intersects the HUD. // UTILITIES ------------- // +function ignore() { } // Utility to make it easier to setup and disconnect cleanly. function setupHandler(event, handler) { @@ -47,21 +44,61 @@ function TimeLock(expiration) { } var handControllerLockOut = new TimeLock(2000); -// Calls onFunction() or offFunction() when swtich(on), but only if it is to a new value. -function LatchedToggle(onFunction, offFunction, state) { - this.getState = function () { - return state; +function Trigger(label) { + // This part is copied and adapted from handControllerGrab.js. Maybe we should refactor this. + var that = this; + that.label = label; + that.TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing + that.TRIGGER_ON_VALUE = 0.4; // Squeezed just enough to activate search or near grab + that.TRIGGER_GRAB_VALUE = 0.85; // Squeezed far enough to complete distant grab + that.TRIGGER_OFF_VALUE = 0.15; + that.rawTriggerValue = 0; + that.triggerValue = 0; // rolling average of trigger value + that.triggerPress = function (value) { + that.rawTriggerValue = value; }; - this.setState = function (on) { - if (state === on) { - return; - } - state = on; - if (on) { - onFunction(); - } else { - offFunction(); + that.updateSmoothedTrigger = function () { // e.g., call once/update for effect + var triggerValue = that.rawTriggerValue; + // smooth out trigger value + that.triggerValue = (that.triggerValue * that.TRIGGER_SMOOTH_RATIO) + + (triggerValue * (1.0 - that.TRIGGER_SMOOTH_RATIO)); + }; + // Current smoothed state, without hysteresis. Answering booleans. + that.triggerSmoothedGrab = function () { + return that.triggerValue > that.TRIGGER_GRAB_VALUE; + }; + that.triggerSmoothedSqueezed = function () { + return that.triggerValue > that.TRIGGER_ON_VALUE; + }; + that.triggerSmoothedReleased = function () { + return that.triggerValue < that.TRIGGER_OFF_VALUE; + }; + + // This part is not from handControllerGrab.js + that.state = null; // tri-state: falsey, 'partial', 'full' + that.update = function () { // update state, called from an update function + var state = that.state; + that.updateSmoothedTrigger(); + + // The first two are independent of previous state: + if (that.triggerSmoothedGrab()) { + state = 'full'; + } else if (that.triggerSmoothedReleased()) { + state = null; + } else if (that.triggerSmoothedSqueezed()) { + // Another way to do this would be to have hysteresis in this branch, but that seems to make things harder to use. + // In particular, the vive has a nice detent as you release off of full, and we want that to be a transition from + // full to partial. + state = 'partial'; } + that.state = state; + }; + // Answer a controller source function (answering either 0.0 or 1.0). + that.partial = function () { + return that.state ? 1.0 : 0.0; // either 'partial' or 'full' + }; + that.full = function () { + return (that.state === 'full') ? 1.0 : 0.0; }; } @@ -107,9 +144,8 @@ function isPointingAtOverlay(optionalHudPosition2d) { } // Generalized HUD utilities, with or without HMD: -// These two "vars" are for documentation. Do not change their values! -var SPHERICAL_HUD_DISTANCE = 1; // meters. -var PLANAR_PERPENDICULAR_HUD_DISTANCE = SPHERICAL_HUD_DISTANCE; +// This "var" is for documentation. Do not change the value! +var PLANAR_PERPENDICULAR_HUD_DISTANCE = 1; function calculateRayUICollisionPoint(position, direction) { // Answer the 3D intersection of the HUD by the given ray, or falsey if no intersection. if (HMD.active) { @@ -172,9 +208,9 @@ function isShakingMouse() { // True if the person is waving the mouse around try return isShaking; } var NON_LINEAR_DIVISOR = 2; -var MINIMUM_SEEK_DISTANCE = 0.01; -function updateSeeking() { - if (!Reticle.visible || isShakingMouse()) { +var MINIMUM_SEEK_DISTANCE = 0.1; +function updateSeeking(doNotStartSeeking) { + if (!doNotStartSeeking && (!Reticle.visible || isShakingMouse())) { if (!isSeeking) { print('Start seeking mouse.'); isSeeking = true; @@ -185,12 +221,11 @@ function updateSeeking() { } averageMouseVelocity = lastIntegration = 0; var lookAt2D = HMD.getHUDLookAtPosition2D(); - if (!lookAt2D) { - // FIXME - determine if this message is useful but make it so it doesn't spam the - // log in the case that it is happening - //print('Cannot seek without lookAt position'); - return; - } // E.g., if parallel to location in HUD + if (!lookAt2D) { // If this happens, something has gone terribly wrong. + print('Cannot seek without lookAt position'); + isSeeking = false; + return; // E.g., if parallel to location in HUD + } var copy = Reticle.position; function updateDimension(axis) { var distanceBetween = lookAt2D[axis] - Reticle.position[axis]; @@ -229,6 +264,11 @@ function expireMouseCursor(now) { Reticle.visible = false; } } +function hudReticleDistance() { // 3d distance from camera to the reticle position on hud + // (The camera is only in the center of the sphere on reset.) + var reticlePositionOnHUD = HMD.worldPointFromOverlay(Reticle.position); + return Vec3.distance(reticlePositionOnHUD, HMD.position); +} function onMouseMove() { // Display cursor at correct depth (as in depthReticle.js), and updateMouseActivity. if (ignoreMouseActivity()) { @@ -238,11 +278,10 @@ function onMouseMove() { if (HMD.active) { // set depth updateSeeking(); if (isPointingAtOverlay()) { - Reticle.setDepth(SPHERICAL_HUD_DISTANCE); // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! + Reticle.depth = hudReticleDistance(); } else { var result = findRayIntersection(Camera.computePickRay(Reticle.position.x, Reticle.position.y)); - var depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; - Reticle.setDepth(depth); + Reticle.depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; } } updateMouseActivity(); // After the above, just in case the depth movement is awkward when becoming visible. @@ -257,113 +296,123 @@ setupHandler(Controller.mouseDoublePressEvent, onMouseClick); // CONTROLLER MAPPING --------- // +var leftTrigger = new Trigger('left'); +var rightTrigger = new Trigger('right'); +var activeTrigger = rightTrigger; var activeHand = Controller.Standard.RightHand; -function toggleHand() { +var LEFT_HUD_LASER = 1; +var RIGHT_HUD_LASER = 2; +var BOTH_HUD_LASERS = LEFT_HUD_LASER + RIGHT_HUD_LASER; +var activeHudLaser = RIGHT_HUD_LASER; +function toggleHand() { // unequivocally switch which hand controls mouse position if (activeHand === Controller.Standard.RightHand) { activeHand = Controller.Standard.LeftHand; + activeTrigger = leftTrigger; + activeHudLaser = LEFT_HUD_LASER; } else { activeHand = Controller.Standard.RightHand; + activeTrigger = rightTrigger; + activeHudLaser = RIGHT_HUD_LASER; } + clearSystemLaser(); +} +function makeToggleAction(hand) { // return a function(0|1) that makes the specified hand control mouse when 1 + return function (on) { + if (on && (activeHand !== hand)) { + toggleHand(); + } + }; } var clickMapping = Controller.newMapping(Script.resolvePath('') + '-click'); Script.scriptEnding.connect(clickMapping.disable); -clickMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(Controller.Actions.ReticleClick); -clickMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(Controller.Actions.ReticleClick); +// Gather the trigger data for smoothing. +clickMapping.from(Controller.Standard.RT).peek().to(rightTrigger.triggerPress); +clickMapping.from(Controller.Standard.LT).peek().to(leftTrigger.triggerPress); +// Full smoothed trigger is a click. +function isPointingAtOverlayStartedNonFullTrigger(trigger) { + // true if isPointingAtOverlay AND we were NOT full triggered when we became so. + // The idea is to not count clicks when we're full-triggering and reach the edge of a window. + var lockedIn = false; + return function () { + if (trigger !== activeTrigger) { + return lockedIn = false; + } + if (!isPointingAtOverlay()) { + return lockedIn = false; + } + if (lockedIn) { + return true; + } + lockedIn = !trigger.full(); + return lockedIn; + } +} +clickMapping.from(rightTrigger.full).when(isPointingAtOverlayStartedNonFullTrigger(rightTrigger)).to(Controller.Actions.ReticleClick); +clickMapping.from(leftTrigger.full).when(isPointingAtOverlayStartedNonFullTrigger(leftTrigger)).to(Controller.Actions.ReticleClick); clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu); clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(Controller.Actions.ContextMenu); -clickMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(function (on) { - if (on && (activeHand !== Controller.Standard.RightHand)) { - toggleHand(); - } -}); -clickMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(function (on) { - if (on && (activeHand !== Controller.Standard.LeftHand)) { - toggleHand(); - } +clickMapping.from(Controller.Hardware.Keyboard.RightMouseClicked).peek().to(function () { + // Allow the reticle depth to be set correctly: + // Wait a tick for the context menu to be displayed, and then simulate a (non-hand-controller) mouse move + // so that the system updates qml state (Reticle.pointingAtSystemOverlay) before it gives us a mouseMove. + // We don't want the system code to always do this for us, because, e.g., we do not want to get a mouseMove + // after the Left/RightSecondaryThumb gives us a context menu. Only from the mouse. + Script.setTimeout(function () { + Reticle.setPosition(Reticle.position); + }, 0); }); +// Partial smoothed trigger is activation. +clickMapping.from(rightTrigger.partial).to(makeToggleAction(Controller.Standard.RightHand)); +clickMapping.from(leftTrigger.partial).to(makeToggleAction(Controller.Standard.LeftHand)); clickMapping.enable(); // VISUAL AID ----------- // Same properties as handControllerGrab search sphere -var BALL_SIZE = 0.011; -var BALL_ALPHA = 0.5; -var fakeProjectionBall = Overlays.addOverlay("sphere", { - size: 5 * BALL_SIZE, - color: {red: 255, green: 10, blue: 10}, - ignoreRayIntersection: true, - alpha: BALL_ALPHA, - visible: false, - solid: true, - drawInFront: true // Even when burried inside of something, show it. -}); -var overlays = [fakeProjectionBall]; // If we want to try showing multiple balls and lasers. -Script.scriptEnding.connect(function () { - overlays.forEach(Overlays.deleteOverlay); -}); -var visualizationIsShowing = false; // Not whether it desired, but simply whether it is. Just an optimziation. -function turnOffVisualization(optionalEnableClicks) { // because we're showing cursor on HUD - if (!optionalEnableClicks) { - expireMouseCursor(); - } - if (!visualizationIsShowing) { +var LASER_ALPHA = 0.5; +var LASER_SEARCH_COLOR_XYZW = {x: 10 / 255, y: 10 / 255, z: 255 / 255, w: LASER_ALPHA}; +var LASER_TRIGGER_COLOR_XYZW = {x: 250 / 255, y: 10 / 255, z: 10 / 255, w: LASER_ALPHA}; +var SYSTEM_LASER_DIRECTION = {x: 0, y: 0, z: -1}; +var systemLaserOn = false; +function clearSystemLaser() { + if (!systemLaserOn) { return; } - visualizationIsShowing = false; - overlays.forEach(function (overlay) { - Overlays.editOverlay(overlay, {visible: false}); - }); + HMD.disableHandLasers(BOTH_HUD_LASERS); + systemLaserOn = false; } -var MAX_RAY_SCALE = 32000; // Anything large. It's a scale, not a distance. -function updateVisualization(controllerPosition, controllerDirection, hudPosition3d, hudPosition2d) { - // Show an indication of where the cursor will appear when crossing a HUD element, - // and where in-world clicking will occur. - // - // There are a number of ways we could do this, but for now, it's a blue sphere that rolls along - // the HUD surface, and a red sphere that rolls along the 3d objects that will receive the click. - // We'll leave it to other scripts (like handControllerGrab) to show a search beam when desired. +function setColoredLaser() { // answer trigger state if lasers supported, else falsey. + var color = (activeTrigger.state === 'full') ? LASER_TRIGGER_COLOR_XYZW : LASER_SEARCH_COLOR_XYZW; + return HMD.setHandLasers(activeHudLaser, true, color, SYSTEM_LASER_DIRECTION) && activeTrigger.state; - function intersection3d(position, direction) { - // Answer in-world intersection (entity or 3d overlay), or way-out point - var pickRay = {origin: position, direction: direction}; - var result = findRayIntersection(pickRay); - return result.intersects ? result.intersection : Vec3.sum(position, Vec3.multiply(MAX_RAY_SCALE, direction)); - } - - visualizationIsShowing = true; - // We'd rather in-world interactions be done at the termination of the hand beam - // -- intersection3d(controllerPosition, controllerDirection). Maybe have handControllerGrab - // direclty manipulate both entity and 3d overlay objects. - // For now, though, we present a false projection of the cursor onto whatever is below it. This is - // different from the hand beam termination because the false projection is from the camera, while - // the hand beam termination is from the hand. - var eye = Camera.getPosition(); - var falseProjection = intersection3d(eye, Vec3.subtract(hudPosition3d, eye)); - Overlays.editOverlay(fakeProjectionBall, {visible: true, position: falseProjection}); - Reticle.visible = false; - - return visualizationIsShowing; // In case we change caller to act conditionally. } // MAIN OPERATIONS ----------- // function update() { var now = Date.now(); - if (!handControllerLockOut.expired(now)) { - return turnOffVisualization(); - } // Let them use mouse it in peace. - if (!Menu.isOptionChecked("First Person")) { - return turnOffVisualization(); - } // What to do? menus can be behind hand! - if (!Window.hasFocus()) { // Don't mess with other apps - return turnOffVisualization(); + function off() { + expireMouseCursor(); + clearSystemLaser(); } + updateSeeking(true); + if (!handControllerLockOut.expired(now)) { + return off(); // Let them use mouse it in peace. + } + if (!Menu.isOptionChecked("First Person")) { + return off(); // What to do? menus can be behind hand! + } + if (!Window.hasFocus() || !Reticle.allowMouseCapture) { + return off(); // Don't mess with other apps or paused mouse activity + } + leftTrigger.update(); + rightTrigger.update(); var controllerPose = Controller.getPoseValue(activeHand); // Valid if any plugged-in hand controller is "on". (uncradled Hydra, green-lighted Vive...) if (!controllerPose.valid) { - return turnOffVisualization(); - } // Controller is cradled. + return off(); // Controller is cradled. + } var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), MyAvatar.position); // This gets point direction right, but if you want general quaternion it would be more complicated: @@ -371,10 +420,10 @@ function update() { var hudPoint3d = calculateRayUICollisionPoint(controllerPosition, controllerDirection); if (!hudPoint3d) { - // FIXME - determine if this message is useful but make it so it doesn't spam the - // log in the case that it is happening - //print('Controller is parallel to HUD'); - return turnOffVisualization(); + if (Menu.isOptionChecked("Overlays")) { // With our hud resetting strategy, hudPoint3d should be valid here + print('Controller is parallel to HUD'); // so let us know that our assumptions are wrong. + } + return off(); } var hudPoint2d = overlayFromWorldPoint(hudPoint3d); @@ -383,14 +432,25 @@ function update() { setReticlePosition(hudPoint2d); // If there's a HUD element at the (newly moved) reticle, just make it visible and bail. if (isPointingAtOverlay(hudPoint2d)) { - if (HMD.active) { // Doesn't hurt anything without the guard, but consider it documentation. - Reticle.depth = SPHERICAL_HUD_DISTANCE; // NOT CORRECT IF WE SWITCH TO OFFSET SPHERE! + if (HMD.active) { + Reticle.depth = hudReticleDistance(); } - Reticle.visible = true; - return turnOffVisualization(true); + if (activeTrigger.state && (!systemLaserOn || (systemLaserOn !== activeTrigger.state))) { // last=>wrong color + // If the active plugin doesn't implement hand lasers, show the mouse reticle instead. + systemLaserOn = setColoredLaser(); + Reticle.visible = !systemLaserOn; + } else if ((systemLaserOn || Reticle.visible) && !activeTrigger.state) { + clearSystemLaser(); + Reticle.visible = false; + } + return; } // We are not pointing at a HUD element (but it could be a 3d overlay). - updateVisualization(controllerPosition, controllerDirection, hudPoint3d, hudPoint2d); + if (!activeTrigger.state) { + return off(); // No trigger + } + clearSystemLaser(); + Reticle.visible = false; } var UPDATE_INTERVAL = 50; // milliseconds. Script.update is too frequent. diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 42eddf11c3..9d5585e353 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -150,6 +150,8 @@ function showMarketplace(marketplaceID) { marketplaceWindow.setURL(url); marketplaceWindow.setVisible(true); marketplaceWindow.raise(); + + UserActivityLogger.logAction("opened_marketplace"); } function hideMarketplace() { @@ -181,7 +183,7 @@ var toolBar = (function() { function initialize() { toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.edit.toolbar", function(windowDimensions, toolbar) { return { - x: windowDimensions.x / 2, + x: (windowDimensions.x / 2) + (Tool.IMAGE_WIDTH * 2), y: windowDimensions.y }; }, { @@ -332,7 +334,7 @@ var toolBar = (function() { that.setActive = function(active) { if (active != isActive) { - if (active && !Entities.canAdjustLocks()) { + if (active && !Entities.canRez() && !Entities.canRezTmp()) { Window.alert(INSUFFICIENT_PERMISSIONS_ERROR_MSG); } else { Messages.sendLocalMessage("edit-events", JSON.stringify({ @@ -347,6 +349,7 @@ var toolBar = (function() { selectionManager.clearSelections(); cameraManager.disable(); } else { + UserActivityLogger.enabledEdit(); hasShownPropertiesTool = false; entityListTool.setVisible(true); gridTool.setVisible(true); diff --git a/scripts/system/examples.js b/scripts/system/examples.js index 9d33e473af..a948f9e563 100644 --- a/scripts/system/examples.js +++ b/scripts/system/examples.js @@ -9,10 +9,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -Script.include([ - "libraries/toolBars.js", -]); - var toolIconUrl = Script.resolvePath("assets/images/tools/"); var EXAMPLES_URL = "https://metaverse.highfidelity.com/examples"; @@ -37,6 +33,8 @@ function showExamples(marketplaceID) { print("setting examples URL to " + url); examplesWindow.setURL(url); examplesWindow.setVisible(true); + + UserActivityLogger.openedMarketplace(); } function hideExamples() { @@ -52,87 +50,21 @@ function toggleExamples() { } } -var toolBar = (function() { - var that = {}, - toolBar, - browseExamplesButton; +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - function initialize() { - toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.examples.toolbar", function(windowDimensions, toolbar) { - return { - x: windowDimensions.x / 2, - y: windowDimensions.y - }; - }, { - x: -toolWidth / 2, - y: -TOOLBAR_MARGIN_Y - toolHeight - }); - browseExamplesButton = toolBar.addTool({ - imageURL: toolIconUrl + "examples-01.svg", - subImage: { - x: 0, - y: Tool.IMAGE_WIDTH, - width: Tool.IMAGE_WIDTH, - height: Tool.IMAGE_HEIGHT - }, - width: toolWidth, - height: toolHeight, - alpha: 0.9, - visible: true, - showButtonDown: true - }); +var browseExamplesButton = toolBar.addButton({ + imageURL: toolIconUrl + "examples-01.svg", + objectName: "examples", + yOffset: 50, + alpha: 0.9, +}); - toolBar.showTool(browseExamplesButton, true); - } +var browseExamplesButtonDown = false; - var browseExamplesButtonDown = false; - that.mousePressEvent = function(event) { - var clickedOverlay, - url, - file; +browseExamplesButton.clicked.connect(function(){ + toggleExamples(); +}); - if (!event.isLeftButton) { - // if another mouse button than left is pressed ignore it - return false; - } - - clickedOverlay = Overlays.getOverlayAtPoint({ - x: event.x, - y: event.y - }); - - if (browseExamplesButton === toolBar.clicked(clickedOverlay)) { - toggleExamples(); - return true; - } - - return false; - }; - - that.mouseReleaseEvent = function(event) { - var handled = false; - - - if (browseExamplesButtonDown) { - var clickedOverlay = Overlays.getOverlayAtPoint({ - x: event.x, - y: event.y - }); - } - - newModelButtonDown = false; - browseExamplesButtonDown = false; - - return handled; - } - - that.cleanup = function() { - toolBar.cleanup(); - }; - - initialize(); - return that; -}()); - -Controller.mousePressEvent.connect(toolBar.mousePressEvent) -Script.scriptEnding.connect(toolBar.cleanup); \ No newline at end of file +Script.scriptEnding.connect(function () { + browseExamplesButton.clicked.disconnect(); +}); diff --git a/scripts/system/firstPersonHMD.js b/scripts/system/firstPersonHMD.js new file mode 100644 index 0000000000..082c6304be --- /dev/null +++ b/scripts/system/firstPersonHMD.js @@ -0,0 +1,17 @@ +// +// firstPersonHMD.js +// system +// +// Created by Zander Otavka on 6/24/16 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// Automatically enter first person mode when entering HMD mode +HMD.displayModeChanged.connect(function(isHMDMode) { + if (isHMDMode) { + Camera.setModeString("first person"); + } +}); diff --git a/scripts/system/goto.js b/scripts/system/goto.js new file mode 100644 index 0000000000..a2ade02a78 --- /dev/null +++ b/scripts/system/goto.js @@ -0,0 +1,29 @@ +// +// goto.js +// scripts/system/ +// +// Created by Howard Stearns on 2 Jun 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + + +var button = toolBar.addButton({ + objectName: "goto", + imageURL: Script.resolvePath("assets/images/tools/directory-01.svg"), + visible: true, + yOffset: 50, + alpha: 0.9, +}); + +button.clicked.connect(function(){ + DialogsManager.toggleAddressBar(); +}); + +Script.scriptEnding.connect(function () { + button.clicked.disconnect(); +}); diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js new file mode 100644 index 0000000000..2965c0d254 --- /dev/null +++ b/scripts/system/hmd.js @@ -0,0 +1,42 @@ +// +// hmd.js +// scripts/system/ +// +// Created by Howard Stearns on 2 Jun 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var headset; // The preferred headset. Default to the first one found in the following list. +var displayMenuName = "Display"; +var desktopMenuItemName = "Desktop"; +['OpenVR (Vive)', 'Oculus Rift'].forEach(function (name) { + if (!headset && Menu.menuItemExists(displayMenuName, name)) { + headset = name; + } +}); + +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); +var button; + +if (headset) { + button = toolBar.addButton({ + objectName: "hmdToggle", + imageURL: Script.resolvePath("assets/images/tools/hmd-switch-01.svg"), + visible: true, + yOffset: 50, + alpha: 0.9, + }); + + button.clicked.connect(function(){ + var isDesktop = Menu.isOptionChecked(desktopMenuItemName); + Menu.setIsOptionChecked(isDesktop ? headset : desktopMenuItemName, true); + }); + + Script.scriptEnding.connect(function () { + button.clicked.disconnect(); + }); +} + diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 0af199ef56..121e38c340 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -1646,6 +1646,7 @@ +
diff --git a/scripts/system/libraries/Xform.js b/scripts/system/libraries/Xform.js new file mode 100644 index 0000000000..75051c4983 --- /dev/null +++ b/scripts/system/libraries/Xform.js @@ -0,0 +1,40 @@ +// +// Created by Anthony J. Thibault on 2016/06/21 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// ctor +Xform = function(rot, pos) { + this.rot = rot; + this.pos = pos; +} + +Xform.ident = function() { + return new Xform({x: 0, y: 0, z: 0, w: 1}, {x: 0, y: 0, z: 0}); +}; + +Xform.mul = function(lhs, rhs) { + var rot = Quat.multiply(lhs.rot, rhs.rot); + var pos = Vec3.sum(lhs.pos, Vec3.multiplyQbyV(lhs.rot, rhs.pos)); + return new Xform(rot, pos); +}; + +Xform.prototype.inv = function() { + var invRot = Quat.inverse(this.rot); + var invPos = Vec3.multiply(-1, this.pos); + return new Xform(invRot, Vec3.multiplyQbyV(invRot, invPos)); +}; + +Xform.prototype.mirrorX = function() { + return new Xform({x: this.rot.x, y: -this.rot.y, z: -this.rot.z, w: this.rot.w}, + {x: -this.pos.x, y: this.pos.y, z: this.pos.z}); +}; + +Xform.prototype.toString = function() { + var rot = this.rot; + var pos = this.pos; + return "Xform rot = (" + rot.x + ", " + rot.y + ", " + rot.z + ", " + rot.w + "), pos = (" + pos.x + ", " + pos.y + ", " + pos.z + ")"; +}; diff --git a/scripts/system/libraries/entityCameraTool.js b/scripts/system/libraries/entityCameraTool.js index a8e9335956..301b60f550 100644 --- a/scripts/system/libraries/entityCameraTool.js +++ b/scripts/system/libraries/entityCameraTool.js @@ -141,7 +141,9 @@ CameraManager = function() { }; that.enable = function() { - if (Camera.mode == "independent" || that.enabled) return; + if (Camera.mode == "independent" || that.enabled || HMD.active) { + return; + } for (var i = 0; i < CAPTURED_KEYS.length; i++) { Controller.captureKeyEvents({ @@ -179,7 +181,9 @@ CameraManager = function() { } that.disable = function(ignoreCamera) { - if (!that.enabled) return; + if (!that.enabled) { + return; + } for (var i = 0; i < CAPTURED_KEYS.length; i++) { Controller.releaseKeyEvents({ @@ -352,27 +356,21 @@ CameraManager = function() { that.mousePressEvent = function(event) { if (cameraTool.mousePressEvent(event)) { - return true; } - - if (!that.enabled) return; + if (!that.enabled) { + return; + } if (event.isRightButton || (event.isLeftButton && event.isControl && !event.isShifted)) { - that.mode = MODE_ORBIT; } else if (event.isMiddleButton || (event.isLeftButton && event.isControl && event.isShifted)) { - - that.mode = MODE_PAN; } if (that.mode !== MODE_INACTIVE) { - - hasDragged = false; - return true; } @@ -381,7 +379,9 @@ CameraManager = function() { that.mouseReleaseEvent = function(event) { - if (!that.enabled) return; + if (!that.enabled) { + return; + } that.mode = MODE_INACTIVE; Reticle.setVisible(true); @@ -403,7 +403,9 @@ CameraManager = function() { }; that.wheelEvent = function(event) { - if (!that.enabled) return; + if (!that.enabled) { + return; + } var dZoom = -event.delta * SCROLL_SENSITIVITY; @@ -459,8 +461,12 @@ CameraManager = function() { } function normalizeDegrees(degrees) { - while (degrees > 180) degrees -= 360; - while (degrees < -180) degrees += 360; + while (degrees > 180) { + degrees -= 360; + } + while (degrees < -180) { + degrees += 360; + } return degrees; } @@ -483,7 +489,6 @@ CameraManager = function() { that.targetZoomDistance = clamp(that.targetZoomDistance, MIN_ZOOM_DISTANCE, MAX_ZOOM_DISTANCE); } - if (easing) { easingTime = Math.min(EASE_TIME, easingTime + dt); } diff --git a/scripts/system/mute.js b/scripts/system/mute.js new file mode 100644 index 0000000000..f66b6852ea --- /dev/null +++ b/scripts/system/mute.js @@ -0,0 +1,29 @@ +// +// goto.js +// scripts/system/ +// +// Created by Howard Stearns on 2 Jun 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + + +var button = toolBar.addButton({ + objectName: "mute", + imageURL: Script.resolvePath("assets/images/tools/microphone.svg"), + visible: true, + alpha: 0.9, +}); + +button.clicked.connect(function(){ + var menuItem = "Mute Microphone"; + Menu.setIsOptionChecked(menuItem, !Menu.isOptionChecked(menuItem)); +}); + +Script.scriptEnding.connect(function () { + button.clicked.disconnect(); +}); diff --git a/scripts/system/users.js b/scripts/system/users.js index 5b0ba42a45..d25e1b76b4 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var PopUpMenu = function(properties) { +var PopUpMenu = function (properties) { var value = properties.value, promptOverlay, valueOverlay, @@ -217,7 +217,7 @@ var PopUpMenu = function(properties) { }; }; -var usersWindow = (function() { +var usersWindow = (function () { var baseURL = Script.resolvePath("assets/images/tools/"), WINDOW_WIDTH = 260, @@ -253,11 +253,7 @@ var usersWindow = (function() { WINDOW_BORDER_BOTTOM_MARGIN = WINDOW_BASE_MARGIN, WINDOW_BORDER_LEFT_MARGIN = WINDOW_BASE_MARGIN, WINDOW_BORDER_RADIUS = 4, - WINDOW_BORDER_COLOR = { - red: 255, - green: 255, - blue: 255 - }, + WINDOW_BORDER_COLOR = { red: 255, green: 255, blue: 255 }, WINDOW_BORDER_ALPHA = 0.5, windowBorder, @@ -363,30 +359,31 @@ var usersWindow = (function() { usersTimer = null, USERS_UPDATE_TIMEOUT = 5000, // ms = 5s + showMe, myVisibility, - MENU_NAME = "Tools", + MENU_NAME = "View", MENU_ITEM = "Users Online", - MENU_ITEM_AFTER = "Chat...", + MENU_ITEM_AFTER = "Overlays", + SETTING_USERS_SHOW_ME = "UsersWindow.ShowMe", + SETTING_USERS_VISIBLE_TO = "UsersWindow.VisibleTo", SETTING_USERS_WINDOW_MINIMIZED = "UsersWindow.Minimized", - SETINGS_USERS_WINDOW_OFFSET = "UsersWindow.Offset", + SETTING_USERS_WINDOW_OFFSET = "UsersWindow.Offset", // +ve x, y values are offset from left, top of screen; -ve from right, bottom. + isLoggedIn = false, isVisible = true, - isMinimized = false, + isMinimized = true, isBorderVisible = false, viewport, isMirrorDisplay = false, isFullscreenMirror = false, - windowPosition = {}, // Bottom left corner of window pane. + windowPosition = {}, // Bottom left corner of window pane. isMovingWindow = false, - movingClickOffset = { - x: 0, - y: 0 - }, + movingClickOffset = { x: 0, y: 0 }, isUsingScrollbars = false, isMovingScrollbar = false, @@ -395,6 +392,12 @@ var usersWindow = (function() { scrollbarBarClickedAt, // 0.0 .. 1.0 scrollbarValue = 0.0; // 0.0 .. 1.0 + function isValueTrue(value) { + // Work around Boolean Settings values being read as string when Interface starts up but as Booleans when re-read after + // Being written if refresh script. + return value === true || value === "true"; + } + function calculateWindowHeight() { var AUDIO_METER_HEIGHT = 52, MIRROR_HEIGHT = 220, @@ -408,7 +411,9 @@ var usersWindow = (function() { } // Reserve space for title, friends button, and option controls - nonUsersHeight = WINDOW_MARGIN + windowLineHeight + FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER + windowLineHeight + VISIBILITY_SPACER + windowLineHeight + WINDOW_BASE_MARGIN; + nonUsersHeight = WINDOW_MARGIN + windowLineHeight + FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT + DISPLAY_SPACER + + windowLineHeight + VISIBILITY_SPACER + + windowLineHeight + WINDOW_BASE_MARGIN; // Limit window to height of viewport above window position minus VU meter and mirror if displayed windowHeight = linesOfUsers.length * windowLineHeight - windowLineSpacing + nonUsersHeight; @@ -461,14 +466,17 @@ var usersWindow = (function() { x: scrollbarBackgroundPosition.x, y: scrollbarBackgroundPosition.y }); - scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); Overlays.editOverlay(scrollbarBar, { x: scrollbarBackgroundPosition.x + 1, y: scrollbarBarPosition.y }); x = windowLeft + WINDOW_MARGIN; - y = windowPosition.y - FRIENDS_BUTTON_HEIGHT - DISPLAY_SPACER - windowLineHeight - VISIBILITY_SPACER - windowLineHeight - WINDOW_BASE_MARGIN; + y = windowPosition.y - FRIENDS_BUTTON_HEIGHT - DISPLAY_SPACER + - windowLineHeight - VISIBILITY_SPACER + - windowLineHeight - WINDOW_BASE_MARGIN; Overlays.editOverlay(friendsButton, { x: x, y: y @@ -519,13 +527,13 @@ var usersWindow = (function() { scrollbarBackgroundHeight = numUsersToDisplay * windowLineHeight - windowLineSpacing / 2; Overlays.editOverlay(scrollbarBackground, { height: scrollbarBackgroundHeight, - visible: isUsingScrollbars + visible: isLoggedIn && isUsingScrollbars }); scrollbarBarHeight = Math.max(numUsersToDisplay / linesOfUsers.length * scrollbarBackgroundHeight, SCROLLBAR_BAR_MIN_HEIGHT); Overlays.editOverlay(scrollbarBar, { height: scrollbarBarHeight, - visible: isUsingScrollbars + visible: isLoggedIn && isUsingScrollbars }); } @@ -543,10 +551,45 @@ var usersWindow = (function() { }); } + function updateOverlayVisibility() { + Overlays.editOverlay(windowBorder, { + visible: isLoggedIn && isVisible && isBorderVisible + }); + Overlays.editOverlay(windowPane, { + visible: isLoggedIn && isVisible + }); + Overlays.editOverlay(windowHeading, { + visible: isLoggedIn && isVisible + }); + Overlays.editOverlay(minimizeButton, { + visible: isLoggedIn && isVisible + }); + Overlays.editOverlay(scrollbarBackground, { + visible: isLoggedIn && isVisible && isUsingScrollbars && !isMinimized + }); + Overlays.editOverlay(scrollbarBar, { + visible: isLoggedIn && isVisible && isUsingScrollbars && !isMinimized + }); + Overlays.editOverlay(friendsButton, { + visible: isLoggedIn && isVisible && !isMinimized + }); + displayControl.setVisible(isLoggedIn && isVisible && !isMinimized); + visibilityControl.setVisible(isLoggedIn && isVisible && !isMinimized); + } + + function checkLoggedIn() { + var wasLoggedIn = isLoggedIn; + + isLoggedIn = Account.isLoggedIn(); + if (isLoggedIn !== wasLoggedIn) { + updateOverlayVisibility(); + } + } + function pollUsers() { var url = API_URL; - if (displayControl.getValue() === DISPLAY_FRIENDS) { + if (showMe === DISPLAY_FRIENDS) { url += API_FRIENDS_FILTER; } @@ -556,36 +599,9 @@ var usersWindow = (function() { usersRequest.ontimeout = pollUsersTimedOut; usersRequest.onreadystatechange = processUsers; usersRequest.send(); - checkLoggedIn(); } - var loggedIn = false; - - function checkLoggedIn() { - loggedIn = Account.isLoggedIn(); - if (loggedIn === false) { - Overlays.editOverlay(friendsButton, { - visible: false - }); - visibilityControl.setVisible(false); - displayControl.setVisible(false); - } else { - if (isMinimized === true) { - loggedIn = true; - return - } - Overlays.editOverlay(friendsButton, { - visible: true - }); - visibilityControl.setVisible(true); - displayControl.setVisible(true); - loggedIn = true; - - } - } - - - processUsers = function() { + processUsers = function () { var response, myUsername, user, @@ -628,6 +644,8 @@ var usersWindow = (function() { updateUsersDisplay(); updateOverlayPositions(); + checkLoggedIn(); + } else { print("Error: Request for users status returned " + usersRequest.status + " " + usersRequest.statusText); usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. @@ -638,41 +656,11 @@ var usersWindow = (function() { } }; - pollUsersTimedOut = function() { + pollUsersTimedOut = function () { print("Error: Request for users status timed out"); usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. }; - function updateOverlayVisibility() { - Overlays.editOverlay(windowBorder, { - visible: isVisible && isBorderVisible - }); - Overlays.editOverlay(windowPane, { - visible: isVisible - }); - Overlays.editOverlay(windowHeading, { - visible: isVisible - }); - Overlays.editOverlay(minimizeButton, { - visible: isVisible - }); - Overlays.editOverlay(scrollbarBackground, { - visible: isVisible && isUsingScrollbars && !isMinimized - }); - Overlays.editOverlay(scrollbarBar, { - visible: isVisible && isUsingScrollbars && !isMinimized - }); - - if (loggedIn === true) { - Overlays.editOverlay(friendsButton, { - visible: isVisible && !isMinimized - }); - displayControl.setVisible(isVisible && !isMinimized); - visibilityControl.setVisible(isVisible && !isMinimized); - } - - } - function setVisible(visible) { isVisible = visible; @@ -686,7 +674,6 @@ var usersWindow = (function() { } updateOverlayVisibility(); - } function setMinimized(minimized) { @@ -697,6 +684,7 @@ var usersWindow = (function() { } }); updateOverlayVisibility(); + Settings.setValue(SETTING_USERS_WINDOW_MINIMIZED, isMinimized); } function onMenuItemEvent(event) { @@ -707,9 +695,11 @@ var usersWindow = (function() { function onFindableByChanged(event) { if (VISIBILITY_VALUES.indexOf(event) !== -1) { + myVisibility = event; visibilityControl.setValue(event); + Settings.setValue(SETTING_USERS_VISIBLE_TO, myVisibility); } else { - print("Error: Unrecognized onFindableByChanged value: " + myVisibility); + print("Error: Unrecognized onFindableByChanged value: " + event); } } @@ -739,11 +729,15 @@ var usersWindow = (function() { usersTimer = null; } pollUsers(); + showMe = displayControl.getValue(); + Settings.setValue(SETTING_USERS_SHOW_ME, showMe); return; } if (visibilityControl.handleClick(clickedOverlay)) { - GlobalServices.findableBy = visibilityControl.getValue(); + myVisibility = visibilityControl.getValue(); + GlobalServices.findableBy = myVisibility; + Settings.setValue(SETTING_USERS_VISIBLE_TO, myVisibility); return; } @@ -763,7 +757,9 @@ var usersWindow = (function() { userClicked = firstUserToDisplay + lineClicked; - if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { + if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX + && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { + //print("Go to " + usersOnline[linesOfUsers[userClicked]].username); location.goToUser(usersOnline[linesOfUsers[userClicked]].username); } @@ -831,8 +827,12 @@ var usersWindow = (function() { var isVisible; if (isMovingScrollbar) { - if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { - scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x + && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN + && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y + && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { + scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) + / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); scrollbarValue = Math.min(Math.max(scrollbarValue, 0.0), 1.0); firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); updateOverlayPositions(); @@ -858,9 +858,13 @@ var usersWindow = (function() { isVisible = isBorderVisible; if (isVisible) { - isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x && event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH && windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y && event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; + isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x + && event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH + && windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y + && event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; } else { - isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH && windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; + isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH + && windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; } if (isVisible !== isBorderVisible) { isBorderVisible = isVisible; @@ -883,9 +887,11 @@ var usersWindow = (function() { if (isMovingWindow) { // Save offset of bottom of window to nearest edge of the window. - offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) ? windowPosition.x : windowPosition.x - viewport.x; - offset.y = (windowPosition.y < viewport.y / 2) ? windowPosition.y : windowPosition.y - viewport.y; - Settings.setValue(SETINGS_USERS_WINDOW_OFFSET, JSON.stringify(offset)); + offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) + ? windowPosition.x : windowPosition.x - viewport.x; + offset.y = (windowPosition.y < viewport.y / 2) + ? windowPosition.y : windowPosition.y - viewport.y; + Settings.setValue(SETTING_USERS_WINDOW_OFFSET, JSON.stringify(offset)); isMovingWindow = false; } } @@ -901,7 +907,8 @@ var usersWindow = (function() { isMirrorDisplay = Menu.isOptionChecked(MIRROR_MENU_ITEM); isFullscreenMirror = Menu.isOptionChecked(FULLSCREEN_MIRROR_MENU_ITEM); - if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay || isFullscreenMirror !== oldIsFullscreenMirror) { + if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay + || isFullscreenMirror !== oldIsFullscreenMirror) { calculateWindowHeight(); updateUsersDisplay(); } @@ -940,9 +947,9 @@ var usersWindow = (function() { viewport = Controller.getViewportDimensions(); - offsetSetting = Settings.getValue(SETINGS_USERS_WINDOW_OFFSET); + offsetSetting = Settings.getValue(SETTING_USERS_WINDOW_OFFSET); if (offsetSetting !== "") { - offset = JSON.parse(Settings.getValue(SETINGS_USERS_WINDOW_OFFSET)); + offset = JSON.parse(Settings.getValue(SETTING_USERS_WINDOW_OFFSET)); } if (offset.hasOwnProperty("x") && offset.hasOwnProperty("y")) { windowPosition.x = offset.x < 0 ? viewport.x + offset.x : offset.x; @@ -951,8 +958,8 @@ var usersWindow = (function() { } else { hmdViewport = Controller.getRecommendedOverlayRect(); windowPosition = { - x: (viewport.x - hmdViewport.width) / 2, // HMD viewport is narrower than screen. - y: hmdViewport.height // HMD viewport starts at top of screen but only extends down so far. + x: (viewport.x - hmdViewport.width) / 2, // HMD viewport is narrower than screen. + y: hmdViewport.height // HMD viewport starts at top of screen but only extends down so far. }; } @@ -960,13 +967,13 @@ var usersWindow = (function() { windowBorder = Overlays.addOverlay("rectangle", { x: 0, - y: viewport.y, // Start up off-screen + y: viewport.y, // Start up off-screen width: WINDOW_BORDER_WIDTH, height: windowBorderHeight, radius: WINDOW_BORDER_RADIUS, color: WINDOW_BORDER_COLOR, alpha: WINDOW_BORDER_ALPHA, - visible: isVisible && isBorderVisible + visible: false }); windowPane = Overlays.addOverlay("text", { @@ -982,7 +989,7 @@ var usersWindow = (function() { backgroundAlpha: WINDOW_BACKGROUND_ALPHA, text: "", font: WINDOW_FONT, - visible: isVisible + visible: false }); windowHeading = Overlays.addOverlay("text", { @@ -997,7 +1004,7 @@ var usersWindow = (function() { backgroundAlpha: 0.0, text: "No users online", font: WINDOW_FONT, - visible: isVisible && !isMinimized + visible: false }); minimizeButton = Overlays.addOverlay("image", { @@ -1014,7 +1021,7 @@ var usersWindow = (function() { }, color: MIN_MAX_BUTTON_COLOR, alpha: MIN_MAX_BUTTON_ALPHA, - visible: isVisible && !isMinimized + visible: false }); scrollbarBackgroundPosition = { @@ -1029,7 +1036,7 @@ var usersWindow = (function() { backgroundColor: SCROLLBAR_BACKGROUND_COLOR, backgroundAlpha: SCROLLBAR_BACKGROUND_ALPHA, text: "", - visible: isVisible && isUsingScrollbars && !isMinimized + visible: false }); scrollbarBarPosition = { @@ -1044,7 +1051,7 @@ var usersWindow = (function() { backgroundColor: SCROLLBAR_BAR_COLOR, backgroundAlpha: SCROLLBAR_BAR_ALPHA, text: "", - visible: isVisible && isUsingScrollbars && !isMinimized + visible: false }); friendsButton = Overlays.addOverlay("image", { @@ -1060,12 +1067,18 @@ var usersWindow = (function() { height: FRIENDS_BUTTON_SVG_HEIGHT }, color: FRIENDS_BUTTON_COLOR, - alpha: FRIENDS_BUTTON_ALPHA + alpha: FRIENDS_BUTTON_ALPHA, + visible: false }); + showMe = Settings.getValue(SETTING_USERS_SHOW_ME, ""); + if (DISPLAY_VALUES.indexOf(showMe) === -1) { + showMe = DISPLAY_EVERYONE; + } + displayControl = new PopUpMenu({ prompt: DISPLAY_PROMPT, - value: DISPLAY_VALUES[0], + value: showMe, values: DISPLAY_VALUES, displayValues: DISPLAY_DISPLAY_VALUES, x: 0, @@ -1087,13 +1100,12 @@ var usersWindow = (function() { popupBackgroundAlpha: DISPLAY_OPTIONS_BACKGROUND_ALPHA, buttonColor: MIN_MAX_BUTTON_COLOR, buttonAlpha: MIN_MAX_BUTTON_ALPHA, - visible: isVisible && !isMinimized + visible: false }); - myVisibility = GlobalServices.findableBy; + myVisibility = Settings.getValue(SETTING_USERS_VISIBLE_TO, ""); if (VISIBILITY_VALUES.indexOf(myVisibility) === -1) { - print("Error: Unrecognized findableBy value: " + myVisibility); - myVisibility = VISIBILITY_ALL; + myVisibility = VISIBILITY_FRIENDS; } visibilityControl = new PopUpMenu({ @@ -1120,14 +1132,9 @@ var usersWindow = (function() { popupBackgroundAlpha: DISPLAY_OPTIONS_BACKGROUND_ALPHA, buttonColor: MIN_MAX_BUTTON_COLOR, buttonAlpha: MIN_MAX_BUTTON_ALPHA, - visible: isVisible && !isMinimized + visible: false }); - - Script.setTimeout(function() { - checkLoggedIn() - }, 0); - Controller.mousePressEvent.connect(onMousePressEvent); Controller.mouseMoveEvent.connect(onMouseMoveEvent); Controller.mouseReleaseEvent.connect(onMouseReleaseEvent); @@ -1148,12 +1155,10 @@ var usersWindow = (function() { pollUsers(); // Set minimized at end - setup code does not handle `minimized == false` correctly - setMinimized(Settings.getValue(SETTING_USERS_WINDOW_MINIMIZED, false)); + setMinimized(isValueTrue(Settings.getValue(SETTING_USERS_WINDOW_MINIMIZED, false))); } function tearDown() { - Settings.setValue(SETTING_USERS_WINDOW_MINIMIZED, isMinimized); - Menu.removeMenuItem(MENU_NAME, MENU_ITEM); Script.clearTimeout(usersTimer); @@ -1170,4 +1175,4 @@ var usersWindow = (function() { setUp(); Script.scriptEnding.connect(tearDown); -}()); \ No newline at end of file +}()); diff --git a/scripts/tutorials/getDomainMetadata.js b/scripts/tutorials/getDomainMetadata.js new file mode 100644 index 0000000000..54c356ae7b --- /dev/null +++ b/scripts/tutorials/getDomainMetadata.js @@ -0,0 +1,129 @@ +// +// Created by Zach Pomerantz on June 16, 2016. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var SERVER = 'https://metaverse.highfidelity.com/api/v1'; + +var OVERLAY = null; + +// Every time you enter a domain, display the domain's metadata +location.hostChanged.connect(function(host) { + print('Detected host change:', host); + + // Fetch the domain ID from the metaverse + var placeData = request(SERVER + '/places/' + host); + if (!placeData) { + print('Cannot find place name - abandoning metadata request for', host); + return; + + } + var domainID = placeData.data.place.domain.id; + print('Domain ID:', domainID); + + // Fetch the domain metadata from the metaverse + var domainData = request(SERVER + '/domains/' + domainID); + print(SERVER + '/domains/' + domainID); + if (!domainData) { + print('Cannot find domain data - abandoning metadata request for', domainID); + return; + } + var metadata = domainData.domain; + print('Domain metadata:', JSON.stringify(metadata)); + + // Display the fetched metadata in an overlay + displayMetadata(host, metadata); +}); + +Script.scriptEnding.connect(clearMetadata); + +function displayMetadata(place, metadata) { + clearMetadata(); + + var COLOR_TEXT = { red: 255, green: 255, blue: 255 }; + var COLOR_BACKGROUND = { red: 0, green: 0, blue: 0 }; + var MARGIN = 200; + var STARTING_OPACITY = 0.8; + var FADE_AFTER_SEC = 2; + var FADE_FOR_SEC = 4; + + var fade_per_sec = STARTING_OPACITY / FADE_FOR_SEC; + var properties = { + color: COLOR_TEXT, + alpha: STARTING_OPACITY, + backgroundColor: COLOR_BACKGROUND, + backgroundAlpha: STARTING_OPACITY, + font: { size: 24 }, + x: MARGIN, + y: MARGIN + }; + + // Center the overlay on the screen + properties.width = Window.innerWidth - MARGIN*2; + properties.height = Window.innerHeight - MARGIN*2; + + // Parse the metadata into text + parsed = [ 'Welcome to ' + place + '!',, ]; + if (metadata.description) { + parsed.push(description); + } + if (metadata.tags && metadata.tags.length) { + parsed.push('Tags: ' + metadata.tags.join(',')); + } + if (metadata.capacity) { + parsed.push('Capacity (max users): ' + metadata.capacity); + } + if (metadata.maturity) { + parsed.push('Maturity: ' + metadata.maturity); + } + if (metadata.hosts && metadata.hosts.length) { + parsed.push('Hosts: ' + metadata.tags.join(',')); + } + if (metadata.online_users) { + parsed.push('Users online: ' + metadata.online_users); + } + + properties.text = parsed.join('\n\n'); + + // Display the overlay + OVERLAY = Overlays.addOverlay('text', properties); + + // Fade out the overlay over 10 seconds + !function() { + var overlay = OVERLAY; + var alpha = STARTING_OPACITY; + + var fade = function() { + // Only fade so long as the same overlay is up + if (overlay == OVERLAY) { + alpha -= fade_per_sec / 10; + if (alpha <= 0) { + clearMetadata(); + } else { + Overlays.editOverlay(overlay, { alpha: alpha, backgroundAlpha: alpha }); + Script.setTimeout(fade, 100); + } + } + }; + Script.setTimeout(fade, FADE_AFTER_SEC * 1000); + }(); +} + +function clearMetadata() { + if (OVERLAY) { + Overlays.deleteOverlay(OVERLAY); + } +} + +// Request JSON from a url, synchronously +function request(url) { + var req = new XMLHttpRequest(); + req.responseType = 'json'; + req.open('GET', url, false); + req.send(); + return req.status == 200 ? req.response : null; +} + diff --git a/tests/controllers/CMakeLists.txt b/tests/controllers/CMakeLists.txt index cf1152da02..3aac4db0a8 100644 --- a/tests/controllers/CMakeLists.txt +++ b/tests/controllers/CMakeLists.txt @@ -6,7 +6,7 @@ setup_hifi_project(Script Qml) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries -link_hifi_libraries(shared gl script-engine plugins render-utils input-plugins display-plugins controllers) +link_hifi_libraries(shared gl script-engine plugins render-utils ui-plugins input-plugins display-plugins controllers) if (WIN32) diff --git a/tests/controllers/src/main.cpp b/tests/controllers/src/main.cpp index 36ed566ea7..15b768bb36 100644 --- a/tests/controllers/src/main.cpp +++ b/tests/controllers/src/main.cpp @@ -34,7 +34,7 @@ #include #include -#include +#include #include #include #include @@ -90,8 +90,8 @@ public: virtual MainWindow* getPrimaryWindow() override { return nullptr; } virtual QOpenGLContext* getPrimaryContext() override { return nullptr; } virtual ui::Menu* getPrimaryMenu() override { return nullptr; } - virtual bool isForeground() override { return true; } - virtual const DisplayPluginPointer getActiveDisplayPlugin() const override { return DisplayPluginPointer(); } + virtual bool isForeground() const override { return true; } + virtual DisplayPluginPointer getActiveDisplayPlugin() const override { return DisplayPluginPointer(); } }; class MyControllerScriptingInterface : public controller::ScriptingInterface { diff --git a/tests/gpu-test/src/TestFbx.cpp b/tests/gpu-test/src/TestFbx.cpp new file mode 100644 index 0000000000..cea356e125 --- /dev/null +++ b/tests/gpu-test/src/TestFbx.cpp @@ -0,0 +1,187 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// 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 "TestFbx.h" + +#include + +#include + +#include + +struct MyVertex { + vec3 position; + vec2 texCoords; + vec3 normal; + uint32_t color; + + static gpu::Stream::FormatPointer getVertexFormat() { + static const gpu::Element POSITION_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; + static const gpu::Element TEXTURE_ELEMENT { gpu::VEC2, gpu::FLOAT, gpu::UV }; + static const gpu::Element NORMAL_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; + static const gpu::Element COLOR_ELEMENT { gpu::VEC4, gpu::NUINT8, gpu::RGBA }; + gpu::Stream::FormatPointer vertexFormat { std::make_shared() }; + vertexFormat->setAttribute(gpu::Stream::POSITION, 0, POSITION_ELEMENT, offsetof(MyVertex, position)); + vertexFormat->setAttribute(gpu::Stream::TEXCOORD, 0, TEXTURE_ELEMENT, offsetof(MyVertex, texCoords)); + vertexFormat->setAttribute(gpu::Stream::COLOR, 0, COLOR_ELEMENT, offsetof(MyVertex, color)); + vertexFormat->setAttribute(gpu::Stream::NORMAL, 0, NORMAL_ELEMENT, offsetof(MyVertex, normal)); + return vertexFormat; + } + +}; + +struct Part { + size_t baseVertex; + size_t baseIndex; + size_t materialId; +}; + +struct DrawElementsIndirectCommand { + uint count { 0 }; + uint instanceCount { 1 }; + uint firstIndex { 0 }; + uint baseVertex { 0 }; + uint baseInstance { 0 }; +}; + + +class FileDownloader : public QObject { + Q_OBJECT +public: + explicit FileDownloader(QUrl imageUrl, QObject *parent = 0) : QObject(parent) { + connect(&m_WebCtrl, SIGNAL(finished(QNetworkReply*)), this, SLOT(fileDownloaded(QNetworkReply*))); + QNetworkRequest request(imageUrl); + m_WebCtrl.get(request); + } + + virtual ~FileDownloader() {} + + const QByteArray& downloadedData() const { + return m_DownloadedData; + } + +signals: + void downloaded(); + +private slots: + void fileDownloaded(QNetworkReply* pReply) { + m_DownloadedData = pReply->readAll(); + pReply->deleteLater(); + emit downloaded(); + } + +private: + QNetworkAccessManager m_WebCtrl; + QByteArray m_DownloadedData; +}; + + +static const QUrl TEST_ASSET = QString("https://s3.amazonaws.com/DreamingContent/assets/models/tardis/console.fbx"); +static const mat4 TEST_ASSET_TRANSFORM = glm::translate(mat4(), vec3(0, -1.5f, 0)) * glm::scale(mat4(), vec3(0.01f)); +//static const QUrl TEST_ASSET = QString("https://s3.amazonaws.com/DreamingContent/assets/simple/SimpleMilitary/Models/Vehicles/tank_02_c.fbx"); +//static const mat4 TEST_ASSET_TRANSFORM = glm::translate(mat4(), vec3(0, -0.5f, 0)) * glm::scale(mat4(), vec3(0.1f)); + +TestFbx::TestFbx(const render::ShapePlumberPointer& shapePlumber) : _shapePlumber(shapePlumber) { + FileDownloader* downloader = new FileDownloader(TEST_ASSET, qApp); + QObject::connect(downloader, &FileDownloader::downloaded, [this, downloader] { + parseFbx(downloader->downloadedData()); + }); +} + +bool TestFbx::isReady() const { + return _partCount != 0; +} + +void TestFbx::parseFbx(const QByteArray& fbxData) { + QVariantHash mapping; + FBXGeometry* fbx = readFBX(fbxData, mapping); + size_t totalVertexCount = 0; + size_t totalIndexCount = 0; + size_t totalPartCount = 0; + size_t highestIndex = 0; + for (const auto& mesh : fbx->meshes) { + size_t vertexCount = mesh.vertices.size(); + totalVertexCount += mesh.vertices.size(); + highestIndex = std::max(highestIndex, vertexCount); + totalPartCount += mesh.parts.size(); + for (const auto& part : mesh.parts) { + totalIndexCount += part.quadTrianglesIndices.size(); + totalIndexCount += part.triangleIndices.size(); + } + } + size_t baseVertex = 0; + std::vector vertices; + vertices.reserve(totalVertexCount); + std::vector indices; + indices.reserve(totalIndexCount); + std::vector parts; + parts.reserve(totalPartCount); + _partCount = totalPartCount; + for (const auto& mesh : fbx->meshes) { + baseVertex = vertices.size(); + + vec3 color; + for (const auto& part : mesh.parts) { + DrawElementsIndirectCommand partIndirect; + partIndirect.baseVertex = (uint)baseVertex; + partIndirect.firstIndex = (uint)indices.size(); + partIndirect.baseInstance = (uint)parts.size(); + _partTransforms.push_back(mesh.modelTransform); + auto material = fbx->materials[part.materialID]; + color = material.diffuseColor; + for (auto index : part.quadTrianglesIndices) { + indices.push_back(index); + } + for (auto index : part.triangleIndices) { + indices.push_back(index); + } + size_t triangles = (indices.size() - partIndirect.firstIndex); + Q_ASSERT(0 == (triangles % 3)); + //triangles /= 3; + partIndirect.count = (uint)triangles; + parts.push_back(partIndirect); + } + + size_t vertexCount = mesh.vertices.size(); + for (size_t i = 0; i < vertexCount; ++i) { + MyVertex vertex; + vertex.position = mesh.vertices[(int)i]; + vec3 n = mesh.normals[(int)i]; + vertex.normal = n; + vertex.texCoords = mesh.texCoords[(int)i]; + vertex.color = toCompactColor(vec4(color, 1)); + vertices.push_back(vertex); + } + } + + _vertexBuffer->append(vertices); + _indexBuffer->append(indices); + _indirectBuffer->append(parts); + delete fbx; +} + +void TestFbx::renderTest(size_t testId, RenderArgs* args) { + gpu::Batch& batch = *(args->_batch); + //pipeline->pipeline + if (_partCount) { + for (size_t i = 0; i < _partCount; ++i) { + batch.setModelTransform(TEST_ASSET_TRANSFORM * _partTransforms[i]); + batch.setupNamedCalls(__FUNCTION__, [this](gpu::Batch& batch, gpu::Batch::NamedBatchData&) { + RenderArgs args; args._batch = &batch; + _shapePlumber->pickPipeline(&args, render::ShapeKey()); + batch.setInputBuffer(0, _vertexBuffer, 0, sizeof(MyVertex)); + batch.setIndexBuffer(gpu::UINT16, _indexBuffer, 0); + batch.setInputFormat(MyVertex::getVertexFormat()); + batch.setIndirectBuffer(_indirectBuffer, 0); + batch.multiDrawIndexedIndirect((uint)_partCount, gpu::TRIANGLES); + }); + } + } +} + +#include "TestFbx.moc" diff --git a/tests/gpu-test/src/TestFbx.h b/tests/gpu-test/src/TestFbx.h new file mode 100644 index 0000000000..1f3c0bb50e --- /dev/null +++ b/tests/gpu-test/src/TestFbx.h @@ -0,0 +1,35 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// 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 +// +#pragma once + +#include "TestHelpers.h" + +#include + +class FBXGeometry; + +class TestFbx : public GpuTestBase { + size_t _partCount { 0 }; + model::Material _material; + render::ShapeKey _shapeKey; + std::vector _partTransforms; + render::ShapePlumberPointer _shapePlumber; + gpu::Stream::FormatPointer _vertexFormat { std::make_shared() }; + gpu::BufferPointer _vertexBuffer { std::make_shared() }; + gpu::BufferPointer _indexBuffer { std::make_shared() }; + gpu::BufferPointer _indirectBuffer { std::make_shared() }; +public: + TestFbx(const render::ShapePlumberPointer& shapePlumber); + bool isReady() const override; + void renderTest(size_t test, RenderArgs* args) override; + +private: + void parseFbx(const QByteArray& fbxData); +}; + + diff --git a/tests/gpu-test/src/TestFloorGrid.cpp b/tests/gpu-test/src/TestFloorGrid.cpp new file mode 100644 index 0000000000..a24b4778d4 --- /dev/null +++ b/tests/gpu-test/src/TestFloorGrid.cpp @@ -0,0 +1,54 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// 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 "TestFloorGrid.h" + + +TestFloorGrid::TestFloorGrid() { + auto geometryCache = DependencyManager::get(); + // Render grid on xz plane (not the optimal way to do things, but w/e) + // Note: GeometryCache::renderGrid will *not* work, as it is apparenly unaffected by batch rotations and renders xy only + static const std::string GRID_INSTANCE = "Grid"; + static auto compactColor1 = toCompactColor(vec4 { 0.35f, 0.25f, 0.15f, 1.0f }); + static auto compactColor2 = toCompactColor(vec4 { 0.15f, 0.25f, 0.35f, 1.0f }); + static std::vector transforms; + static gpu::BufferPointer colorBuffer; + if (!transforms.empty()) { + transforms.reserve(200); + colorBuffer = std::make_shared(); + for (int i = 0; i < 100; ++i) { + { + glm::mat4 transform = glm::translate(mat4(), vec3(0, -1, -50 + i)); + transform = glm::scale(transform, vec3(100, 1, 1)); + transforms.push_back(transform); + colorBuffer->append(compactColor1); + } + + { + glm::mat4 transform = glm::mat4_cast(quat(vec3(0, PI / 2.0f, 0))); + transform = glm::translate(transform, vec3(0, -1, -50 + i)); + transform = glm::scale(transform, vec3(100, 1, 1)); + transforms.push_back(transform); + colorBuffer->append(compactColor2); + } + } + } + +} + +void TestFloorGrid::renderTest(size_t testId, RenderArgs* args) { + //gpu::Batch& batch = *(args->_batch); + //auto pipeline = geometryCache->getSimplePipeline(); + //for (auto& transform : transforms) { + // batch.setModelTransform(transform); + // batch.setupNamedCalls(GRID_INSTANCE, [=](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) { + // batch.setPipeline(_pipeline); + // geometryCache->renderWireShapeInstances(batch, GeometryCache::Line, data.count(), colorBuffer); + // }); + //} +} diff --git a/tests/gpu-test/src/TestFloorGrid.h b/tests/gpu-test/src/TestFloorGrid.h new file mode 100644 index 0000000000..566c8f0968 --- /dev/null +++ b/tests/gpu-test/src/TestFloorGrid.h @@ -0,0 +1,26 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// 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 +// + +#pragma once + +#include +#include +#include + +#include +#include + +#include "TestHelpers.h" + +class TestFloorGrid : public GpuTestBase { +public: + TestFloorGrid(); + void renderTest(size_t testId, RenderArgs* args) override; +}; + + diff --git a/tests/gpu-test/src/TestFloorTexture.cpp b/tests/gpu-test/src/TestFloorTexture.cpp new file mode 100644 index 0000000000..884be5ec30 --- /dev/null +++ b/tests/gpu-test/src/TestFloorTexture.cpp @@ -0,0 +1,88 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// 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 "TestFloorTexture.h" + +struct Vertex { + vec4 position; + vec4 texture; + vec4 normal; + vec4 color; +}; +static const uint TEXTURE_OFFSET = offsetof(Vertex, texture); +static const uint NORMAL_OFFSET = offsetof(Vertex, normal); +static const uint POSITION_OFFSET = offsetof(Vertex, position); +static const uint COLOR_OFFSET = offsetof(Vertex, color); +static const gpu::Element POSITION_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +static const gpu::Element TEXTURE_ELEMENT { gpu::VEC2, gpu::FLOAT, gpu::UV }; +static const gpu::Element NORMAL_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ }; +static const gpu::Element COLOR_ELEMENT { gpu::VEC4, gpu::FLOAT, gpu::RGBA }; + +FloorTextureTest::FloorTextureTest() { + auto geometryCache = DependencyManager::get(); + std::vector vertices; + const int MINX = -1000; + const int MAXX = 1000; + + vertices.push_back({ + vec4(MAXX, 0, MAXX, 1), + vec4(MAXX, MAXX, 0, 0), + vec4(0, 1, 0, 1), + vec4(1), + }); + + vertices.push_back({ + vec4(MAXX, 0, MINX, 1), + vec4(MAXX, 0, 0, 0), + vec4(0, 1, 0, 1), + vec4(1), + }); + + vertices.push_back({ + vec4(MINX, 0, MINX, 1), + vec4(0, 0, 0, 0), + vec4(0, 1, 0, 1), + vec4(1), + }); + + vertices.push_back({ + vec4(MINX, 0, MAXX, 1), + vec4(0, MAXX, 0, 0), + vec4(0, 1, 0, 1), + vec4(1), + }); + + vertexBuffer->append(vertices); + indexBuffer->append(std::vector({ 0, 1, 2, 2, 3, 0 })); + texture = DependencyManager::get()->getImageTexture("C:/Users/bdavis/Git/openvr/samples/bin/cube_texture.png"); + //texture = DependencyManager::get()->getImageTexture("H:/test.png"); + //texture = DependencyManager::get()->getImageTexture("H:/crate_blue.fbm/lambert8SG_Normal_OpenGL.png"); + vertexFormat->setAttribute(gpu::Stream::POSITION, 0, POSITION_ELEMENT, POSITION_OFFSET); + vertexFormat->setAttribute(gpu::Stream::TEXCOORD, 0, TEXTURE_ELEMENT, TEXTURE_OFFSET); + vertexFormat->setAttribute(gpu::Stream::COLOR, 0, COLOR_ELEMENT, COLOR_OFFSET); + vertexFormat->setAttribute(gpu::Stream::NORMAL, 0, NORMAL_ELEMENT, NORMAL_OFFSET); +} + +void FloorTextureTest::renderTest(size_t testId, RenderArgs* args) { + gpu::Batch& batch = *(args->_batch); + auto geometryCache = DependencyManager::get(); + static auto start = usecTimestampNow(); + auto now = usecTimestampNow(); + if ((now - start) > USECS_PER_SECOND * 1) { + start = now; + texture->incremementMinMip(); + } + + geometryCache->bindSimpleProgram(batch, true, true, true); + batch.setInputBuffer(0, vertexBuffer, 0, sizeof(Vertex)); + batch.setInputFormat(vertexFormat); + batch.setIndexBuffer(gpu::UINT16, indexBuffer, 0); + batch.setResourceTexture(0, texture); + batch.setModelTransform(glm::translate(glm::mat4(), vec3(0, -0.1, 0))); + batch.drawIndexed(gpu::TRIANGLES, 6, 0); +} diff --git a/tests/gpu-test/src/TestFloorTexture.h b/tests/gpu-test/src/TestFloorTexture.h new file mode 100644 index 0000000000..99eaf902b8 --- /dev/null +++ b/tests/gpu-test/src/TestFloorTexture.h @@ -0,0 +1,22 @@ +// +// Created by Bradley Austin Davis on 2016/05/16 +// 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 +// +#pragma once + +#include "TestHelpers.h" + +class FloorTextureTest : public GpuTestBase { + gpu::BufferPointer vertexBuffer { std::make_shared() }; + gpu::BufferPointer indexBuffer { std::make_shared() }; + gpu::Stream::FormatPointer vertexFormat { std::make_shared() }; + gpu::TexturePointer texture; +public: + FloorTextureTest(); + void renderTest(size_t testId, RenderArgs* args) override; +}; + + diff --git a/tests/gpu-test/src/main.cpp b/tests/gpu-test/src/main.cpp index e672fe3c86..0f06546327 100644 --- a/tests/gpu-test/src/main.cpp +++ b/tests/gpu-test/src/main.cpp @@ -62,6 +62,9 @@ #include #include "TestWindow.h" +#include "TestFbx.h" +#include "TestFloorGrid.h" +#include "TestFloorTexture.h" #include "TestInstancedShapes.h" #include "TestShapes.h" @@ -90,8 +93,8 @@ class MyTestWindow : public TestWindow { #endif updateCamera(); _testBuilders = TestBuilders({ - //[this] { return new TestFbx(_shapePlumber); }, - [] { return new TestShapes(); }, + [this] { return new TestFbx(_shapePlumber); }, + [] { return new TestInstancedShapes(); }, }); } diff --git a/tests/gpu-test/src/unlit.slf b/tests/gpu-test/src/unlit.slf deleted file mode 100644 index f88fcb510b..0000000000 --- a/tests/gpu-test/src/unlit.slf +++ /dev/null @@ -1,28 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// simple.frag -// fragment shader -// -// Created by Andrzej Kapolka on 9/15/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -<@include DeferredBufferWrite.slh@> -<@include model/Material.slh@> - -// the interpolated normal -in vec3 _normal; -in vec3 _color; - -void main(void) { - packDeferredFragment( - normalize(_normal.xyz), - 1.0, - _color.rgb, - DEFAULT_ROUGHNESS, DEFAULT_METALLIC, DEFAULT_EMISSIVE, DEFAULT_OCCLUSION); -} diff --git a/tests/gpu-test/src/unlit.slv b/tests/gpu-test/src/unlit.slv deleted file mode 100644 index d51d817429..0000000000 --- a/tests/gpu-test/src/unlit.slv +++ /dev/null @@ -1,36 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// simple.vert -// vertex shader -// -// Created by Andrzej Kapolka on 9/15/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -<@include gpu/Inputs.slh@> - -<@include gpu/Transform.slh@> - -<$declareStandardTransform()$> - -// the interpolated normal -out vec3 _normal; -out vec3 _color; -out vec2 _texCoord0; - -void main(void) { - _color = inColor.rgb; - _texCoord0 = inTexCoord0.st; - - // standard transform - TransformCamera cam = getTransformCamera(); - TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> - <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> - _normal = vec3(0.0, 0.0, 1.0); -} \ No newline at end of file diff --git a/tests/physics/src/ShapeManagerTests.cpp b/tests/physics/src/ShapeManagerTests.cpp index 66ac9d0c4a..c8805132fa 100644 --- a/tests/physics/src/ShapeManagerTests.cpp +++ b/tests/physics/src/ShapeManagerTests.cpp @@ -194,23 +194,23 @@ void ShapeManagerTests::addCompoundShape() { int numHullPoints = tetrahedron.size(); // compute the points of the hulls - QVector< QVector > hulls; + ShapeInfo::PointCollection pointCollection; int numHulls = 5; glm::vec3 offsetNormal(1.0f, 0.0f, 0.0f); for (int i = 0; i < numHulls; ++i) { glm::vec3 offset = (float)(i - numHulls/2) * offsetNormal; - QVector hull; + ShapeInfo::PointList pointList; float radius = (float)(i + 1); for (int j = 0; j < numHullPoints; ++j) { glm::vec3 point = radius * tetrahedron[j] + offset; - hull.push_back(point); + pointList.push_back(point); } - hulls.push_back(hull); + pointCollection.push_back(pointList); } // create the ShapeInfo ShapeInfo info; - info.setConvexHulls(hulls); + info.setPointCollection(hulls); // create the shape ShapeManager shapeManager; diff --git a/tests/ui/qml/Stubs.qml b/tests/ui/qml/Stubs.qml index 8f828a0186..8c1465d54c 100644 --- a/tests/ui/qml/Stubs.qml +++ b/tests/ui/qml/Stubs.qml @@ -23,11 +23,23 @@ Item { function getUsername() { return "Jherico"; } } + Item { + objectName: "GL" + property string vendor: "" + } + Item { objectName: "ApplicationCompositor" property bool reticleOverDesktop: true } + Item { + objectName: "Controller" + function getRecommendedOverlayRect() { + return Qt.rect(0, 0, 1920, 1080); + } + } + Item { objectName: "Preferences" // List of categories obtained by logging categories as they are added in Interface in Preferences::addPreference(). diff --git a/tests/ui/qml/main.qml b/tests/ui/qml/main.qml index 97f0c0a613..47d0f6d601 100644 --- a/tests/ui/qml/main.qml +++ b/tests/ui/qml/main.qml @@ -5,7 +5,7 @@ import Qt.labs.settings 1.0 import "../../../interface/resources/qml" //import "../../../interface/resources/qml/windows" -import "../../../interface/resources/qml/windows-uit" +import "../../../interface/resources/qml/windows" import "../../../interface/resources/qml/dialogs" import "../../../interface/resources/qml/hifi" import "../../../interface/resources/qml/hifi/dialogs" @@ -17,6 +17,311 @@ ApplicationWindow { width: 1280 height: 800 title: qsTr("Scratch App") + toolBar: Row { + id: testButtons + anchors { margins: 8; left: parent.left; top: parent.top } + spacing: 8 + property int count: 0 + + property var tabs: []; + property var urls: []; + property var toolbar; + property var lastButton; + + // Window visibility + Button { + text: "toggle desktop" + onClicked: desktop.togglePinned() + } + + Button { + text: "Create Toolbar" + onClicked: testButtons.toolbar = desktop.getToolbar("com.highfidelity.interface.toolbar.system"); + } + + Button { + text: "Toggle Toolbar Direction" + onClicked: testButtons.toolbar.horizontal = !testButtons.toolbar.horizontal + } + + Button { + readonly property var icons: [ + "edit-01.svg", + "model-01.svg", + "cube-01.svg", + "sphere-01.svg", + "light-01.svg", + "text-01.svg", + "web-01.svg", + "zone-01.svg", + "particle-01.svg", + ] + property int iconIndex: 0 + readonly property string toolIconUrl: "../../../../../scripts/system/assets/images/tools/" + text: "Create Button" + onClicked: { + var name = icons[iconIndex]; + var url = toolIconUrl + name; + iconIndex = (iconIndex + 1) % icons.length; + var button = testButtons.lastButton = testButtons.toolbar.addButton({ + imageURL: url, + objectName: name, + subImage: { + y: 50, + }, + alpha: 0.9 + }); + + button.clicked.connect(function(){ + console.log("Clicked on button " + button.imageURL + " alpha " + button.alpha) + }); + } + } + + Button { + text: "Toggle Button Visible" + onClicked: testButtons.lastButton.visible = !testButtons.lastButton.visible + } + + + + + // Error alerts + /* + Button { + // Message without title. + text: "Show Error" + onClicked: { + var messageBox = desktop.messageBox({ + text: "Diagnostic cycle will be complete in 30 seconds", + icon: hifi.icons.critical, + }); + messageBox.selected.connect(function(button) { + console.log("You clicked " + button) + }) + } + } + Button { + // detailedText is not currently used anywhere in Interface but it is easier to leave in and style good enough. + text: "Show Long Error" + onClicked: { + desktop.messageBox({ + informativeText: "Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds ", + text: "Baloney", + icon: hifi.icons.warning, + detailedText: "sakjd;laskj dksa;dl jka;lsd j;lkjas ;dlkaj s;dlakjd ;alkjda; slkjda; lkjda;lksjd ;alksjd; alksjd ;alksjd; alksjd; alksdjas;ldkjas;lkdja ;kj ;lkasjd; lkj as;dlka jsd;lka jsd;laksjd a" + }); + } + } + */ + + // query + /* + // There is no such desktop.queryBox() function; may need to update test to cover QueryDialog.qml? + Button { + text: "Show Query" + onClicked: { + var queryBox = desktop.queryBox({ + text: "Have you stopped beating your wife?", + placeholderText: "Are you sure?", + // icon: hifi.icons.critical, + }); + queryBox.selected.connect(function(result) { + console.log("User responded with " + result); + }); + + queryBox.canceled.connect(function() { + console.log("User cancelled query box "); + }) + } + } + */ + + // Browser + /* + Button { + text: "Open Browser" + onClicked: builder.createObject(desktop); + property var builder: Component { + Browser {} + } + } + */ + + + // file dialog + /* + + Button { + text: "Open Directory" + property var builder: Component { + FileDialog { selectDirectory: true } + } + + onClicked: { + var fileDialog = builder.createObject(desktop); + fileDialog.canceled.connect(function(){ + console.log("Cancelled") + }) + fileDialog.selectedFile.connect(function(file){ + console.log("Selected " + file) + }) + } + } + Button { + text: "Open File" + property var builder: Component { + FileDialog { + title: "Open File" + filter: "All Files (*.*)" + //filter: "HTML files (*.html);;Other(*.png)" + } + } + + onClicked: { + var fileDialog = builder.createObject(desktop); + fileDialog.canceled.connect(function(){ + console.log("Cancelled") + }) + fileDialog.selectedFile.connect(function(file){ + console.log("Selected " + file) + }) + } + } + */ + + // tabs + /* + Button { + text: "Add Tab" + onClicked: { + console.log(desktop.toolWindow); + desktop.toolWindow.addWebTab({ source: "Foo" }); + desktop.toolWindow.showTabForUrl("Foo", true); + } + } + + Button { + text: "Add Tab 2" + onClicked: { + console.log(desktop.toolWindow); + desktop.toolWindow.addWebTab({ source: "Foo 2" }); + desktop.toolWindow.showTabForUrl("Foo 2", true); + } + } + + Button { + text: "Add Tab 3" + onClicked: { + console.log(desktop.toolWindow); + desktop.toolWindow.addWebTab({ source: "Foo 3" }); + desktop.toolWindow.showTabForUrl("Foo 3", true); + } + } + + Button { + text: "Destroy Tab" + onClicked: { + console.log(desktop.toolWindow); + desktop.toolWindow.removeTabForUrl("Foo"); + } + } + */ + + // Hifi specific stuff + /* + Button { + // Shows the dialog with preferences sections but not each section's preference items + // because Preferences.preferencesByCategory() method is not stubbed out. + text: "Settings > General..." + property var builder: Component { + GeneralPreferencesDialog { } + } + onClicked: { + var runningScripts = builder.createObject(desktop); + } + } + + Button { + text: "Running Scripts" + property var builder: Component { + RunningScripts { } + } + onClicked: { + var runningScripts = builder.createObject(desktop); + } + } + + Button { + text: "Attachments" + property var builder: Component { + AttachmentsDialog { } + } + onClicked: { + var attachmentsDialog = builder.createObject(desktop); + } + } + Button { + // Replicates message box that pops up after selecting new avatar. Includes title. + text: "Confirm Avatar" + onClicked: { + var messageBox = desktop.messageBox({ + title: "Set Avatar", + text: "Would you like to use 'Albert' for your avatar?", + icon: hifi.icons.question, // Test question icon + //icon: hifi.icons.information, // Test informaton icon + //icon: hifi.icons.warning, // Test warning icon + //icon: hifi.icons.critical, // Test critical icon + //icon: hifi.icons.none, // Test no icon + buttons: OriginalDialogs.StandardButton.Ok + OriginalDialogs.StandardButton.Cancel, + defaultButton: OriginalDialogs.StandardButton.Ok + }); + messageBox.selected.connect(function(button) { + console.log("You clicked " + button) + }) + } + } + */ + // bookmarks + /* + Button { + text: "Bookmark Location" + onClicked: { + desktop.inputDialog({ + title: "Bookmark Location", + icon: hifi.icons.placemark, + label: "Name" + }); + } + } + Button { + text: "Delete Bookmark" + onClicked: { + desktop.inputDialog({ + title: "Delete Bookmark", + icon: hifi.icons.placemark, + label: "Select the bookmark to delete", + items: ["Bookmark A", "Bookmark B", "Bookmark C"] + }); + } + } + Button { + text: "Duplicate Bookmark" + onClicked: { + desktop.messageBox({ + title: "Duplicate Bookmark", + icon: hifi.icons.warning, + text: "The bookmark name you entered alread exists in yoru list.", + informativeText: "Would you like to overwrite it?", + buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No, + defaultButton: OriginalDialogs.StandardButton.Yes + }); + } + } + */ + + } + HifiConstants { id: hifi } @@ -35,249 +340,13 @@ ApplicationWindow { } */ - Row { - id: testButtons - anchors { margins: 8; left: parent.left; top: parent.top } - spacing: 8 - property int count: 0 - - property var tabs: []; - property var urls: []; - - Button { - // Shows the dialog with preferences sections but not each section's preference items - // because Preferences.preferencesByCategory() method is not stubbed out. - text: "Settings > General..." - property var builder: Component { - GeneralPreferencesDialog { } - } - onClicked: { - var runningScripts = builder.createObject(desktop); - } - } - - Button { - text: "Running Scripts" - property var builder: Component { - RunningScripts { } - } - onClicked: { - var runningScripts = builder.createObject(desktop); - } - } - - Button { - text: "Attachments" - property var builder: Component { - AttachmentsDialog { } - } - onClicked: { - var attachmentsDialog = builder.createObject(desktop); - } - } - - /* - Button { - text: "restore all" - onClicked: { - for (var i = 0; i < desktop.windows.length; ++i) { - desktop.windows[i].visible = true - } - } - } - Button { - text: "toggle blue visible" - onClicked: { - blue.visible = !blue.visible - } - } - Button { - text: "toggle blue enabled" - onClicked: { - blue.enabled = !blue.enabled - } - } - */ - Button { - // Replicates message box that pops up after selecting new avatar. Includes title. - text: "Confirm Avatar" - onClicked: { - var messageBox = desktop.messageBox({ - title: "Set Avatar", - text: "Would you like to use 'Albert' for your avatar?", - icon: hifi.icons.question, // Test question icon - //icon: hifi.icons.information, // Test informaton icon - //icon: hifi.icons.warning, // Test warning icon - //icon: hifi.icons.critical, // Test critical icon - //icon: hifi.icons.none, // Test no icon - buttons: OriginalDialogs.StandardButton.Ok + OriginalDialogs.StandardButton.Cancel, - defaultButton: OriginalDialogs.StandardButton.Ok - }); - messageBox.selected.connect(function(button) { - console.log("You clicked " + button) - }) - } - } - Button { - // Message without title. - text: "Show Error" - onClicked: { - var messageBox = desktop.messageBox({ - text: "Diagnostic cycle will be complete in 30 seconds", - icon: hifi.icons.critical, - }); - messageBox.selected.connect(function(button) { - console.log("You clicked " + button) - }) - } - } - Button { - // detailedText is not currently used anywhere in Interface but it is easier to leave in and style good enough. - text: "Show Long Error" - onClicked: { - desktop.messageBox({ - informativeText: "Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds ", - text: "Baloney", - icon: hifi.icons.warning, - detailedText: "sakjd;laskj dksa;dl jka;lsd j;lkjas ;dlkaj s;dlakjd ;alkjda; slkjda; lkjda;lksjd ;alksjd; alksjd ;alksjd; alksjd; alksdjas;ldkjas;lkdja ;kj ;lkasjd; lkj as;dlka jsd;lka jsd;laksjd a" - }); - } - } - Button { - text: "Bookmark Location" - onClicked: { - desktop.inputDialog({ - title: "Bookmark Location", - icon: hifi.icons.placemark, - label: "Name" - }); - } - } - Button { - text: "Delete Bookmark" - onClicked: { - desktop.inputDialog({ - title: "Delete Bookmark", - icon: hifi.icons.placemark, - label: "Select the bookmark to delete", - items: ["Bookmark A", "Bookmark B", "Bookmark C"] - }); - } - } - Button { - text: "Duplicate Bookmark" - onClicked: { - desktop.messageBox({ - title: "Duplicate Bookmark", - icon: hifi.icons.warning, - text: "The bookmark name you entered alread exists in yoru list.", - informativeText: "Would you like to overwrite it?", - buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No, - defaultButton: OriginalDialogs.StandardButton.Yes - }); - } - } - /* - // There is no such desktop.queryBox() function; may need to update test to cover QueryDialog.qml? - Button { - text: "Show Query" - onClicked: { - var queryBox = desktop.queryBox({ - text: "Have you stopped beating your wife?", - placeholderText: "Are you sure?", - // icon: hifi.icons.critical, - }); - queryBox.selected.connect(function(result) { - console.log("User responded with " + result); - }); - - queryBox.canceled.connect(function() { - console.log("User cancelled query box "); - }) - } - } - */ - Button { - text: "Open Directory" - property var builder: Component { - FileDialog { selectDirectory: true } - } - - onClicked: { - var fileDialog = builder.createObject(desktop); - fileDialog.canceled.connect(function(){ - console.log("Cancelled") - }) - fileDialog.selectedFile.connect(function(file){ - console.log("Selected " + file) - }) - } - } - - Button { - text: "Open File" - property var builder: Component { - FileDialog { - title: "Open File" - filter: "All Files (*.*)" - //filter: "HTML files (*.html);;Other(*.png)" - } - } - - onClicked: { - var fileDialog = builder.createObject(desktop); - fileDialog.canceled.connect(function(){ - console.log("Cancelled") - }) - fileDialog.selectedFile.connect(function(file){ - console.log("Selected " + file) - }) - } - } - - Button { - text: "Add Tab" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo" }); - desktop.toolWindow.showTabForUrl("Foo", true); - } - } - - Button { - text: "Add Tab 2" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo 2" }); - desktop.toolWindow.showTabForUrl("Foo 2", true); - } - } - - Button { - text: "Add Tab 3" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo 3" }); - desktop.toolWindow.showTabForUrl("Foo 3", true); - } - } - - Button { - text: "Destroy Tab" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.removeTabForUrl("Foo"); - } - } - - } /* Window { id: blue closable: true visible: true resizable: true - destroyOnInvisible: false + destroyOnHidden: false width: 100; height: 100 x: 1280 / 2; y: 720 / 2 @@ -296,32 +365,35 @@ ApplicationWindow { Component.onDestruction: console.log("Blue destroyed") } } - */ - /* + + + Rectangle { width: 100; height: 100; x: 100; y: 100; color: "#00f" } + Window { id: green alwaysOnTop: true + frame: HiddenFrame{} + hideBackground: true closable: true visible: true resizable: false x: 1280 / 2; y: 720 / 2 - Settings { - category: "TestWindow.Green" - property alias x: green.x - property alias y: green.y - property alias width: green.width - property alias height: green.height - } width: 100; height: 100 - Rectangle { anchors.fill: parent; color: "green" } + Rectangle { + color: "#0f0" + width: green.width; + height: green.height; + } } + */ +/* Window { id: yellow - objectName: "Yellow" closable: true visible: true resizable: true + x: 100; y: 100 width: 100; height: 100 Rectangle { anchors.fill: parent @@ -329,10 +401,11 @@ ApplicationWindow { color: "yellow" } } - */ +*/ } Action { + id: openBrowserAction text: "Open Browser" shortcut: "Ctrl+Shift+X" onTriggered: { @@ -343,3 +416,7 @@ ApplicationWindow { } } } + + + + diff --git a/tests/ui/qmlscratch.pro b/tests/ui/qmlscratch.pro index 417d7dad5b..151893de2f 100644 --- a/tests/ui/qmlscratch.pro +++ b/tests/ui/qmlscratch.pro @@ -18,6 +18,7 @@ DISTFILES += \ qml/*.qml \ ../../interface/resources/qml/*.qml \ ../../interface/resources/qml/controls/*.qml \ + ../../interface/resources/qml/controls-uit/*.qml \ ../../interface/resources/qml/dialogs/*.qml \ ../../interface/resources/qml/dialogs/fileDialog/*.qml \ ../../interface/resources/qml/dialogs/preferences/*.qml \ @@ -25,9 +26,10 @@ DISTFILES += \ ../../interface/resources/qml/desktop/*.qml \ ../../interface/resources/qml/menus/*.qml \ ../../interface/resources/qml/styles/*.qml \ + ../../interface/resources/qml/styles-uit/*.qml \ ../../interface/resources/qml/windows/*.qml \ ../../interface/resources/qml/hifi/*.qml \ + ../../interface/resources/qml/hifi/toolbars/*.qml \ ../../interface/resources/qml/hifi/dialogs/*.qml \ ../../interface/resources/qml/hifi/dialogs/preferences/*.qml \ ../../interface/resources/qml/hifi/overlays/*.qml - diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 0cabfe28f5..e3cf37ba04 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -86,9 +86,11 @@ int main(int argc, char *argv[]) { setChild(engine, "offscreenFlags"); setChild(engine, "Account"); setChild(engine, "ApplicationCompositor"); + setChild(engine, "Controller"); setChild(engine, "Desktop"); setChild(engine, "ScriptDiscoveryService"); setChild(engine, "HMD"); + setChild(engine, "GL"); setChild(engine, "MenuHelper"); setChild(engine, "Preferences"); setChild(engine, "urlHandler"); @@ -101,3 +103,4 @@ int main(int argc, char *argv[]) { } #include "main.moc" +